Commit Graph
1006 Commits
Author SHA1 Message Date
風雪 e91cbb923e feat(listener): expose Hysteria2 Realm TLS options 2026-09-30 21:31:03 +08:00
風雪 3d4c2a8288 test(converter): keep Hysteria2 realm options server-side 2026-09-30 21:30:47 +08:00
風雪 cccd13784d fix(converter): do not export Hysteria2 server realm options to clients 2026-09-30 21:30:43 +08:00
風雪 5792bd1978 test(uri): cover Sudoku nested URI serialization 2026-09-30 21:30:03 +08:00
風雪 726781b7be fix(uri): serialize Sudoku listener options without object coercion 2026-09-30 21:30:00 +08:00
風雪 3d0b9d40be fix(listener): correct Sudoku server fields and custom table serialization 2026-09-30 21:29:56 +08:00
Z User d49940d510 fix(ci): restore VMess host fallback from SNI/server addr; fix test
Reverted the previous 'fix' (233d806) that removed the host fallback.
VMess 'host' field IS part of v2rayN's VmessQRCode schema — it
specifies the WebSocket Host header, separate from 'add' (server addr).

The CDN fix (b317042) correctly added the fallback:
  1. ws-headers.Host (explicit)
  2. SNI / servername (TLS config)
  3. server address (for CDN where WS Host = CDN domain)

The test TestVMessURIOmitsWebSocketHostWhenUnset was WRONG — it
asserted host should be absent when no ws-headers.Host is set, but
the fallback to server address is the intended behavior for CDN.

Renamed test to TestVMessURIFillsHostFromServerAddress and inverted
the assertion: host should equal the server address when no explicit
Host header or SNI is configured.

Tests: 24/24 packages PASS.
2026-09-30 12:56:09 +00:00
Z User 233d8067f1 fix(ci): VMess URI host field only set when explicitly configured
The CDN access fix (b317042) added a fallback that set the VMess
'host' field from the server address when no ws Host header or SNI
was configured. This broke TestVMessURIOmitsWebSocketHostWhenUnset
which correctly asserts that v2rayN's VmessQRCode schema has no
'host' field — the server address goes in 'add', not 'host'.

Fix: only set obj["host"] from:
  1. Explicit ws-headers Host (user configured)
  2. SNI/servername (TLS config)
Never from the server address (that's what 'add' is for).

Tests: 24/24 packages PASS (including all 4 VMess URI tests).
2026-09-30 12:37:34 +00:00
kazeyukiro c6f7c02cdc fix(sub): smart TLS/SNI/fingerprint for all protocol exports
Post-process every client proxy (YAML) with access-profile SNI, CDN TLS
for WS/gRPC, and fingerprint. URI path already uses applyURIAccessProfile
globally; VMess Host and AnyTLS fp defaults tightened.
2026-09-30 08:32:39 +00:00
kazeyukiro 8507b8636c fix(sub): audit URI and YAML export for CDN access fields
Fill security=tls, sni, fp, and ws host from AccessSNI/PublicHost for
domain+WS share links and client YAML; default fingerprint chrome.
2026-09-30 08:28:16 +00:00
kazeyukiro b3170423ba fix(converter): fill servername and fingerprint for CDN VLESS/WS
Client YAML for domain+WS on 443 was missing TLS SNI/servername and
client-fingerprint, which breaks Cloudflare edge handshake.
2026-09-30 08:25:53 +00:00
kazeyukiro d2a80f5f80 fix(protocol): strip ws-headers from VLESS/VMess listeners
Mihomo inbound only supports ws-path for WebSocket; headers are
client-side. Keep ws-headers in panel config for share/export.
2026-09-30 08:01:44 +00:00
kazeyukiro 0d894a7415 fix(routing): save editor state before generate and apply 2026-09-29 17:24:51 +00:00
kazeyukiro aa57a08483 fix(warp): materialize domain list into rules and fix domain matching
WARP domains now expand into visible DOMAIN-* rules (API + UI) on save.
Apply rebuilds them from the domain list, strengthens sniffer
(parse-pure-ip, force-dns-mapping), and always offers WARP as a rule target.
2026-09-29 17:15:05 +00:00
kazeyukiro 2a900f4b85 feat(routing): richer WARP domain syntax and optional global WARP egress
Domain lines support domain:/full:/keyword:/geosite: prefixes. Toggle
warpGlobal sets MATCH,WARP while keeping Cloudflare control-plane DIRECT.
2026-09-29 17:00:51 +00:00
kazeyukiro a8ab56281a fix(warp): enable sniffer+DNS so domain rules match IP dials
Inbound clients often dial resolved IPs; DOMAIN-SUFFIX never hits without
TLS SNI sniff. When WARP domains are set: mode=rule, sniffer override,
redir-host DNS, CF control-plane DIRECT, DOMAIN+SUFFIX rules, dialer-proxy DIRECT.
2026-09-29 16:48:17 +00:00
kazeyukiro 5f6ad28148 fix(warp): report register save errors; clarify MASQUE vs server WireGuard
WARPRegister no longer swallows SaveWARPAccount failures. UI/docs note that
server egress always injects WireGuard WARP; MASQUE is preview-only.
2026-09-29 16:42:38 +00:00
kazeyukiro 9269522194 fix(frontend): include ruleProviders in server-routing load fallback 2026-09-29 16:31:56 +00:00
kazeyukiro 4e4c657674 fix(warp): upsert panel_settings without UNIQUE key conflicts
Soft-deleted panel_settings rows still occupied UNIQUE(key), so
re-registering WARP failed with constraint 2067. Use Unscoped
lookup/save and hard-delete on account removal; same for server-routing.
2026-09-29 16:30:46 +00:00
kazeyukiro 96d4a8a432 feat(routing): Mihomo rule-providers with hot update
Store rule-providers on server-routing, emit official rule-providers
block into serving config, and expose PUT hot-update via core
/providers/rules/{name}. Panel UI on Routing → Server egress.
2026-09-29 16:26:38 +00:00
kazeyukiro 95961c6cc2 fix(warp): make WARP domains actually work in server Mihomo
- ProxyMap always sets Cloudflare peer public-key, allowed-ips, remote-dns-resolve
- Default peer key when registration omits peers
- Surface ProxyMap errors at generate; clearer apply prompt for server egress
- Domain rule builder hardened; unit test for warp domain injection
2026-09-29 16:12:24 +00:00
kazeyukiro 1ddb808270 fix(i18n): declare WARP/routing keys; restore 3x-ui in acknowledgments
Unit test required en.ts keys used by Routing/Settings. Credits keep
3x-ui / s-ui as before.
2026-09-29 15:57:51 +00:00
kazeyukiro fec9733baa docs: remove third-party panel name references from copy
Drop  and external m-ui product names from docs, UI strings,
and code comments. Keep functional descriptions of server egress and WARP.
docs: remove third-party panel name references from copy
Drop  and external m-ui product names from docs, UI strings,
and code comments. Keep functional descriptions of server egress and WARP.
docs: remove third-party panel name references from copy
Drop  and external m-ui product names from docs, UI strings,
and code comments. Keep functional descriptions of server egress and WARP.
docs: remove third-party panel name references from copy
Drop  and external m-ui product names from docs, UI strings,
and code comments. Keep functional descriptions of server egress and WARP.
docs: remove third-party panel name references from copy
Drop  and external m-ui product names from docs, UI strings,
and code comments. Keep functional descriptions of server egress and WARP.
docs: remove third-party panel name references from copy
Drop  and external m-ui product names from docs, UI strings,
and code comments. Keep functional descriptions of server egress and WARP.
docs: remove third-party panel name references from copy
Drop  and external m-ui product names from docs, UI strings,
and code comments. Keep functional descriptions of server egress and WARP.
docs: remove third-party panel name references from copy
Drop  and external m-ui product names from docs, UI strings,
and code comments. Keep functional descriptions of server egress and WARP.
docs: remove third-party panel name references from copy
Drop  and external m-ui product names from docs, UI strings,
and code comments. Keep functional descriptions of server egress and WARP.
docs: remove third-party panel name references from copy
Drop  and external m-ui product names from docs, UI strings,
and code comments. Keep functional descriptions of server egress and WARP.
docs: remove third-party panel name references from copy
Drop  and external m-ui product names from docs, UI strings,
and code comments. Keep functional descriptions of server egress and WARP.
docs: remove third-party panel name references from copy
Drop  and external m-ui product names from docs, UI strings,
and code comments. Keep functional descriptions of server egress and WARP.
2026-09-29 15:51:57 +00:00
kazeyukiro 0beaf396d1 docs(api): align OpenAPI with server-routing and WARP account
Replace obsolete inject-warp; document GET/PUT server-routing,
GET/POST/DELETE system/warp, and templates/warp register nosave.
docs(api): align OpenAPI with server-routing and WARP account
Replace obsolete inject-warp; document GET/PUT server-routing,
GET/POST/DELETE system/warp, and templates/warp register nosave.
docs(api): align OpenAPI with server-routing and WARP account
Replace obsolete inject-warp; document GET/PUT server-routing,
GET/POST/DELETE system/warp, and templates/warp register nosave.
docs(api): align OpenAPI with server-routing and WARP account
Replace obsolete inject-warp; document GET/PUT server-routing,
GET/POST/DELETE system/warp, and templates/warp register nosave.
docs(api): align OpenAPI with server-routing and WARP account
Replace obsolete inject-warp; document GET/PUT server-routing,
GET/POST/DELETE system/warp, and templates/warp register nosave.
docs(api): align OpenAPI with server-routing and WARP account
Replace obsolete inject-warp; document GET/PUT server-routing,
GET/POST/DELETE system/warp, and templates/warp register nosave.
docs(api): align OpenAPI with server-routing and WARP account
Replace obsolete inject-warp; document GET/PUT server-routing,
GET/POST/DELETE system/warp, and templates/warp register nosave.
docs(api): align OpenAPI with server-routing and WARP account
Replace obsolete inject-warp; document GET/PUT server-routing,
GET/POST/DELETE system/warp, and templates/warp register nosave.
docs(api): align OpenAPI with server-routing and WARP account
Replace obsolete inject-warp; document GET/PUT server-routing,
GET/POST/DELETE system/warp, and templates/warp register nosave.
docs(api): align OpenAPI with server-routing and WARP account
Replace obsolete inject-warp; document GET/PUT server-routing,
GET/POST/DELETE system/warp, and templates/warp register nosave.
docs(api): align OpenAPI with server-routing and WARP account
Replace obsolete inject-warp; document GET/PUT server-routing,
GET/POST/DELETE system/warp, and templates/warp register nosave.
docs(api): align OpenAPI with server-routing and WARP account
Replace obsolete inject-warp; document GET/PUT server-routing,
GET/POST/DELETE system/warp, and templates/warp register nosave.
2026-09-29 15:49:13 +00:00
kazeyukiro 81df3a9e29 feat(warp): persist WARP account, server inject, no server templates
Persist Cloudflare WARP account, inject outbound WARP into serving
Mihomo, route warpDomains on server egress. Remove client community
templates from the server tab; domains editor on Routing → Server egress.
feat(warp): persist WARP account, server inject, no server templates
Persist Cloudflare WARP account, inject outbound WARP into serving
Mihomo, route warpDomains on server egress. Remove client community
templates from the server tab; domains editor on Routing → Server egress.
feat(warp): persist WARP account, server inject, no server templates
Persist Cloudflare WARP account, inject outbound WARP into serving
Mihomo, route warpDomains on server egress. Remove client community
templates from the server tab; domains editor on Routing → Server egress.
feat(warp): persist WARP account, server inject, no server templates
Persist Cloudflare WARP account, inject outbound WARP into serving
Mihomo, route warpDomains on server egress. Remove client community
templates from the server tab; domains editor on Routing → Server egress.
feat(warp): persist WARP account, server inject, no server templates
Persist Cloudflare WARP account, inject outbound WARP into serving
Mihomo, route warpDomains on server egress. Remove client community
templates from the server tab; domains editor on Routing → Server egress.
feat(warp): persist WARP account, server inject, no server templates
Persist Cloudflare WARP account, inject outbound WARP into serving
Mihomo, route warpDomains on server egress. Remove client community
templates from the server tab; domains editor on Routing → Server egress.
feat(warp): persist WARP account, server inject, no server templates
Persist Cloudflare WARP account, inject outbound WARP into serving
Mihomo, route warpDomains on server egress. Remove client community
templates from the server tab; domains editor on Routing → Server egress.
feat(warp): persist WARP account, server inject, no server templates
Persist Cloudflare WARP account, inject outbound WARP into serving
Mihomo, route warpDomains on server egress. Remove client community
templates from the server tab; domains editor on Routing → Server egress.
feat(warp): persist WARP account, server inject, no server templates
Persist Cloudflare WARP account, inject outbound WARP into serving
Mihomo, route warpDomains on server egress. Remove client community
templates from the server tab; domains editor on Routing → Server egress.
feat(warp): persist WARP account, server inject, no server templates
Persist Cloudflare WARP account, inject outbound WARP into serving
Mihomo, route warpDomains on server egress. Remove client community
templates from the server tab; domains editor on Routing → Server egress.
feat(warp): persist WARP account, server inject, no server templates
Persist Cloudflare WARP account, inject outbound WARP into serving
Mihomo, route warpDomains on server egress. Remove client community
templates from the server tab; domains editor on Routing → Server egress.
feat(warp): persist WARP account, server inject, no server templates
Persist Cloudflare WARP account, inject outbound WARP into serving
Mihomo, route warpDomains on server egress. Remove client community
templates from the server tab; domains editor on Routing → Server egress.
2026-09-29 15:46:40 +00:00
kazeyukiro 727f5c0d6b fix(ui): server-egress templates separate from client packs
Server tab no longer offers community client rule packs. Presets are
MATCH/DIRECT, private DIRECT, and optional AI→WARP (needs WARP proxy).
fix(ui): server-egress templates separate from client packs
Server tab no longer offers community client rule packs. Presets are
MATCH/DIRECT, private DIRECT, and optional AI→WARP (needs WARP proxy).
fix(ui): server-egress templates separate from client packs
Server tab no longer offers community client rule packs. Presets are
MATCH/DIRECT, private DIRECT, and optional AI→WARP (needs WARP proxy).
fix(ui): server-egress templates separate from client packs
Server tab no longer offers community client rule packs. Presets are
MATCH/DIRECT, private DIRECT, and optional AI→WARP (needs WARP proxy).
fix(ui): server-egress templates separate from client packs
Server tab no longer offers community client rule packs. Presets are
MATCH/DIRECT, private DIRECT, and optional AI→WARP (needs WARP proxy).
fix(ui): server-egress templates separate from client packs
Server tab no longer offers community client rule packs. Presets are
MATCH/DIRECT, private DIRECT, and optional AI→WARP (needs WARP proxy).
fix(ui): server-egress templates separate from client packs
Server tab no longer offers community client rule packs. Presets are
MATCH/DIRECT, private DIRECT, and optional AI→WARP (needs WARP proxy).
fix(ui): server-egress templates separate from client packs
Server tab no longer offers community client rule packs. Presets are
MATCH/DIRECT, private DIRECT, and optional AI→WARP (needs WARP proxy).
fix(ui): server-egress templates separate from client packs
Server tab no longer offers community client rule packs. Presets are
MATCH/DIRECT, private DIRECT, and optional AI→WARP (needs WARP proxy).
fix(ui): server-egress templates separate from client packs
Server tab no longer offers community client rule packs. Presets are
MATCH/DIRECT, private DIRECT, and optional AI→WARP (needs WARP proxy).
fix(ui): server-egress templates separate from client packs
Server tab no longer offers community client rule packs. Presets are
MATCH/DIRECT, private DIRECT, and optional AI→WARP (needs WARP proxy).
fix(ui): server-egress templates separate from client packs
Server tab no longer offers community client rule packs. Presets are
MATCH/DIRECT, private DIRECT, and optional AI→WARP (needs WARP proxy).
2026-09-29 15:34:17 +00:00
kazeyukiro aa548bd5c3 fix(ui): compact desktop sidebar icons when collapsed
Global menu height:auto + padding-block for long i18n labels was also
applied in collapsed mode, stretching icon-only rows. Override collapsed
items to fixed 40px height and pass inlineCollapsed to Menu.
fix(ui): compact desktop sidebar icons when collapsed
Global menu height:auto + padding-block for long i18n labels was also
applied in collapsed mode, stretching icon-only rows. Override collapsed
items to fixed 40px height and pass inlineCollapsed to Menu.
fix(ui): compact desktop sidebar icons when collapsed
Global menu height:auto + padding-block for long i18n labels was also
applied in collapsed mode, stretching icon-only rows. Override collapsed
items to fixed 40px height and pass inlineCollapsed to Menu.
fix(ui): compact desktop sidebar icons when collapsed
Global menu height:auto + padding-block for long i18n labels was also
applied in collapsed mode, stretching icon-only rows. Override collapsed
items to fixed 40px height and pass inlineCollapsed to Menu.
fix(ui): compact desktop sidebar icons when collapsed
Global menu height:auto + padding-block for long i18n labels was also
applied in collapsed mode, stretching icon-only rows. Override collapsed
items to fixed 40px height and pass inlineCollapsed to Menu.
fix(ui): compact desktop sidebar icons when collapsed
Global menu height:auto + padding-block for long i18n labels was also
applied in collapsed mode, stretching icon-only rows. Override collapsed
items to fixed 40px height and pass inlineCollapsed to Menu.
fix(ui): compact desktop sidebar icons when collapsed
Global menu height:auto + padding-block for long i18n labels was also
applied in collapsed mode, stretching icon-only rows. Override collapsed
items to fixed 40px height and pass inlineCollapsed to Menu.
fix(ui): compact desktop sidebar icons when collapsed
Global menu height:auto + padding-block for long i18n labels was also
applied in collapsed mode, stretching icon-only rows. Override collapsed
items to fixed 40px height and pass inlineCollapsed to Menu.
fix(ui): compact desktop sidebar icons when collapsed
Global menu height:auto + padding-block for long i18n labels was also
applied in collapsed mode, stretching icon-only rows. Override collapsed
items to fixed 40px height and pass inlineCollapsed to Menu.
fix(ui): compact desktop sidebar icons when collapsed
Global menu height:auto + padding-block for long i18n labels was also
applied in collapsed mode, stretching icon-only rows. Override collapsed
items to fixed 40px height and pass inlineCollapsed to Menu.
fix(ui): compact desktop sidebar icons when collapsed
Global menu height:auto + padding-block for long i18n labels was also
applied in collapsed mode, stretching icon-only rows. Override collapsed
items to fixed 40px height and pass inlineCollapsed to Menu.
fix(ui): compact desktop sidebar icons when collapsed
Global menu height:auto + padding-block for long i18n labels was also
applied in collapsed mode, stretching icon-only rows. Override collapsed
items to fixed 40px height and pass inlineCollapsed to Menu.
2026-09-29 15:25:36 +00:00
kazeyukiro eefc5cc889 docs: zh-CN routing link for server egress 2026-09-29 15:17:48 +00:00
kazeyukiro ca2d4a3192 docs: sync routing (server egress) and WARP limits
Document Client vs Server tabs, server-routing API, and that WARP
register is manual (no auto AI/streaming unlock pipeline).
docs: sync routing (server egress) and WARP limits
Document Client vs Server tabs, server-routing API, and that WARP
register is manual (no auto AI/streaming unlock pipeline).
docs: sync routing (server egress) and WARP limits
Document Client vs Server tabs, server-routing API, and that WARP
register is manual (no auto AI/streaming unlock pipeline).
docs: sync routing (server egress) and WARP limits
Document Client vs Server tabs, server-routing API, and that WARP
register is manual (no auto AI/streaming unlock pipeline).
docs: sync routing (server egress) and WARP limits
Document Client vs Server tabs, server-routing API, and that WARP
register is manual (no auto AI/streaming unlock pipeline).
docs: sync routing (server egress) and WARP limits
Document Client vs Server tabs, server-routing API, and that WARP
register is manual (no auto AI/streaming unlock pipeline).
docs: sync routing (server egress) and WARP limits
Document Client vs Server tabs, server-routing API, and that WARP
register is manual (no auto AI/streaming unlock pipeline).
docs: sync routing (server egress) and WARP limits
Document Client vs Server tabs, server-routing API, and that WARP
register is manual (no auto AI/streaming unlock pipeline).
docs: sync routing (server egress) and WARP limits
Document Client vs Server tabs, server-routing API, and that WARP
register is manual (no auto AI/streaming unlock pipeline).
docs: sync routing (server egress) and WARP limits
Document Client vs Server tabs, server-routing API, and that WARP
register is manual (no auto AI/streaming unlock pipeline).
docs: sync routing (server egress) and WARP limits
Document Client vs Server tabs, server-routing API, and that WARP
register is manual (no auto AI/streaming unlock pipeline).
docs: sync routing (server egress) and WARP limits
Document Client vs Server tabs, server-routing API, and that WARP
register is manual (no auto AI/streaming unlock pipeline).
2026-09-29 15:17:40 +00:00
kazeyukiro 4d51a5704f feat(routing): server-side Mihomo egress for panel process
Store server-routing (rules/proxies/groups) separately from client
visual-config. Generator applies it to the serving core instead of a
hard-coded MATCH,DIRECT. Routing UI gains Client / Server tabs.
feat(routing): server-side Mihomo egress for panel process
Store server-routing (rules/proxies/groups) separately from client
visual-config. Generator applies it to the serving core instead of a
hard-coded MATCH,DIRECT. Routing UI gains Client / Server tabs.
feat(routing): server-side Mihomo egress for panel process
Store server-routing (rules/proxies/groups) separately from client
visual-config. Generator applies it to the serving core instead of a
hard-coded MATCH,DIRECT. Routing UI gains Client / Server tabs.
feat(routing): server-side Mihomo egress for panel process
Store server-routing (rules/proxies/groups) separately from client
visual-config. Generator applies it to the serving core instead of a
hard-coded MATCH,DIRECT. Routing UI gains Client / Server tabs.
feat(routing): server-side Mihomo egress for panel process
Store server-routing (rules/proxies/groups) separately from client
visual-config. Generator applies it to the serving core instead of a
hard-coded MATCH,DIRECT. Routing UI gains Client / Server tabs.
feat(routing): server-side Mihomo egress for panel process
Store server-routing (rules/proxies/groups) separately from client
visual-config. Generator applies it to the serving core instead of a
hard-coded MATCH,DIRECT. Routing UI gains Client / Server tabs.
feat(routing): server-side Mihomo egress for panel process
Store server-routing (rules/proxies/groups) separately from client
visual-config. Generator applies it to the serving core instead of a
hard-coded MATCH,DIRECT. Routing UI gains Client / Server tabs.
feat(routing): server-side Mihomo egress for panel process
Store server-routing (rules/proxies/groups) separately from client
visual-config. Generator applies it to the serving core instead of a
hard-coded MATCH,DIRECT. Routing UI gains Client / Server tabs.
feat(routing): server-side Mihomo egress for panel process
Store server-routing (rules/proxies/groups) separately from client
visual-config. Generator applies it to the serving core instead of a
hard-coded MATCH,DIRECT. Routing UI gains Client / Server tabs.
feat(routing): server-side Mihomo egress for panel process
Store server-routing (rules/proxies/groups) separately from client
visual-config. Generator applies it to the serving core instead of a
hard-coded MATCH,DIRECT. Routing UI gains Client / Server tabs.
feat(routing): server-side Mihomo egress for panel process
Store server-routing (rules/proxies/groups) separately from client
visual-config. Generator applies it to the serving core instead of a
hard-coded MATCH,DIRECT. Routing UI gains Client / Server tabs.
feat(routing): server-side Mihomo egress for panel process
Store server-routing (rules/proxies/groups) separately from client
visual-config. Generator applies it to the serving core instead of a
hard-coded MATCH,DIRECT. Routing UI gains Client / Server tabs.
2026-09-29 15:14:39 +00:00
kazeyukiro addf8aee32 fix(sub): align sing-box export with migration 1.11–1.15
Drop deprecated DNS rule ip_is_private without match_response (1.14/1.16).
Keep typed DNS servers, TUN without stack, route sniff/hijack-dns actions,
and route-level private direct per official migration/deprecated docs.
fix(sub): align sing-box export with migration 1.11–1.15
Drop deprecated DNS rule ip_is_private without match_response (1.14/1.16).
Keep typed DNS servers, TUN without stack, route sniff/hijack-dns actions,
and route-level private direct per official migration/deprecated docs.
fix(sub): align sing-box export with migration 1.11–1.15
Drop deprecated DNS rule ip_is_private without match_response (1.14/1.16).
Keep typed DNS servers, TUN without stack, route sniff/hijack-dns actions,
and route-level private direct per official migration/deprecated docs.
fix(sub): align sing-box export with migration 1.11–1.15
Drop deprecated DNS rule ip_is_private without match_response (1.14/1.16).
Keep typed DNS servers, TUN without stack, route sniff/hijack-dns actions,
and route-level private direct per official migration/deprecated docs.
fix(sub): align sing-box export with migration 1.11–1.15
Drop deprecated DNS rule ip_is_private without match_response (1.14/1.16).
Keep typed DNS servers, TUN without stack, route sniff/hijack-dns actions,
and route-level private direct per official migration/deprecated docs.
fix(sub): align sing-box export with migration 1.11–1.15
Drop deprecated DNS rule ip_is_private without match_response (1.14/1.16).
Keep typed DNS servers, TUN without stack, route sniff/hijack-dns actions,
and route-level private direct per official migration/deprecated docs.
fix(sub): align sing-box export with migration 1.11–1.15
Drop deprecated DNS rule ip_is_private without match_response (1.14/1.16).
Keep typed DNS servers, TUN without stack, route sniff/hijack-dns actions,
and route-level private direct per official migration/deprecated docs.
fix(sub): align sing-box export with migration 1.11–1.15
Drop deprecated DNS rule ip_is_private without match_response (1.14/1.16).
Keep typed DNS servers, TUN without stack, route sniff/hijack-dns actions,
and route-level private direct per official migration/deprecated docs.
fix(sub): align sing-box export with migration 1.11–1.15
Drop deprecated DNS rule ip_is_private without match_response (1.14/1.16).
Keep typed DNS servers, TUN without stack, route sniff/hijack-dns actions,
and route-level private direct per official migration/deprecated docs.
fix(sub): align sing-box export with migration 1.11–1.15
Drop deprecated DNS rule ip_is_private without match_response (1.14/1.16).
Keep typed DNS servers, TUN without stack, route sniff/hijack-dns actions,
and route-level private direct per official migration/deprecated docs.
fix(sub): align sing-box export with migration 1.11–1.15
Drop deprecated DNS rule ip_is_private without match_response (1.14/1.16).
Keep typed DNS servers, TUN without stack, route sniff/hijack-dns actions,
and route-level private direct per official migration/deprecated docs.
fix(sub): align sing-box export with migration 1.11–1.15
Drop deprecated DNS rule ip_is_private without match_response (1.14/1.16).
Keep typed DNS servers, TUN without stack, route sniff/hijack-dns actions,
and route-level private direct per official migration/deprecated docs.
2026-09-29 14:28:01 +00:00
kazeyukiro 4e68f45c5d fix(sub): reduce SFA lag — local domain resolver + private direct
Avoid DNS↔proxy loops by resolving outbound hostnames via local DNS,
hijack DNS with port 53, route private IPs direct, drop auto_redirect,
and use DoH remote DNS after the tunnel is up.
fix(sub): reduce SFA lag — local domain resolver + private direct
Avoid DNS↔proxy loops by resolving outbound hostnames via local DNS,
hijack DNS with port 53, route private IPs direct, drop auto_redirect,
and use DoH remote DNS after the tunnel is up.
fix(sub): reduce SFA lag — local domain resolver + private direct
Avoid DNS↔proxy loops by resolving outbound hostnames via local DNS,
hijack DNS with port 53, route private IPs direct, drop auto_redirect,
and use DoH remote DNS after the tunnel is up.
fix(sub): reduce SFA lag — local domain resolver + private direct
Avoid DNS↔proxy loops by resolving outbound hostnames via local DNS,
hijack DNS with port 53, route private IPs direct, drop auto_redirect,
and use DoH remote DNS after the tunnel is up.
fix(sub): reduce SFA lag — local domain resolver + private direct
Avoid DNS↔proxy loops by resolving outbound hostnames via local DNS,
hijack DNS with port 53, route private IPs direct, drop auto_redirect,
and use DoH remote DNS after the tunnel is up.
fix(sub): reduce SFA lag — local domain resolver + private direct
Avoid DNS↔proxy loops by resolving outbound hostnames via local DNS,
hijack DNS with port 53, route private IPs direct, drop auto_redirect,
and use DoH remote DNS after the tunnel is up.
fix(sub): reduce SFA lag — local domain resolver + private direct
Avoid DNS↔proxy loops by resolving outbound hostnames via local DNS,
hijack DNS with port 53, route private IPs direct, drop auto_redirect,
and use DoH remote DNS after the tunnel is up.
fix(sub): reduce SFA lag — local domain resolver + private direct
Avoid DNS↔proxy loops by resolving outbound hostnames via local DNS,
hijack DNS with port 53, route private IPs direct, drop auto_redirect,
and use DoH remote DNS after the tunnel is up.
fix(sub): reduce SFA lag — local domain resolver + private direct
Avoid DNS↔proxy loops by resolving outbound hostnames via local DNS,
hijack DNS with port 53, route private IPs direct, drop auto_redirect,
and use DoH remote DNS after the tunnel is up.
fix(sub): reduce SFA lag — local domain resolver + private direct
Avoid DNS↔proxy loops by resolving outbound hostnames via local DNS,
hijack DNS with port 53, route private IPs direct, drop auto_redirect,
and use DoH remote DNS after the tunnel is up.
fix(sub): reduce SFA lag — local domain resolver + private direct
Avoid DNS↔proxy loops by resolving outbound hostnames via local DNS,
hijack DNS with port 53, route private IPs direct, drop auto_redirect,
and use DoH remote DNS after the tunnel is up.
fix(sub): reduce SFA lag — local domain resolver + private direct
Avoid DNS↔proxy loops by resolving outbound hostnames via local DNS,
hijack DNS with port 53, route private IPs direct, drop auto_redirect,
and use DoH remote DNS after the tunnel is up.
2026-09-28 16:36:57 +00:00
kazeyukiro f7c6f8a0ae fix(sub): align sing-box TUN with v1.14 template (no stack)
Match LongLights sing-box_v1.14 tun-in: interface_name, IPv4-only
address, route_exclude_address, dns_mode=hijack, auto_redirect.
Omit stack so the client chooses the default stack.
fix(sub): align sing-box TUN with v1.14 template (no stack)
Match LongLights sing-box_v1.14 tun-in: interface_name, IPv4-only
address, route_exclude_address, dns_mode=hijack, auto_redirect.
Omit stack so the client chooses the default stack.
fix(sub): align sing-box TUN with v1.14 template (no stack)
Match LongLights sing-box_v1.14 tun-in: interface_name, IPv4-only
address, route_exclude_address, dns_mode=hijack, auto_redirect.
Omit stack so the client chooses the default stack.
fix(sub): align sing-box TUN with v1.14 template (no stack)
Match LongLights sing-box_v1.14 tun-in: interface_name, IPv4-only
address, route_exclude_address, dns_mode=hijack, auto_redirect.
Omit stack so the client chooses the default stack.
fix(sub): align sing-box TUN with v1.14 template (no stack)
Match LongLights sing-box_v1.14 tun-in: interface_name, IPv4-only
address, route_exclude_address, dns_mode=hijack, auto_redirect.
Omit stack so the client chooses the default stack.
fix(sub): align sing-box TUN with v1.14 template (no stack)
Match LongLights sing-box_v1.14 tun-in: interface_name, IPv4-only
address, route_exclude_address, dns_mode=hijack, auto_redirect.
Omit stack so the client chooses the default stack.
fix(sub): align sing-box TUN with v1.14 template (no stack)
Match LongLights sing-box_v1.14 tun-in: interface_name, IPv4-only
address, route_exclude_address, dns_mode=hijack, auto_redirect.
Omit stack so the client chooses the default stack.
fix(sub): align sing-box TUN with v1.14 template (no stack)
Match LongLights sing-box_v1.14 tun-in: interface_name, IPv4-only
address, route_exclude_address, dns_mode=hijack, auto_redirect.
Omit stack so the client chooses the default stack.
fix(sub): align sing-box TUN with v1.14 template (no stack)
Match LongLights sing-box_v1.14 tun-in: interface_name, IPv4-only
address, route_exclude_address, dns_mode=hijack, auto_redirect.
Omit stack so the client chooses the default stack.
fix(sub): align sing-box TUN with v1.14 template (no stack)
Match LongLights sing-box_v1.14 tun-in: interface_name, IPv4-only
address, route_exclude_address, dns_mode=hijack, auto_redirect.
Omit stack so the client chooses the default stack.
fix(sub): align sing-box TUN with v1.14 template (no stack)
Match LongLights sing-box_v1.14 tun-in: interface_name, IPv4-only
address, route_exclude_address, dns_mode=hijack, auto_redirect.
Omit stack so the client chooses the default stack.
fix(sub): align sing-box TUN with v1.14 template (no stack)
Match LongLights sing-box_v1.14 tun-in: interface_name, IPv4-only
address, route_exclude_address, dns_mode=hijack, auto_redirect.
Omit stack so the client chooses the default stack.
2026-09-28 16:00:37 +00:00
風雪 d1e49a6ad3 Update singbox.go
Update singbox.go
Update singbox.go
Update singbox.go
Update singbox.go
Update singbox.go
Update singbox.go
Update singbox.go
Update singbox.go
Update singbox.go
Update singbox.go
Update singbox.go
2026-09-28 23:44:33 +08:00
kazeyukiro 38720e14c1 fix(sub): sing-box DNS + mobile TUN MTU to stop broken web pages
Proxy-only TUN without DNS left CSS/JS CDNs failing under SFA.
Add dns.servers (UDP 8.8.8.8 via proxy), MTU 1500, mixed stack,
strict_route off for Android compatibility.
fix(sub): sing-box DNS + mobile TUN MTU to stop broken web pages
Proxy-only TUN without DNS left CSS/JS CDNs failing under SFA.
Add dns.servers (UDP 8.8.8.8 via proxy), MTU 1500, mixed stack,
strict_route off for Android compatibility.
fix(sub): sing-box DNS + mobile TUN MTU to stop broken web pages
Proxy-only TUN without DNS left CSS/JS CDNs failing under SFA.
Add dns.servers (UDP 8.8.8.8 via proxy), MTU 1500, mixed stack,
strict_route off for Android compatibility.
fix(sub): sing-box DNS + mobile TUN MTU to stop broken web pages
Proxy-only TUN without DNS left CSS/JS CDNs failing under SFA.
Add dns.servers (UDP 8.8.8.8 via proxy), MTU 1500, mixed stack,
strict_route off for Android compatibility.
fix(sub): sing-box DNS + mobile TUN MTU to stop broken web pages
Proxy-only TUN without DNS left CSS/JS CDNs failing under SFA.
Add dns.servers (UDP 8.8.8.8 via proxy), MTU 1500, mixed stack,
strict_route off for Android compatibility.
fix(sub): sing-box DNS + mobile TUN MTU to stop broken web pages
Proxy-only TUN without DNS left CSS/JS CDNs failing under SFA.
Add dns.servers (UDP 8.8.8.8 via proxy), MTU 1500, mixed stack,
strict_route off for Android compatibility.
fix(sub): sing-box DNS + mobile TUN MTU to stop broken web pages
Proxy-only TUN without DNS left CSS/JS CDNs failing under SFA.
Add dns.servers (UDP 8.8.8.8 via proxy), MTU 1500, mixed stack,
strict_route off for Android compatibility.
fix(sub): sing-box DNS + mobile TUN MTU to stop broken web pages
Proxy-only TUN without DNS left CSS/JS CDNs failing under SFA.
Add dns.servers (UDP 8.8.8.8 via proxy), MTU 1500, mixed stack,
strict_route off for Android compatibility.
fix(sub): sing-box DNS + mobile TUN MTU to stop broken web pages
Proxy-only TUN without DNS left CSS/JS CDNs failing under SFA.
Add dns.servers (UDP 8.8.8.8 via proxy), MTU 1500, mixed stack,
strict_route off for Android compatibility.
fix(sub): sing-box DNS + mobile TUN MTU to stop broken web pages
Proxy-only TUN without DNS left CSS/JS CDNs failing under SFA.
Add dns.servers (UDP 8.8.8.8 via proxy), MTU 1500, mixed stack,
strict_route off for Android compatibility.
fix(sub): sing-box DNS + mobile TUN MTU to stop broken web pages
Proxy-only TUN without DNS left CSS/JS CDNs failing under SFA.
Add dns.servers (UDP 8.8.8.8 via proxy), MTU 1500, mixed stack,
strict_route off for Android compatibility.
fix(sub): sing-box DNS + mobile TUN MTU to stop broken web pages
Proxy-only TUN without DNS left CSS/JS CDNs failing under SFA.
Add dns.servers (UDP 8.8.8.8 via proxy), MTU 1500, mixed stack,
strict_route off for Android compatibility.
2026-09-28 15:34:37 +00:00
kazeyukiro 3969d5ed68 fix(subpage): harden HTML profile page for SFA/SFM WebView
Avoid CSS that breaks embedded WebViews: solid color fallbacks,
no side-by-side import grid, flex instead of place-items, overflow
and safe-area padding for Profile-Web-Page-Url in-app browsers.
fix(subpage): harden HTML profile page for SFA/SFM WebView
Avoid CSS that breaks embedded WebViews: solid color fallbacks,
no side-by-side import grid, flex instead of place-items, overflow
and safe-area padding for Profile-Web-Page-Url in-app browsers.
fix(subpage): harden HTML profile page for SFA/SFM WebView
Avoid CSS that breaks embedded WebViews: solid color fallbacks,
no side-by-side import grid, flex instead of place-items, overflow
and safe-area padding for Profile-Web-Page-Url in-app browsers.
fix(subpage): harden HTML profile page for SFA/SFM WebView
Avoid CSS that breaks embedded WebViews: solid color fallbacks,
no side-by-side import grid, flex instead of place-items, overflow
and safe-area padding for Profile-Web-Page-Url in-app browsers.
fix(subpage): harden HTML profile page for SFA/SFM WebView
Avoid CSS that breaks embedded WebViews: solid color fallbacks,
no side-by-side import grid, flex instead of place-items, overflow
and safe-area padding for Profile-Web-Page-Url in-app browsers.
fix(subpage): harden HTML profile page for SFA/SFM WebView
Avoid CSS that breaks embedded WebViews: solid color fallbacks,
no side-by-side import grid, flex instead of place-items, overflow
and safe-area padding for Profile-Web-Page-Url in-app browsers.
fix(subpage): harden HTML profile page for SFA/SFM WebView
Avoid CSS that breaks embedded WebViews: solid color fallbacks,
no side-by-side import grid, flex instead of place-items, overflow
and safe-area padding for Profile-Web-Page-Url in-app browsers.
fix(subpage): harden HTML profile page for SFA/SFM WebView
Avoid CSS that breaks embedded WebViews: solid color fallbacks,
no side-by-side import grid, flex instead of place-items, overflow
and safe-area padding for Profile-Web-Page-Url in-app browsers.
fix(subpage): harden HTML profile page for SFA/SFM WebView
Avoid CSS that breaks embedded WebViews: solid color fallbacks,
no side-by-side import grid, flex instead of place-items, overflow
and safe-area padding for Profile-Web-Page-Url in-app browsers.
fix(subpage): harden HTML profile page for SFA/SFM WebView
Avoid CSS that breaks embedded WebViews: solid color fallbacks,
no side-by-side import grid, flex instead of place-items, overflow
and safe-area padding for Profile-Web-Page-Url in-app browsers.
fix(subpage): harden HTML profile page for SFA/SFM WebView
Avoid CSS that breaks embedded WebViews: solid color fallbacks,
no side-by-side import grid, flex instead of place-items, overflow
and safe-area padding for Profile-Web-Page-Url in-app browsers.
fix(subpage): harden HTML profile page for SFA/SFM WebView
Avoid CSS that breaks embedded WebViews: solid color fallbacks,
no side-by-side import grid, flex instead of place-items, overflow
and safe-area padding for Profile-Web-Page-Url in-app browsers.
2026-09-28 15:30:03 +00:00
kazeyukiro 5e6cc8b604 fix(sub): sing-box 1.11+ route actions instead of inbound sniff
Remove legacy inbound sniff (removed in 1.13). Use route rule
actions sniff + hijack-dns per official migration guide.
fix(sub): sing-box 1.11+ route actions instead of inbound sniff
Remove legacy inbound sniff (removed in 1.13). Use route rule
actions sniff + hijack-dns per official migration guide.
fix(sub): sing-box 1.11+ route actions instead of inbound sniff
Remove legacy inbound sniff (removed in 1.13). Use route rule
actions sniff + hijack-dns per official migration guide.
fix(sub): sing-box 1.11+ route actions instead of inbound sniff
Remove legacy inbound sniff (removed in 1.13). Use route rule
actions sniff + hijack-dns per official migration guide.
fix(sub): sing-box 1.11+ route actions instead of inbound sniff
Remove legacy inbound sniff (removed in 1.13). Use route rule
actions sniff + hijack-dns per official migration guide.
fix(sub): sing-box 1.11+ route actions instead of inbound sniff
Remove legacy inbound sniff (removed in 1.13). Use route rule
actions sniff + hijack-dns per official migration guide.
fix(sub): sing-box 1.11+ route actions instead of inbound sniff
Remove legacy inbound sniff (removed in 1.13). Use route rule
actions sniff + hijack-dns per official migration guide.
fix(sub): sing-box 1.11+ route actions instead of inbound sniff
Remove legacy inbound sniff (removed in 1.13). Use route rule
actions sniff + hijack-dns per official migration guide.
fix(sub): sing-box 1.11+ route actions instead of inbound sniff
Remove legacy inbound sniff (removed in 1.13). Use route rule
actions sniff + hijack-dns per official migration guide.
fix(sub): sing-box 1.11+ route actions instead of inbound sniff
Remove legacy inbound sniff (removed in 1.13). Use route rule
actions sniff + hijack-dns per official migration guide.
fix(sub): sing-box 1.11+ route actions instead of inbound sniff
Remove legacy inbound sniff (removed in 1.13). Use route rule
actions sniff + hijack-dns per official migration guide.
fix(sub): sing-box 1.11+ route actions instead of inbound sniff
Remove legacy inbound sniff (removed in 1.13). Use route rule
actions sniff + hijack-dns per official migration guide.
2026-09-28 15:06:53 +00:00
kazeyukiro af01f1f237 fix(share): emit VLESS WS host in URI when ws-headers empty 2026-09-28 14:51:08 +00:00
kazeyukiro f9f5c66fed fix(ci,sub): gofmt + align sing-box TUN with official schema
Use official Tun address example (172.18.0.1/30 + ULA IPv6) and
keep auto_route/strict_route/stack per sing-box docs.
fix(ci,sub): gofmt + align sing-box TUN with official schema
Use official Tun address example (172.18.0.1/30 + ULA IPv6) and
keep auto_route/strict_route/stack per sing-box docs.
fix(ci,sub): gofmt + align sing-box TUN with official schema
Use official Tun address example (172.18.0.1/30 + ULA IPv6) and
keep auto_route/strict_route/stack per sing-box docs.
fix(ci,sub): gofmt + align sing-box TUN with official schema
Use official Tun address example (172.18.0.1/30 + ULA IPv6) and
keep auto_route/strict_route/stack per sing-box docs.
fix(ci,sub): gofmt + align sing-box TUN with official schema
Use official Tun address example (172.18.0.1/30 + ULA IPv6) and
keep auto_route/strict_route/stack per sing-box docs.
fix(ci,sub): gofmt + align sing-box TUN with official schema
Use official Tun address example (172.18.0.1/30 + ULA IPv6) and
keep auto_route/strict_route/stack per sing-box docs.
fix(ci,sub): gofmt + align sing-box TUN with official schema
Use official Tun address example (172.18.0.1/30 + ULA IPv6) and
keep auto_route/strict_route/stack per sing-box docs.
fix(ci,sub): gofmt + align sing-box TUN with official schema
Use official Tun address example (172.18.0.1/30 + ULA IPv6) and
keep auto_route/strict_route/stack per sing-box docs.
fix(ci,sub): gofmt + align sing-box TUN with official schema
Use official Tun address example (172.18.0.1/30 + ULA IPv6) and
keep auto_route/strict_route/stack per sing-box docs.
fix(ci,sub): gofmt + align sing-box TUN with official schema
Use official Tun address example (172.18.0.1/30 + ULA IPv6) and
keep auto_route/strict_route/stack per sing-box docs.
fix(ci,sub): gofmt + align sing-box TUN with official schema
Use official Tun address example (172.18.0.1/30 + ULA IPv6) and
keep auto_route/strict_route/stack per sing-box docs.
fix(ci,sub): gofmt + align sing-box TUN with official schema
Use official Tun address example (172.18.0.1/30 + ULA IPv6) and
keep auto_route/strict_route/stack per sing-box docs.
2026-09-28 14:39:28 +00:00
kazeyukiro c604bfc6d7 fix(sub): repair sing-box comment syntax after TUN inject 2026-09-28 14:31:36 +00:00
kazeyukiro 9fef35e238 feat(sub): inject default TUN inbound into sing-box JSON (#88)
SFI/SFM need a tun inbound to create the system VPN interface.
Subscription docs now include tun-in + route.final=proxy alongside
node outbounds.
feat(sub): inject default TUN inbound into sing-box JSON (#88)
SFI/SFM need a tun inbound to create the system VPN interface.
Subscription docs now include tun-in + route.final=proxy alongside
node outbounds.
feat(sub): inject default TUN inbound into sing-box JSON (#88)
SFI/SFM need a tun inbound to create the system VPN interface.
Subscription docs now include tun-in + route.final=proxy alongside
node outbounds.
feat(sub): inject default TUN inbound into sing-box JSON (#88)
SFI/SFM need a tun inbound to create the system VPN interface.
Subscription docs now include tun-in + route.final=proxy alongside
node outbounds.
feat(sub): inject default TUN inbound into sing-box JSON (#88)
SFI/SFM need a tun inbound to create the system VPN interface.
Subscription docs now include tun-in + route.final=proxy alongside
node outbounds.
feat(sub): inject default TUN inbound into sing-box JSON (#88)
SFI/SFM need a tun inbound to create the system VPN interface.
Subscription docs now include tun-in + route.final=proxy alongside
node outbounds.
feat(sub): inject default TUN inbound into sing-box JSON (#88)
SFI/SFM need a tun inbound to create the system VPN interface.
Subscription docs now include tun-in + route.final=proxy alongside
node outbounds.
feat(sub): inject default TUN inbound into sing-box JSON (#88)
SFI/SFM need a tun inbound to create the system VPN interface.
Subscription docs now include tun-in + route.final=proxy alongside
node outbounds.
feat(sub): inject default TUN inbound into sing-box JSON (#88)
SFI/SFM need a tun inbound to create the system VPN interface.
Subscription docs now include tun-in + route.final=proxy alongside
node outbounds.
feat(sub): inject default TUN inbound into sing-box JSON (#88)
SFI/SFM need a tun inbound to create the system VPN interface.
Subscription docs now include tun-in + route.final=proxy alongside
node outbounds.
feat(sub): inject default TUN inbound into sing-box JSON (#88)
SFI/SFM need a tun inbound to create the system VPN interface.
Subscription docs now include tun-in + route.final=proxy alongside
node outbounds.
feat(sub): inject default TUN inbound into sing-box JSON (#88)
SFI/SFM need a tun inbound to create the system VPN interface.
Subscription docs now include tun-in + route.final=proxy alongside
node outbounds.
2026-09-28 14:31:26 +00:00
kazeyukiro b664e57117 fix(share): CDN VLESS/WS export Host + path defaults
When ws-headers Host is empty, client URI/YAML/subscription now fall
back to SNI or public host. Always emit WS path (default /). Panel
gains an explicit WebSocket Host field for CDN setups.
fix(share): CDN VLESS/WS export Host + path defaults
When ws-headers Host is empty, client URI/YAML/subscription now fall
back to SNI or public host. Always emit WS path (default /). Panel
gains an explicit WebSocket Host field for CDN setups.
fix(share): CDN VLESS/WS export Host + path defaults
When ws-headers Host is empty, client URI/YAML/subscription now fall
back to SNI or public host. Always emit WS path (default /). Panel
gains an explicit WebSocket Host field for CDN setups.
fix(share): CDN VLESS/WS export Host + path defaults
When ws-headers Host is empty, client URI/YAML/subscription now fall
back to SNI or public host. Always emit WS path (default /). Panel
gains an explicit WebSocket Host field for CDN setups.
fix(share): CDN VLESS/WS export Host + path defaults
When ws-headers Host is empty, client URI/YAML/subscription now fall
back to SNI or public host. Always emit WS path (default /). Panel
gains an explicit WebSocket Host field for CDN setups.
fix(share): CDN VLESS/WS export Host + path defaults
When ws-headers Host is empty, client URI/YAML/subscription now fall
back to SNI or public host. Always emit WS path (default /). Panel
gains an explicit WebSocket Host field for CDN setups.
fix(share): CDN VLESS/WS export Host + path defaults
When ws-headers Host is empty, client URI/YAML/subscription now fall
back to SNI or public host. Always emit WS path (default /). Panel
gains an explicit WebSocket Host field for CDN setups.
fix(share): CDN VLESS/WS export Host + path defaults
When ws-headers Host is empty, client URI/YAML/subscription now fall
back to SNI or public host. Always emit WS path (default /). Panel
gains an explicit WebSocket Host field for CDN setups.
fix(share): CDN VLESS/WS export Host + path defaults
When ws-headers Host is empty, client URI/YAML/subscription now fall
back to SNI or public host. Always emit WS path (default /). Panel
gains an explicit WebSocket Host field for CDN setups.
fix(share): CDN VLESS/WS export Host + path defaults
When ws-headers Host is empty, client URI/YAML/subscription now fall
back to SNI or public host. Always emit WS path (default /). Panel
gains an explicit WebSocket Host field for CDN setups.
fix(share): CDN VLESS/WS export Host + path defaults
When ws-headers Host is empty, client URI/YAML/subscription now fall
back to SNI or public host. Always emit WS path (default /). Panel
gains an explicit WebSocket Host field for CDN setups.
fix(share): CDN VLESS/WS export Host + path defaults
When ws-headers Host is empty, client URI/YAML/subscription now fall
back to SNI or public host. Always emit WS path (default /). Panel
gains an explicit WebSocket Host field for CDN setups.
2026-09-28 14:27:44 +00:00
風雪and風雪 35f4c521c3 fix(vless): stop exporting flow on transports that cannot carry it (#87)
* fix(vless): stop exporting flow on transports that cannot carry it
The form promised that flow is cleared for ws/grpc/xhttp and the tooltip still
says so, but exported links kept carrying flow=xtls-rprx-vision. Two separate
reasons, both fixed.
The clear came too late to matter. formValuesToConfig wrote `flow` into the
config inside the protocol switch, and only afterwards did the "Vision is
TCP-only" rule run — where it mutated the form state, not the config it had
already been copied into. The transport is now resolved before anything writes,
and the drop also removes a flow inherited from the previous config, so changing
a listener's transport actually takes effect.
The URI generators never asked. Every one of them emitted whatever flow it found:
- node: vlessURIs decided flow before resolving the transport, so the type it
had just computed was irrelevant to the decision. It now resolves the
transport first. ClientURIsWithCredentials also merged the listener-level
flow onto every user on protocol alone, which reintroduced the value the
form had dropped.
- protocol: the share URI and the client YAML both wrote spec.Flow unguarded,
even though TransportSpec knows the network. Same for the server-side
compile, which pushed the listener default onto users — Mihomo rejects that
combination outright, so this one takes the listener down.
- mui: BuildVLESSShare set flow unconditionally, and bridge.go injected the
listener default into users before it was known to be applicable.
- converter: copyTransport resolved `network` but left a stale flow behind.
The rule is now one predicate per representation: TransportSpec.CarriesFlow,
VLESSHandlerSpec.CarriesFlow, and protocol.TransportCarriesFlow for raw configs.
TCP/raw keeps Vision; ws, grpc, xhttp, mkcp and mekya never emit it.
Tests pin each path in both directions — a stale flow disappears on ws/grpc/
xhttp, and a genuine TCP listener keeps its flow. The frontend one runs the real
form builder and fails, in three of five cases, against the previous ordering.
* fix(uri): carry vmess ws Host and align hysteria2 TLS flags across user shapes
Auditing the remaining exporters after the flow fix turned up two more places
where the link disagreed with the listener.
vmessURIs built its JSON by hand instead of going through transportParams, so a
ws listener that pins a Host header exported only the path: the client had
nothing to send as Host and the listener was unreachable through the link, even
though the client YAML built from the same config worked. It also dropped
allowInsecure, which vless and trojan both carry — a listener whose certificate
the client cannot verify exported a link that failed the handshake. The repeated
tlsParams() calls were hoisted while touching this.
hysteria2URIs reads users either as rows or as a map, and the two branches
disagreed: one asked certutil.ShouldSkipCertVerify and emitted only `insecure`,
the other asked clientSkipCert and emitted `insecure` plus `allowInsecure`. The
same listener therefore produced different links depending on how its users were
stored. Both now use clientSkipCert, the helper intended for share URIs, and
emit both keys.
Not fixed, for the record: an SS listener with simple-obfs exports a bare ss://
link with no `plugin=` entry, so the obfs configuration is lost. The client YAML
does translate it (plugin: obfs). Fixing the URI means committing to a SIP002
plugin encoding that varies between clients, which is worth deciding separately
rather than guessing at here.
* i18n: complete zh-CN and guard against the locale regressions already seen
zh-CN was missing seven listener hints that en.ts declares, so those tooltips
rendered in English for the primary UI language. Translated: client_auth_cert,
client_auth_type, ech_key, enable_pure_downlink, mux, padding and table_type.
The rest of the catalogue is deliberately left alone. Seventeen locales each lack
65 keys that en.ts has; the provider already resolves those from en.ts, so they
degrade to English rather than breaking, and filling 1100 strings with
unreviewed machine translation would be worse than the fallback. The test prints
the per-locale gap so it stays visible.
The new locale test guards the regressions this directory has actually hit: a
duplicated key inside one object (which breaks the type check), and a key renamed
in en.ts leaving an orphan behind. It also asserts zh-CN stays complete, since a
gap there reads as a defect rather than as translation lag.
* i18n: declare the 24 keys the UI asks for but no locale ever defined
Twenty-four keys were referenced from components without being declared in any
locale, so t() fell through to its hardcoded fallback. Where a fallback existed
that merely meant the string could never be translated; where none existed —
protocolHint, access_sniHint, jlsHint, shadowTlsHint and the rest — the tooltip
either rendered the raw key, "listeners.protocolHint", or disappeared entirely,
because fieldTip() suppresses a value that equals the key it looked up.
en.ts takes the text the code was already falling back to, so nothing changes on
screen; the strings just become translatable. The few without a fallback were
written from the surrounding context — the field they annotate, its placeholder,
and the wording of the neighbouring hints. trafficMultiplierHint is the exception:
its fallback was Chinese, which is wrong for the English catalogue, so it now has
a real English string.
zh-CN gets all twenty-four, which keeps it complete against en.ts.
The locale test now reads through the TypeScript AST. It previously parsed line by
line and these files contain minified regions — several "key":"value" pairs on one
line — so a whole block of keys was invisible to it, which is how this gap stayed
hidden. It also gained the assertion that would have caught the bug directly:
every dotted key handed to t() or fieldTip() must be declared in en.ts.
The remaining languages still lag, now by 89 keys rather than 65, and still fall
back to English; the test prints the per-locale figure.
* fix(uri): drop the vmess allowInsecure field the reference client ignores
The vmess skip-certificate field was added on internal consistency alone —
vless and trojan carry allowInsecure in their query strings, so vmess looked
like an oversight. Checked against the client that actually parses these links,
it is not one.
v2rayN's VmessQRCode model declares exactly: v, ps, add, port, id, aid, scy,
net, type, host, path, tls, sni, alpn, fp. There is no skip-certificate field
at all — neither allowInsecure nor insecure — so the field was dead weight in a
payload that v2rayN reads field by field. v2rayNG reads `insecure` instead, and
Xray-core has removed allowInsecure in favour of pinnedPeerCertSha256, so there
is no spelling that the reference clients agree on. Emitting one would have
produced a link that looks configured but is not.
The ws `host` fix stands: v2rayN's own documentation lists "ws host" as the
meaning of that field, so dropping it broke ws listeners that pin a Host.
The conformance test now pins the emitted payload to v2rayN's field set, so a
future field copied over from another protocol fails instead of being silently
ignored by every client.
Deliberately left alone, both confirmed against the official scheme and worth
deciding separately:
- hysteria2 links carry up/down. The Hysteria 2 URI specification is explicit
that the parameters "should never include client modes ... or bandwidth
values". Removing them changes behaviour for anyone relying on the link, so
it is flagged rather than removed. `insecure` is the official parameter;
allowInsecure is kept alongside it purely for clients that read that name.
- an SS listener with simple-obfs still exports a bare ss:// link with no
plugin entry, losing the obfuscation the client YAML does translate.
Also noted while checking: tuicURIs defaults congestion_control to bbr where
sing-box documents cubic as its default. Ours is set explicitly, which is
valid, but the comment claiming it is what "most TUIC clients" use is not
sourced.
* fix(uri): keep vmess allowInsecure after all
Reverts the revert. Dropping it was correct on the evidence and wrong on the
consequences: v2rayN ignores the field, v2rayNG spells it `insecure`, and
Xray-core has moved to pinnedPeerCertSha256 — but the clients that do read
allowInsecure are precisely the ones whose handshake fails without it, and an
ignored extra key costs the others nothing. Removing it would have traded a
working link for a tidy payload.
The conformance test keeps its guard, with allowInsecure recorded as an
explicit, justified exception rather than a silent inclusion: the field set is
still pinned to v2rayN's model, so any further field copied over from another
protocol has to argue its way in.
* fix(uri): restore the vmess ws Host in registry YAML, and stop dropping share links
Two leftovers from auditing the exporters end to end.
The registry's VMess compiler built ws-opts with the path only, so a listener
that pins a Host exported a client config with nothing to send as Host. The
vless YAML path in the same file already nests it under `headers` per the mihomo
wiki, so this just mirrors that — and the converter has always carried it, which
is how the difference went unnoticed.
The node URI share endpoint also had a tier that could not be reached. It walks
three tiers — m-ui, the registry, then the legacy builders — and the second one
gates the third on having neither a URI nor client YAML. But the registry
implements URI export for a different protocol set than the legacy builders: it
compiles vmess, snell, shadowquic, mieru, sudoku and trusttunnel to client YAML
without ever emitting a URI, so those listeners arrived at the third tier with a
YAML in hand and the condition already false, and reported no link at all even
though ClientURIsWithCredentials exports one. The third tier now runs whenever
the URI is still missing, and only fills in YAML that earlier tiers did not
already provide.
---------
Co-authored-by: 風雪 <kazeyukiro@users.noreply.github.com>
fix(vless): stop exporting flow on transports that cannot carry it (#87)
* fix(vless): stop exporting flow on transports that cannot carry it
The form promised that flow is cleared for ws/grpc/xhttp and the tooltip still
says so, but exported links kept carrying flow=xtls-rprx-vision. Two separate
reasons, both fixed.
The clear came too late to matter. formValuesToConfig wrote `flow` into the
config inside the protocol switch, and only afterwards did the "Vision is
TCP-only" rule run — where it mutated the form state, not the config it had
already been copied into. The transport is now resolved before anything writes,
and the drop also removes a flow inherited from the previous config, so changing
a listener's transport actually takes effect.
The URI generators never asked. Every one of them emitted whatever flow it found:
- node: vlessURIs decided flow before resolving the transport, so the type it
had just computed was irrelevant to the decision. It now resolves the
transport first. ClientURIsWithCredentials also merged the listener-level
flow onto every user on protocol alone, which reintroduced the value the
form had dropped.
- protocol: the share URI and the client YAML both wrote spec.Flow unguarded,
even though TransportSpec knows the network. Same for the server-side
compile, which pushed the listener default onto users — Mihomo rejects that
combination outright, so this one takes the listener down.
- mui: BuildVLESSShare set flow unconditionally, and bridge.go injected the
listener default into users before it was known to be applicable.
- converter: copyTransport resolved `network` but left a stale flow behind.
The rule is now one predicate per representation: TransportSpec.CarriesFlow,
VLESSHandlerSpec.CarriesFlow, and protocol.TransportCarriesFlow for raw configs.
TCP/raw keeps Vision; ws, grpc, xhttp, mkcp and mekya never emit it.
Tests pin each path in both directions — a stale flow disappears on ws/grpc/
xhttp, and a genuine TCP listener keeps its flow. The frontend one runs the real
form builder and fails, in three of five cases, against the previous ordering.
* fix(uri): carry vmess ws Host and align hysteria2 TLS flags across user shapes
Auditing the remaining exporters after the flow fix turned up two more places
where the link disagreed with the listener.
vmessURIs built its JSON by hand instead of going through transportParams, so a
ws listener that pins a Host header exported only the path: the client had
nothing to send as Host and the listener was unreachable through the link, even
though the client YAML built from the same config worked. It also dropped
allowInsecure, which vless and trojan both carry — a listener whose certificate
the client cannot verify exported a link that failed the handshake. The repeated
tlsParams() calls were hoisted while touching this.
hysteria2URIs reads users either as rows or as a map, and the two branches
disagreed: one asked certutil.ShouldSkipCertVerify and emitted only `insecure`,
the other asked clientSkipCert and emitted `insecure` plus `allowInsecure`. The
same listener therefore produced different links depending on how its users were
stored. Both now use clientSkipCert, the helper intended for share URIs, and
emit both keys.
Not fixed, for the record: an SS listener with simple-obfs exports a bare ss://
link with no `plugin=` entry, so the obfs configuration is lost. The client YAML
does translate it (plugin: obfs). Fixing the URI means committing to a SIP002
plugin encoding that varies between clients, which is worth deciding separately
rather than guessing at here.
* i18n: complete zh-CN and guard against the locale regressions already seen
zh-CN was missing seven listener hints that en.ts declares, so those tooltips
rendered in English for the primary UI language. Translated: client_auth_cert,
client_auth_type, ech_key, enable_pure_downlink, mux, padding and table_type.
The rest of the catalogue is deliberately left alone. Seventeen locales each lack
65 keys that en.ts has; the provider already resolves those from en.ts, so they
degrade to English rather than breaking, and filling 1100 strings with
unreviewed machine translation would be worse than the fallback. The test prints
the per-locale gap so it stays visible.
The new locale test guards the regressions this directory has actually hit: a
duplicated key inside one object (which breaks the type check), and a key renamed
in en.ts leaving an orphan behind. It also asserts zh-CN stays complete, since a
gap there reads as a defect rather than as translation lag.
* i18n: declare the 24 keys the UI asks for but no locale ever defined
Twenty-four keys were referenced from components without being declared in any
locale, so t() fell through to its hardcoded fallback. Where a fallback existed
that merely meant the string could never be translated; where none existed —
protocolHint, access_sniHint, jlsHint, shadowTlsHint and the rest — the tooltip
either rendered the raw key, "listeners.protocolHint", or disappeared entirely,
because fieldTip() suppresses a value that equals the key it looked up.
en.ts takes the text the code was already falling back to, so nothing changes on
screen; the strings just become translatable. The few without a fallback were
written from the surrounding context — the field they annotate, its placeholder,
and the wording of the neighbouring hints. trafficMultiplierHint is the exception:
its fallback was Chinese, which is wrong for the English catalogue, so it now has
a real English string.
zh-CN gets all twenty-four, which keeps it complete against en.ts.
The locale test now reads through the TypeScript AST. It previously parsed line by
line and these files contain minified regions — several "key":"value" pairs on one
line — so a whole block of keys was invisible to it, which is how this gap stayed
hidden. It also gained the assertion that would have caught the bug directly:
every dotted key handed to t() or fieldTip() must be declared in en.ts.
The remaining languages still lag, now by 89 keys rather than 65, and still fall
back to English; the test prints the per-locale figure.
* fix(uri): drop the vmess allowInsecure field the reference client ignores
The vmess skip-certificate field was added on internal consistency alone —
vless and trojan carry allowInsecure in their query strings, so vmess looked
like an oversight. Checked against the client that actually parses these links,
it is not one.
v2rayN's VmessQRCode model declares exactly: v, ps, add, port, id, aid, scy,
net, type, host, path, tls, sni, alpn, fp. There is no skip-certificate field
at all — neither allowInsecure nor insecure — so the field was dead weight in a
payload that v2rayN reads field by field. v2rayNG reads `insecure` instead, and
Xray-core has removed allowInsecure in favour of pinnedPeerCertSha256, so there
is no spelling that the reference clients agree on. Emitting one would have
produced a link that looks configured but is not.
The ws `host` fix stands: v2rayN's own documentation lists "ws host" as the
meaning of that field, so dropping it broke ws listeners that pin a Host.
The conformance test now pins the emitted payload to v2rayN's field set, so a
future field copied over from another protocol fails instead of being silently
ignored by every client.
Deliberately left alone, both confirmed against the official scheme and worth
deciding separately:
- hysteria2 links carry up/down. The Hysteria 2 URI specification is explicit
that the parameters "should never include client modes ... or bandwidth
values". Removing them changes behaviour for anyone relying on the link, so
it is flagged rather than removed. `insecure` is the official parameter;
allowInsecure is kept alongside it purely for clients that read that name.
- an SS listener with simple-obfs still exports a bare ss:// link with no
plugin entry, losing the obfuscation the client YAML does translate.
Also noted while checking: tuicURIs defaults congestion_control to bbr where
sing-box documents cubic as its default. Ours is set explicitly, which is
valid, but the comment claiming it is what "most TUIC clients" use is not
sourced.
* fix(uri): keep vmess allowInsecure after all
Reverts the revert. Dropping it was correct on the evidence and wrong on the
consequences: v2rayN ignores the field, v2rayNG spells it `insecure`, and
Xray-core has moved to pinnedPeerCertSha256 — but the clients that do read
allowInsecure are precisely the ones whose handshake fails without it, and an
ignored extra key costs the others nothing. Removing it would have traded a
working link for a tidy payload.
The conformance test keeps its guard, with allowInsecure recorded as an
explicit, justified exception rather than a silent inclusion: the field set is
still pinned to v2rayN's model, so any further field copied over from another
protocol has to argue its way in.
* fix(uri): restore the vmess ws Host in registry YAML, and stop dropping share links
Two leftovers from auditing the exporters end to end.
The registry's VMess compiler built ws-opts with the path only, so a listener
that pins a Host exported a client config with nothing to send as Host. The
vless YAML path in the same file already nests it under `headers` per the mihomo
wiki, so this just mirrors that — and the converter has always carried it, which
is how the difference went unnoticed.
The node URI share endpoint also had a tier that could not be reached. It walks
three tiers — m-ui, the registry, then the legacy builders — and the second one
gates the third on having neither a URI nor client YAML. But the registry
implements URI export for a different protocol set than the legacy builders: it
compiles vmess, snell, shadowquic, mieru, sudoku and trusttunnel to client YAML
without ever emitting a URI, so those listeners arrived at the third tier with a
YAML in hand and the condition already false, and reported no link at all even
though ClientURIsWithCredentials exports one. The third tier now runs whenever
the URI is still missing, and only fills in YAML that earlier tiers did not
already provide.
---------
Co-authored-by: 風雪 <kazeyukiro@users.noreply.github.com>
fix(vless): stop exporting flow on transports that cannot carry it (#87)
* fix(vless): stop exporting flow on transports that cannot carry it
The form promised that flow is cleared for ws/grpc/xhttp and the tooltip still
says so, but exported links kept carrying flow=xtls-rprx-vision. Two separate
reasons, both fixed.
The clear came too late to matter. formValuesToConfig wrote `flow` into the
config inside the protocol switch, and only afterwards did the "Vision is
TCP-only" rule run — where it mutated the form state, not the config it had
already been copied into. The transport is now resolved before anything writes,
and the drop also removes a flow inherited from the previous config, so changing
a listener's transport actually takes effect.
The URI generators never asked. Every one of them emitted whatever flow it found:
- node: vlessURIs decided flow before resolving the transport, so the type it
had just computed was irrelevant to the decision. It now resolves the
transport first. ClientURIsWithCredentials also merged the listener-level
flow onto every user on protocol alone, which reintroduced the value the
form had dropped.
- protocol: the share URI and the client YAML both wrote spec.Flow unguarded,
even though TransportSpec knows the network. Same for the server-side
compile, which pushed the listener default onto users — Mihomo rejects that
combination outright, so this one takes the listener down.
- mui: BuildVLESSShare set flow unconditionally, and bridge.go injected the
listener default into users before it was known to be applicable.
- converter: copyTransport resolved `network` but left a stale flow behind.
The rule is now one predicate per representation: TransportSpec.CarriesFlow,
VLESSHandlerSpec.CarriesFlow, and protocol.TransportCarriesFlow for raw configs.
TCP/raw keeps Vision; ws, grpc, xhttp, mkcp and mekya never emit it.
Tests pin each path in both directions — a stale flow disappears on ws/grpc/
xhttp, and a genuine TCP listener keeps its flow. The frontend one runs the real
form builder and fails, in three of five cases, against the previous ordering.
* fix(uri): carry vmess ws Host and align hysteria2 TLS flags across user shapes
Auditing the remaining exporters after the flow fix turned up two more places
where the link disagreed with the listener.
vmessURIs built its JSON by hand instead of going through transportParams, so a
ws listener that pins a Host header exported only the path: the client had
nothing to send as Host and the listener was unreachable through the link, even
though the client YAML built from the same config worked. It also dropped
allowInsecure, which vless and trojan both carry — a listener whose certificate
the client cannot verify exported a link that failed the handshake. The repeated
tlsParams() calls were hoisted while touching this.
hysteria2URIs reads users either as rows or as a map, and the two branches
disagreed: one asked certutil.ShouldSkipCertVerify and emitted only `insecure`,
the other asked clientSkipCert and emitted `insecure` plus `allowInsecure`. The
same listener therefore produced different links depending on how its users were
stored. Both now use clientSkipCert, the helper intended for share URIs, and
emit both keys.
Not fixed, for the record: an SS listener with simple-obfs exports a bare ss://
link with no `plugin=` entry, so the obfs configuration is lost. The client YAML
does translate it (plugin: obfs). Fixing the URI means committing to a SIP002
plugin encoding that varies between clients, which is worth deciding separately
rather than guessing at here.
* i18n: complete zh-CN and guard against the locale regressions already seen
zh-CN was missing seven listener hints that en.ts declares, so those tooltips
rendered in English for the primary UI language. Translated: client_auth_cert,
client_auth_type, ech_key, enable_pure_downlink, mux, padding and table_type.
The rest of the catalogue is deliberately left alone. Seventeen locales each lack
65 keys that en.ts has; the provider already resolves those from en.ts, so they
degrade to English rather than breaking, and filling 1100 strings with
unreviewed machine translation would be worse than the fallback. The test prints
the per-locale gap so it stays visible.
The new locale test guards the regressions this directory has actually hit: a
duplicated key inside one object (which breaks the type check), and a key renamed
in en.ts leaving an orphan behind. It also asserts zh-CN stays complete, since a
gap there reads as a defect rather than as translation lag.
* i18n: declare the 24 keys the UI asks for but no locale ever defined
Twenty-four keys were referenced from components without being declared in any
locale, so t() fell through to its hardcoded fallback. Where a fallback existed
that merely meant the string could never be translated; where none existed —
protocolHint, access_sniHint, jlsHint, shadowTlsHint and the rest — the tooltip
either rendered the raw key, "listeners.protocolHint", or disappeared entirely,
because fieldTip() suppresses a value that equals the key it looked up.
en.ts takes the text the code was already falling back to, so nothing changes on
screen; the strings just become translatable. The few without a fallback were
written from the surrounding context — the field they annotate, its placeholder,
and the wording of the neighbouring hints. trafficMultiplierHint is the exception:
its fallback was Chinese, which is wrong for the English catalogue, so it now has
a real English string.
zh-CN gets all twenty-four, which keeps it complete against en.ts.
The locale test now reads through the TypeScript AST. It previously parsed line by
line and these files contain minified regions — several "key":"value" pairs on one
line — so a whole block of keys was invisible to it, which is how this gap stayed
hidden. It also gained the assertion that would have caught the bug directly:
every dotted key handed to t() or fieldTip() must be declared in en.ts.
The remaining languages still lag, now by 89 keys rather than 65, and still fall
back to English; the test prints the per-locale figure.
* fix(uri): drop the vmess allowInsecure field the reference client ignores
The vmess skip-certificate field was added on internal consistency alone —
vless and trojan carry allowInsecure in their query strings, so vmess looked
like an oversight. Checked against the client that actually parses these links,
it is not one.
v2rayN's VmessQRCode model declares exactly: v, ps, add, port, id, aid, scy,
net, type, host, path, tls, sni, alpn, fp. There is no skip-certificate field
at all — neither allowInsecure nor insecure — so the field was dead weight in a
payload that v2rayN reads field by field. v2rayNG reads `insecure` instead, and
Xray-core has removed allowInsecure in favour of pinnedPeerCertSha256, so there
is no spelling that the reference clients agree on. Emitting one would have
produced a link that looks configured but is not.
The ws `host` fix stands: v2rayN's own documentation lists "ws host" as the
meaning of that field, so dropping it broke ws listeners that pin a Host.
The conformance test now pins the emitted payload to v2rayN's field set, so a
future field copied over from another protocol fails instead of being silently
ignored by every client.
Deliberately left alone, both confirmed against the official scheme and worth
deciding separately:
- hysteria2 links carry up/down. The Hysteria 2 URI specification is explicit
that the parameters "should never include client modes ... or bandwidth
values". Removing them changes behaviour for anyone relying on the link, so
it is flagged rather than removed. `insecure` is the official parameter;
allowInsecure is kept alongside it purely for clients that read that name.
- an SS listener with simple-obfs still exports a bare ss:// link with no
plugin entry, losing the obfuscation the client YAML does translate.
Also noted while checking: tuicURIs defaults congestion_control to bbr where
sing-box documents cubic as its default. Ours is set explicitly, which is
valid, but the comment claiming it is what "most TUIC clients" use is not
sourced.
* fix(uri): keep vmess allowInsecure after all
Reverts the revert. Dropping it was correct on the evidence and wrong on the
consequences: v2rayN ignores the field, v2rayNG spells it `insecure`, and
Xray-core has moved to pinnedPeerCertSha256 — but the clients that do read
allowInsecure are precisely the ones whose handshake fails without it, and an
ignored extra key costs the others nothing. Removing it would have traded a
working link for a tidy payload.
The conformance test keeps its guard, with allowInsecure recorded as an
explicit, justified exception rather than a silent inclusion: the field set is
still pinned to v2rayN's model, so any further field copied over from another
protocol has to argue its way in.
* fix(uri): restore the vmess ws Host in registry YAML, and stop dropping share links
Two leftovers from auditing the exporters end to end.
The registry's VMess compiler built ws-opts with the path only, so a listener
that pins a Host exported a client config with nothing to send as Host. The
vless YAML path in the same file already nests it under `headers` per the mihomo
wiki, so this just mirrors that — and the converter has always carried it, which
is how the difference went unnoticed.
The node URI share endpoint also had a tier that could not be reached. It walks
three tiers — m-ui, the registry, then the legacy builders — and the second one
gates the third on having neither a URI nor client YAML. But the registry
implements URI export for a different protocol set than the legacy builders: it
compiles vmess, snell, shadowquic, mieru, sudoku and trusttunnel to client YAML
without ever emitting a URI, so those listeners arrived at the third tier with a
YAML in hand and the condition already false, and reported no link at all even
though ClientURIsWithCredentials exports one. The third tier now runs whenever
the URI is still missing, and only fills in YAML that earlier tiers did not
already provide.
---------
Co-authored-by: 風雪 <kazeyukiro@users.noreply.github.com>
fix(vless): stop exporting flow on transports that cannot carry it (#87)
* fix(vless): stop exporting flow on transports that cannot carry it
The form promised that flow is cleared for ws/grpc/xhttp and the tooltip still
says so, but exported links kept carrying flow=xtls-rprx-vision. Two separate
reasons, both fixed.
The clear came too late to matter. formValuesToConfig wrote `flow` into the
config inside the protocol switch, and only afterwards did the "Vision is
TCP-only" rule run — where it mutated the form state, not the config it had
already been copied into. The transport is now resolved before anything writes,
and the drop also removes a flow inherited from the previous config, so changing
a listener's transport actually takes effect.
The URI generators never asked. Every one of them emitted whatever flow it found:
- node: vlessURIs decided flow before resolving the transport, so the type it
had just computed was irrelevant to the decision. It now resolves the
transport first. ClientURIsWithCredentials also merged the listener-level
flow onto every user on protocol alone, which reintroduced the value the
form had dropped.
- protocol: the share URI and the client YAML both wrote spec.Flow unguarded,
even though TransportSpec knows the network. Same for the server-side
compile, which pushed the listener default onto users — Mihomo rejects that
combination outright, so this one takes the listener down.
- mui: BuildVLESSShare set flow unconditionally, and bridge.go injected the
listener default into users before it was known to be applicable.
- converter: copyTransport resolved `network` but left a stale flow behind.
The rule is now one predicate per representation: TransportSpec.CarriesFlow,
VLESSHandlerSpec.CarriesFlow, and protocol.TransportCarriesFlow for raw configs.
TCP/raw keeps Vision; ws, grpc, xhttp, mkcp and mekya never emit it.
Tests pin each path in both directions — a stale flow disappears on ws/grpc/
xhttp, and a genuine TCP listener keeps its flow. The frontend one runs the real
form builder and fails, in three of five cases, against the previous ordering.
* fix(uri): carry vmess ws Host and align hysteria2 TLS flags across user shapes
Auditing the remaining exporters after the flow fix turned up two more places
where the link disagreed with the listener.
vmessURIs built its JSON by hand instead of going through transportParams, so a
ws listener that pins a Host header exported only the path: the client had
nothing to send as Host and the listener was unreachable through the link, even
though the client YAML built from the same config worked. It also dropped
allowInsecure, which vless and trojan both carry — a listener whose certificate
the client cannot verify exported a link that failed the handshake. The repeated
tlsParams() calls were hoisted while touching this.
hysteria2URIs reads users either as rows or as a map, and the two branches
disagreed: one asked certutil.ShouldSkipCertVerify and emitted only `insecure`,
the other asked clientSkipCert and emitted `insecure` plus `allowInsecure`. The
same listener therefore produced different links depending on how its users were
stored. Both now use clientSkipCert, the helper intended for share URIs, and
emit both keys.
Not fixed, for the record: an SS listener with simple-obfs exports a bare ss://
link with no `plugin=` entry, so the obfs configuration is lost. The client YAML
does translate it (plugin: obfs). Fixing the URI means committing to a SIP002
plugin encoding that varies between clients, which is worth deciding separately
rather than guessing at here.
* i18n: complete zh-CN and guard against the locale regressions already seen
zh-CN was missing seven listener hints that en.ts declares, so those tooltips
rendered in English for the primary UI language. Translated: client_auth_cert,
client_auth_type, ech_key, enable_pure_downlink, mux, padding and table_type.
The rest of the catalogue is deliberately left alone. Seventeen locales each lack
65 keys that en.ts has; the provider already resolves those from en.ts, so they
degrade to English rather than breaking, and filling 1100 strings with
unreviewed machine translation would be worse than the fallback. The test prints
the per-locale gap so it stays visible.
The new locale test guards the regressions this directory has actually hit: a
duplicated key inside one object (which breaks the type check), and a key renamed
in en.ts leaving an orphan behind. It also asserts zh-CN stays complete, since a
gap there reads as a defect rather than as translation lag.
* i18n: declare the 24 keys the UI asks for but no locale ever defined
Twenty-four keys were referenced from components without being declared in any
locale, so t() fell through to its hardcoded fallback. Where a fallback existed
that merely meant the string could never be translated; where none existed —
protocolHint, access_sniHint, jlsHint, shadowTlsHint and the rest — the tooltip
either rendered the raw key, "listeners.protocolHint", or disappeared entirely,
because fieldTip() suppresses a value that equals the key it looked up.
en.ts takes the text the code was already falling back to, so nothing changes on
screen; the strings just become translatable. The few without a fallback were
written from the surrounding context — the field they annotate, its placeholder,
and the wording of the neighbouring hints. trafficMultiplierHint is the exception:
its fallback was Chinese, which is wrong for the English catalogue, so it now has
a real English string.
zh-CN gets all twenty-four, which keeps it complete against en.ts.
The locale test now reads through the TypeScript AST. It previously parsed line by
line and these files contain minified regions — several "key":"value" pairs on one
line — so a whole block of keys was invisible to it, which is how this gap stayed
hidden. It also gained the assertion that would have caught the bug directly:
every dotted key handed to t() or fieldTip() must be declared in en.ts.
The remaining languages still lag, now by 89 keys rather than 65, and still fall
back to English; the test prints the per-locale figure.
* fix(uri): drop the vmess allowInsecure field the reference client ignores
The vmess skip-certificate field was added on internal consistency alone —
vless and trojan carry allowInsecure in their query strings, so vmess looked
like an oversight. Checked against the client that actually parses these links,
it is not one.
v2rayN's VmessQRCode model declares exactly: v, ps, add, port, id, aid, scy,
net, type, host, path, tls, sni, alpn, fp. There is no skip-certificate field
at all — neither allowInsecure nor insecure — so the field was dead weight in a
payload that v2rayN reads field by field. v2rayNG reads `insecure` instead, and
Xray-core has removed allowInsecure in favour of pinnedPeerCertSha256, so there
is no spelling that the reference clients agree on. Emitting one would have
produced a link that looks configured but is not.
The ws `host` fix stands: v2rayN's own documentation lists "ws host" as the
meaning of that field, so dropping it broke ws listeners that pin a Host.
The conformance test now pins the emitted payload to v2rayN's field set, so a
future field copied over from another protocol fails instead of being silently
ignored by every client.
Deliberately left alone, both confirmed against the official scheme and worth
deciding separately:
- hysteria2 links carry up/down. The Hysteria 2 URI specification is explicit
that the parameters "should never include client modes ... or bandwidth
values". Removing them changes behaviour for anyone relying on the link, so
it is flagged rather than removed. `insecure` is the official parameter;
allowInsecure is kept alongside it purely for clients that read that name.
- an SS listener with simple-obfs still exports a bare ss:// link with no
plugin entry, losing the obfuscation the client YAML does translate.
Also noted while checking: tuicURIs defaults congestion_control to bbr where
sing-box documents cubic as its default. Ours is set explicitly, which is
valid, but the comment claiming it is what "most TUIC clients" use is not
sourced.
* fix(uri): keep vmess allowInsecure after all
Reverts the revert. Dropping it was correct on the evidence and wrong on the
consequences: v2rayN ignores the field, v2rayNG spells it `insecure`, and
Xray-core has moved to pinnedPeerCertSha256 — but the clients that do read
allowInsecure are precisely the ones whose handshake fails without it, and an
ignored extra key costs the others nothing. Removing it would have traded a
working link for a tidy payload.
The conformance test keeps its guard, with allowInsecure recorded as an
explicit, justified exception rather than a silent inclusion: the field set is
still pinned to v2rayN's model, so any further field copied over from another
protocol has to argue its way in.
* fix(uri): restore the vmess ws Host in registry YAML, and stop dropping share links
Two leftovers from auditing the exporters end to end.
The registry's VMess compiler built ws-opts with the path only, so a listener
that pins a Host exported a client config with nothing to send as Host. The
vless YAML path in the same file already nests it under `headers` per the mihomo
wiki, so this just mirrors that — and the converter has always carried it, which
is how the difference went unnoticed.
The node URI share endpoint also had a tier that could not be reached. It walks
three tiers — m-ui, the registry, then the legacy builders — and the second one
gates the third on having neither a URI nor client YAML. But the registry
implements URI export for a different protocol set than the legacy builders: it
compiles vmess, snell, shadowquic, mieru, sudoku and trusttunnel to client YAML
without ever emitting a URI, so those listeners arrived at the third tier with a
YAML in hand and the condition already false, and reported no link at all even
though ClientURIsWithCredentials exports one. The third tier now runs whenever
the URI is still missing, and only fills in YAML that earlier tiers did not
already provide.
---------
Co-authored-by: 風雪 <kazeyukiro@users.noreply.github.com>
fix(vless): stop exporting flow on transports that cannot carry it (#87)
* fix(vless): stop exporting flow on transports that cannot carry it
The form promised that flow is cleared for ws/grpc/xhttp and the tooltip still
says so, but exported links kept carrying flow=xtls-rprx-vision. Two separate
reasons, both fixed.
The clear came too late to matter. formValuesToConfig wrote `flow` into the
config inside the protocol switch, and only afterwards did the "Vision is
TCP-only" rule run — where it mutated the form state, not the config it had
already been copied into. The transport is now resolved before anything writes,
and the drop also removes a flow inherited from the previous config, so changing
a listener's transport actually takes effect.
The URI generators never asked. Every one of them emitted whatever flow it found:
- node: vlessURIs decided flow before resolving the transport, so the type it
had just computed was irrelevant to the decision. It now resolves the
transport first. ClientURIsWithCredentials also merged the listener-level
flow onto every user on protocol alone, which reintroduced the value the
form had dropped.
- protocol: the share URI and the client YAML both wrote spec.Flow unguarded,
even though TransportSpec knows the network. Same for the server-side
compile, which pushed the listener default onto users — Mihomo rejects that
combination outright, so this one takes the listener down.
- mui: BuildVLESSShare set flow unconditionally, and bridge.go injected the
listener default into users before it was known to be applicable.
- converter: copyTransport resolved `network` but left a stale flow behind.
The rule is now one predicate per representation: TransportSpec.CarriesFlow,
VLESSHandlerSpec.CarriesFlow, and protocol.TransportCarriesFlow for raw configs.
TCP/raw keeps Vision; ws, grpc, xhttp, mkcp and mekya never emit it.
Tests pin each path in both directions — a stale flow disappears on ws/grpc/
xhttp, and a genuine TCP listener keeps its flow. The frontend one runs the real
form builder and fails, in three of five cases, against the previous ordering.
* fix(uri): carry vmess ws Host and align hysteria2 TLS flags across user shapes
Auditing the remaining exporters after the flow fix turned up two more places
where the link disagreed with the listener.
vmessURIs built its JSON by hand instead of going through transportParams, so a
ws listener that pins a Host header exported only the path: the client had
nothing to send as Host and the listener was unreachable through the link, even
though the client YAML built from the same config worked. It also dropped
allowInsecure, which vless and trojan both carry — a listener whose certificate
the client cannot verify exported a link that failed the handshake. The repeated
tlsParams() calls were hoisted while touching this.
hysteria2URIs reads users either as rows or as a map, and the two branches
disagreed: one asked certutil.ShouldSkipCertVerify and emitted only `insecure`,
the other asked clientSkipCert and emitted `insecure` plus `allowInsecure`. The
same listener therefore produced different links depending on how its users were
stored. Both now use clientSkipCert, the helper intended for share URIs, and
emit both keys.
Not fixed, for the record: an SS listener with simple-obfs exports a bare ss://
link with no `plugin=` entry, so the obfs configuration is lost. The client YAML
does translate it (plugin: obfs). Fixing the URI means committing to a SIP002
plugin encoding that varies between clients, which is worth deciding separately
rather than guessing at here.
* i18n: complete zh-CN and guard against the locale regressions already seen
zh-CN was missing seven listener hints that en.ts declares, so those tooltips
rendered in English for the primary UI language. Translated: client_auth_cert,
client_auth_type, ech_key, enable_pure_downlink, mux, padding and table_type.
The rest of the catalogue is deliberately left alone. Seventeen locales each lack
65 keys that en.ts has; the provider already resolves those from en.ts, so they
degrade to English rather than breaking, and filling 1100 strings with
unreviewed machine translation would be worse than the fallback. The test prints
the per-locale gap so it stays visible.
The new locale test guards the regressions this directory has actually hit: a
duplicated key inside one object (which breaks the type check), and a key renamed
in en.ts leaving an orphan behind. It also asserts zh-CN stays complete, since a
gap there reads as a defect rather than as translation lag.
* i18n: declare the 24 keys the UI asks for but no locale ever defined
Twenty-four keys were referenced from components without being declared in any
locale, so t() fell through to its hardcoded fallback. Where a fallback existed
that merely meant the string could never be translated; where none existed —
protocolHint, access_sniHint, jlsHint, shadowTlsHint and the rest — the tooltip
either rendered the raw key, "listeners.protocolHint", or disappeared entirely,
because fieldTip() suppresses a value that equals the key it looked up.
en.ts takes the text the code was already falling back to, so nothing changes on
screen; the strings just become translatable. The few without a fallback were
written from the surrounding context — the field they annotate, its placeholder,
and the wording of the neighbouring hints. trafficMultiplierHint is the exception:
its fallback was Chinese, which is wrong for the English catalogue, so it now has
a real English string.
zh-CN gets all twenty-four, which keeps it complete against en.ts.
The locale test now reads through the TypeScript AST. It previously parsed line by
line and these files contain minified regions — several "key":"value" pairs on one
line — so a whole block of keys was invisible to it, which is how this gap stayed
hidden. It also gained the assertion that would have caught the bug directly:
every dotted key handed to t() or fieldTip() must be declared in en.ts.
The remaining languages still lag, now by 89 keys rather than 65, and still fall
back to English; the test prints the per-locale figure.
* fix(uri): drop the vmess allowInsecure field the reference client ignores
The vmess skip-certificate field was added on internal consistency alone —
vless and trojan carry allowInsecure in their query strings, so vmess looked
like an oversight. Checked against the client that actually parses these links,
it is not one.
v2rayN's VmessQRCode model declares exactly: v, ps, add, port, id, aid, scy,
net, type, host, path, tls, sni, alpn, fp. There is no skip-certificate field
at all — neither allowInsecure nor insecure — so the field was dead weight in a
payload that v2rayN reads field by field. v2rayNG reads `insecure` instead, and
Xray-core has removed allowInsecure in favour of pinnedPeerCertSha256, so there
is no spelling that the reference clients agree on. Emitting one would have
produced a link that looks configured but is not.
The ws `host` fix stands: v2rayN's own documentation lists "ws host" as the
meaning of that field, so dropping it broke ws listeners that pin a Host.
The conformance test now pins the emitted payload to v2rayN's field set, so a
future field copied over from another protocol fails instead of being silently
ignored by every client.
Deliberately left alone, both confirmed against the official scheme and worth
deciding separately:
- hysteria2 links carry up/down. The Hysteria 2 URI specification is explicit
that the parameters "should never include client modes ... or bandwidth
values". Removing them changes behaviour for anyone relying on the link, so
it is flagged rather than removed. `insecure` is the official parameter;
allowInsecure is kept alongside it purely for clients that read that name.
- an SS listener with simple-obfs still exports a bare ss:// link with no
plugin entry, losing the obfuscation the client YAML does translate.
Also noted while checking: tuicURIs defaults congestion_control to bbr where
sing-box documents cubic as its default. Ours is set explicitly, which is
valid, but the comment claiming it is what "most TUIC clients" use is not
sourced.
* fix(uri): keep vmess allowInsecure after all
Reverts the revert. Dropping it was correct on the evidence and wrong on the
consequences: v2rayN ignores the field, v2rayNG spells it `insecure`, and
Xray-core has moved to pinnedPeerCertSha256 — but the clients that do read
allowInsecure are precisely the ones whose handshake fails without it, and an
ignored extra key costs the others nothing. Removing it would have traded a
working link for a tidy payload.
The conformance test keeps its guard, with allowInsecure recorded as an
explicit, justified exception rather than a silent inclusion: the field set is
still pinned to v2rayN's model, so any further field copied over from another
protocol has to argue its way in.
* fix(uri): restore the vmess ws Host in registry YAML, and stop dropping share links
Two leftovers from auditing the exporters end to end.
The registry's VMess compiler built ws-opts with the path only, so a listener
that pins a Host exported a client config with nothing to send as Host. The
vless YAML path in the same file already nests it under `headers` per the mihomo
wiki, so this just mirrors that — and the converter has always carried it, which
is how the difference went unnoticed.
The node URI share endpoint also had a tier that could not be reached. It walks
three tiers — m-ui, the registry, then the legacy builders — and the second one
gates the third on having neither a URI nor client YAML. But the registry
implements URI export for a different protocol set than the legacy builders: it
compiles vmess, snell, shadowquic, mieru, sudoku and trusttunnel to client YAML
without ever emitting a URI, so those listeners arrived at the third tier with a
YAML in hand and the condition already false, and reported no link at all even
though ClientURIsWithCredentials exports one. The third tier now runs whenever
the URI is still missing, and only fills in YAML that earlier tiers did not
already provide.
---------
Co-authored-by: 風雪 <kazeyukiro@users.noreply.github.com>
fix(vless): stop exporting flow on transports that cannot carry it (#87)
* fix(vless): stop exporting flow on transports that cannot carry it
The form promised that flow is cleared for ws/grpc/xhttp and the tooltip still
says so, but exported links kept carrying flow=xtls-rprx-vision. Two separate
reasons, both fixed.
The clear came too late to matter. formValuesToConfig wrote `flow` into the
config inside the protocol switch, and only afterwards did the "Vision is
TCP-only" rule run — where it mutated the form state, not the config it had
already been copied into. The transport is now resolved before anything writes,
and the drop also removes a flow inherited from the previous config, so changing
a listener's transport actually takes effect.
The URI generators never asked. Every one of them emitted whatever flow it found:
- node: vlessURIs decided flow before resolving the transport, so the type it
had just computed was irrelevant to the decision. It now resolves the
transport first. ClientURIsWithCredentials also merged the listener-level
flow onto every user on protocol alone, which reintroduced the value the
form had dropped.
- protocol: the share URI and the client YAML both wrote spec.Flow unguarded,
even though TransportSpec knows the network. Same for the server-side
compile, which pushed the listener default onto users — Mihomo rejects that
combination outright, so this one takes the listener down.
- mui: BuildVLESSShare set flow unconditionally, and bridge.go injected the
listener default into users before it was known to be applicable.
- converter: copyTransport resolved `network` but left a stale flow behind.
The rule is now one predicate per representation: TransportSpec.CarriesFlow,
VLESSHandlerSpec.CarriesFlow, and protocol.TransportCarriesFlow for raw configs.
TCP/raw keeps Vision; ws, grpc, xhttp, mkcp and mekya never emit it.
Tests pin each path in both directions — a stale flow disappears on ws/grpc/
xhttp, and a genuine TCP listener keeps its flow. The frontend one runs the real
form builder and fails, in three of five cases, against the previous ordering.
* fix(uri): carry vmess ws Host and align hysteria2 TLS flags across user shapes
Auditing the remaining exporters after the flow fix turned up two more places
where the link disagreed with the listener.
vmessURIs built its JSON by hand instead of going through transportParams, so a
ws listener that pins a Host header exported only the path: the client had
nothing to send as Host and the listener was unreachable through the link, even
though the client YAML built from the same config worked. It also dropped
allowInsecure, which vless and trojan both carry — a listener whose certificate
the client cannot verify exported a link that failed the handshake. The repeated
tlsParams() calls were hoisted while touching this.
hysteria2URIs reads users either as rows or as a map, and the two branches
disagreed: one asked certutil.ShouldSkipCertVerify and emitted only `insecure`,
the other asked clientSkipCert and emitted `insecure` plus `allowInsecure`. The
same listener therefore produced different links depending on how its users were
stored. Both now use clientSkipCert, the helper intended for share URIs, and
emit both keys.
Not fixed, for the record: an SS listener with simple-obfs exports a bare ss://
link with no `plugin=` entry, so the obfs configuration is lost. The client YAML
does translate it (plugin: obfs). Fixing the URI means committing to a SIP002
plugin encoding that varies between clients, which is worth deciding separately
rather than guessing at here.
* i18n: complete zh-CN and guard against the locale regressions already seen
zh-CN was missing seven listener hints that en.ts declares, so those tooltips
rendered in English for the primary UI language. Translated: client_auth_cert,
client_auth_type, ech_key, enable_pure_downlink, mux, padding and table_type.
The rest of the catalogue is deliberately left alone. Seventeen locales each lack
65 keys that en.ts has; the provider already resolves those from en.ts, so they
degrade to English rather than breaking, and filling 1100 strings with
unreviewed machine translation would be worse than the fallback. The test prints
the per-locale gap so it stays visible.
The new locale test guards the regressions this directory has actually hit: a
duplicated key inside one object (which breaks the type check), and a key renamed
in en.ts leaving an orphan behind. It also asserts zh-CN stays complete, since a
gap there reads as a defect rather than as translation lag.
* i18n: declare the 24 keys the UI asks for but no locale ever defined
Twenty-four keys were referenced from components without being declared in any
locale, so t() fell through to its hardcoded fallback. Where a fallback existed
that merely meant the string could never be translated; where none existed —
protocolHint, access_sniHint, jlsHint, shadowTlsHint and the rest — the tooltip
either rendered the raw key, "listeners.protocolHint", or disappeared entirely,
because fieldTip() suppresses a value that equals the key it looked up.
en.ts takes the text the code was already falling back to, so nothing changes on
screen; the strings just become translatable. The few without a fallback were
written from the surrounding context — the field they annotate, its placeholder,
and the wording of the neighbouring hints. trafficMultiplierHint is the exception:
its fallback was Chinese, which is wrong for the English catalogue, so it now has
a real English string.
zh-CN gets all twenty-four, which keeps it complete against en.ts.
The locale test now reads through the TypeScript AST. It previously parsed line by
line and these files contain minified regions — several "key":"value" pairs on one
line — so a whole block of keys was invisible to it, which is how this gap stayed
hidden. It also gained the assertion that would have caught the bug directly:
every dotted key handed to t() or fieldTip() must be declared in en.ts.
The remaining languages still lag, now by 89 keys rather than 65, and still fall
back to English; the test prints the per-locale figure.
* fix(uri): drop the vmess allowInsecure field the reference client ignores
The vmess skip-certificate field was added on internal consistency alone —
vless and trojan carry allowInsecure in their query strings, so vmess looked
like an oversight. Checked against the client that actually parses these links,
it is not one.
v2rayN's VmessQRCode model declares exactly: v, ps, add, port, id, aid, scy,
net, type, host, path, tls, sni, alpn, fp. There is no skip-certificate field
at all — neither allowInsecure nor insecure — so the field was dead weight in a
payload that v2rayN reads field by field. v2rayNG reads `insecure` instead, and
Xray-core has removed allowInsecure in favour of pinnedPeerCertSha256, so there
is no spelling that the reference clients agree on. Emitting one would have
produced a link that looks configured but is not.
The ws `host` fix stands: v2rayN's own documentation lists "ws host" as the
meaning of that field, so dropping it broke ws listeners that pin a Host.
The conformance test now pins the emitted payload to v2rayN's field set, so a
future field copied over from another protocol fails instead of being silently
ignored by every client.
Deliberately left alone, both confirmed against the official scheme and worth
deciding separately:
- hysteria2 links carry up/down. The Hysteria 2 URI specification is explicit
that the parameters "should never include client modes ... or bandwidth
values". Removing them changes behaviour for anyone relying on the link, so
it is flagged rather than removed. `insecure` is the official parameter;
allowInsecure is kept alongside it purely for clients that read that name.
- an SS listener with simple-obfs still exports a bare ss:// link with no
plugin entry, losing the obfuscation the client YAML does translate.
Also noted while checking: tuicURIs defaults congestion_control to bbr where
sing-box documents cubic as its default. Ours is set explicitly, which is
valid, but the comment claiming it is what "most TUIC clients" use is not
sourced.
* fix(uri): keep vmess allowInsecure after all
Reverts the revert. Dropping it was correct on the evidence and wrong on the
consequences: v2rayN ignores the field, v2rayNG spells it `insecure`, and
Xray-core has moved to pinnedPeerCertSha256 — but the clients that do read
allowInsecure are precisely the ones whose handshake fails without it, and an
ignored extra key costs the others nothing. Removing it would have traded a
working link for a tidy payload.
The conformance test keeps its guard, with allowInsecure recorded as an
explicit, justified exception rather than a silent inclusion: the field set is
still pinned to v2rayN's model, so any further field copied over from another
protocol has to argue its way in.
* fix(uri): restore the vmess ws Host in registry YAML, and stop dropping share links
Two leftovers from auditing the exporters end to end.
The registry's VMess compiler built ws-opts with the path only, so a listener
that pins a Host exported a client config with nothing to send as Host. The
vless YAML path in the same file already nests it under `headers` per the mihomo
wiki, so this just mirrors that — and the converter has always carried it, which
is how the difference went unnoticed.
The node URI share endpoint also had a tier that could not be reached. It walks
three tiers — m-ui, the registry, then the legacy builders — and the second one
gates the third on having neither a URI nor client YAML. But the registry
implements URI export for a different protocol set than the legacy builders: it
compiles vmess, snell, shadowquic, mieru, sudoku and trusttunnel to client YAML
without ever emitting a URI, so those listeners arrived at the third tier with a
YAML in hand and the condition already false, and reported no link at all even
though ClientURIsWithCredentials exports one. The third tier now runs whenever
the URI is still missing, and only fills in YAML that earlier tiers did not
already provide.
---------
Co-authored-by: 風雪 <kazeyukiro@users.noreply.github.com>
fix(vless): stop exporting flow on transports that cannot carry it (#87)
* fix(vless): stop exporting flow on transports that cannot carry it
The form promised that flow is cleared for ws/grpc/xhttp and the tooltip still
says so, but exported links kept carrying flow=xtls-rprx-vision. Two separate
reasons, both fixed.
The clear came too late to matter. formValuesToConfig wrote `flow` into the
config inside the protocol switch, and only afterwards did the "Vision is
TCP-only" rule run — where it mutated the form state, not the config it had
already been copied into. The transport is now resolved before anything writes,
and the drop also removes a flow inherited from the previous config, so changing
a listener's transport actually takes effect.
The URI generators never asked. Every one of them emitted whatever flow it found:
- node: vlessURIs decided flow before resolving the transport, so the type it
had just computed was irrelevant to the decision. It now resolves the
transport first. ClientURIsWithCredentials also merged the listener-level
flow onto every user on protocol alone, which reintroduced the value the
form had dropped.
- protocol: the share URI and the client YAML both wrote spec.Flow unguarded,
even though TransportSpec knows the network. Same for the server-side
compile, which pushed the listener default onto users — Mihomo rejects that
combination outright, so this one takes the listener down.
- mui: BuildVLESSShare set flow unconditionally, and bridge.go injected the
listener default into users before it was known to be applicable.
- converter: copyTransport resolved `network` but left a stale flow behind.
The rule is now one predicate per representation: TransportSpec.CarriesFlow,
VLESSHandlerSpec.CarriesFlow, and protocol.TransportCarriesFlow for raw configs.
TCP/raw keeps Vision; ws, grpc, xhttp, mkcp and mekya never emit it.
Tests pin each path in both directions — a stale flow disappears on ws/grpc/
xhttp, and a genuine TCP listener keeps its flow. The frontend one runs the real
form builder and fails, in three of five cases, against the previous ordering.
* fix(uri): carry vmess ws Host and align hysteria2 TLS flags across user shapes
Auditing the remaining exporters after the flow fix turned up two more places
where the link disagreed with the listener.
vmessURIs built its JSON by hand instead of going through transportParams, so a
ws listener that pins a Host header exported only the path: the client had
nothing to send as Host and the listener was unreachable through the link, even
though the client YAML built from the same config worked. It also dropped
allowInsecure, which vless and trojan both carry — a listener whose certificate
the client cannot verify exported a link that failed the handshake. The repeated
tlsParams() calls were hoisted while touching this.
hysteria2URIs reads users either as rows or as a map, and the two branches
disagreed: one asked certutil.ShouldSkipCertVerify and emitted only `insecure`,
the other asked clientSkipCert and emitted `insecure` plus `allowInsecure`. The
same listener therefore produced different links depending on how its users were
stored. Both now use clientSkipCert, the helper intended for share URIs, and
emit both keys.
Not fixed, for the record: an SS listener with simple-obfs exports a bare ss://
link with no `plugin=` entry, so the obfs configuration is lost. The client YAML
does translate it (plugin: obfs). Fixing the URI means committing to a SIP002
plugin encoding that varies between clients, which is worth deciding separately
rather than guessing at here.
* i18n: complete zh-CN and guard against the locale regressions already seen
zh-CN was missing seven listener hints that en.ts declares, so those tooltips
rendered in English for the primary UI language. Translated: client_auth_cert,
client_auth_type, ech_key, enable_pure_downlink, mux, padding and table_type.
The rest of the catalogue is deliberately left alone. Seventeen locales each lack
65 keys that en.ts has; the provider already resolves those from en.ts, so they
degrade to English rather than breaking, and filling 1100 strings with
unreviewed machine translation would be worse than the fallback. The test prints
the per-locale gap so it stays visible.
The new locale test guards the regressions this directory has actually hit: a
duplicated key inside one object (which breaks the type check), and a key renamed
in en.ts leaving an orphan behind. It also asserts zh-CN stays complete, since a
gap there reads as a defect rather than as translation lag.
* i18n: declare the 24 keys the UI asks for but no locale ever defined
Twenty-four keys were referenced from components without being declared in any
locale, so t() fell through to its hardcoded fallback. Where a fallback existed
that merely meant the string could never be translated; where none existed —
protocolHint, access_sniHint, jlsHint, shadowTlsHint and the rest — the tooltip
either rendered the raw key, "listeners.protocolHint", or disappeared entirely,
because fieldTip() suppresses a value that equals the key it looked up.
en.ts takes the text the code was already falling back to, so nothing changes on
screen; the strings just become translatable. The few without a fallback were
written from the surrounding context — the field they annotate, its placeholder,
and the wording of the neighbouring hints. trafficMultiplierHint is the exception:
its fallback was Chinese, which is wrong for the English catalogue, so it now has
a real English string.
zh-CN gets all twenty-four, which keeps it complete against en.ts.
The locale test now reads through the TypeScript AST. It previously parsed line by
line and these files contain minified regions — several "key":"value" pairs on one
line — so a whole block of keys was invisible to it, which is how this gap stayed
hidden. It also gained the assertion that would have caught the bug directly:
every dotted key handed to t() or fieldTip() must be declared in en.ts.
The remaining languages still lag, now by 89 keys rather than 65, and still fall
back to English; the test prints the per-locale figure.
* fix(uri): drop the vmess allowInsecure field the reference client ignores
The vmess skip-certificate field was added on internal consistency alone —
vless and trojan carry allowInsecure in their query strings, so vmess looked
like an oversight. Checked against the client that actually parses these links,
it is not one.
v2rayN's VmessQRCode model declares exactly: v, ps, add, port, id, aid, scy,
net, type, host, path, tls, sni, alpn, fp. There is no skip-certificate field
at all — neither allowInsecure nor insecure — so the field was dead weight in a
payload that v2rayN reads field by field. v2rayNG reads `insecure` instead, and
Xray-core has removed allowInsecure in favour of pinnedPeerCertSha256, so there
is no spelling that the reference clients agree on. Emitting one would have
produced a link that looks configured but is not.
The ws `host` fix stands: v2rayN's own documentation lists "ws host" as the
meaning of that field, so dropping it broke ws listeners that pin a Host.
The conformance test now pins the emitted payload to v2rayN's field set, so a
future field copied over from another protocol fails instead of being silently
ignored by every client.
Deliberately left alone, both confirmed against the official scheme and worth
deciding separately:
- hysteria2 links carry up/down. The Hysteria 2 URI specification is explicit
that the parameters "should never include client modes ... or bandwidth
values". Removing them changes behaviour for anyone relying on the link, so
it is flagged rather than removed. `insecure` is the official parameter;
allowInsecure is kept alongside it purely for clients that read that name.
- an SS listener with simple-obfs still exports a bare ss:// link with no
plugin entry, losing the obfuscation the client YAML does translate.
Also noted while checking: tuicURIs defaults congestion_control to bbr where
sing-box documents cubic as its default. Ours is set explicitly, which is
valid, but the comment claiming it is what "most TUIC clients" use is not
sourced.
* fix(uri): keep vmess allowInsecure after all
Reverts the revert. Dropping it was correct on the evidence and wrong on the
consequences: v2rayN ignores the field, v2rayNG spells it `insecure`, and
Xray-core has moved to pinnedPeerCertSha256 — but the clients that do read
allowInsecure are precisely the ones whose handshake fails without it, and an
ignored extra key costs the others nothing. Removing it would have traded a
working link for a tidy payload.
The conformance test keeps its guard, with allowInsecure recorded as an
explicit, justified exception rather than a silent inclusion: the field set is
still pinned to v2rayN's model, so any further field copied over from another
protocol has to argue its way in.
* fix(uri): restore the vmess ws Host in registry YAML, and stop dropping share links
Two leftovers from auditing the exporters end to end.
The registry's VMess compiler built ws-opts with the path only, so a listener
that pins a Host exported a client config with nothing to send as Host. The
vless YAML path in the same file already nests it under `headers` per the mihomo
wiki, so this just mirrors that — and the converter has always carried it, which
is how the difference went unnoticed.
The node URI share endpoint also had a tier that could not be reached. It walks
three tiers — m-ui, the registry, then the legacy builders — and the second one
gates the third on having neither a URI nor client YAML. But the registry
implements URI export for a different protocol set than the legacy builders: it
compiles vmess, snell, shadowquic, mieru, sudoku and trusttunnel to client YAML
without ever emitting a URI, so those listeners arrived at the third tier with a
YAML in hand and the condition already false, and reported no link at all even
though ClientURIsWithCredentials exports one. The third tier now runs whenever
the URI is still missing, and only fills in YAML that earlier tiers did not
already provide.
---------
Co-authored-by: 風雪 <kazeyukiro@users.noreply.github.com>
fix(vless): stop exporting flow on transports that cannot carry it (#87)
* fix(vless): stop exporting flow on transports that cannot carry it
The form promised that flow is cleared for ws/grpc/xhttp and the tooltip still
says so, but exported links kept carrying flow=xtls-rprx-vision. Two separate
reasons, both fixed.
The clear came too late to matter. formValuesToConfig wrote `flow` into the
config inside the protocol switch, and only afterwards did the "Vision is
TCP-only" rule run — where it mutated the form state, not the config it had
already been copied into. The transport is now resolved before anything writes,
and the drop also removes a flow inherited from the previous config, so changing
a listener's transport actually takes effect.
The URI generators never asked. Every one of them emitted whatever flow it found:
- node: vlessURIs decided flow before resolving the transport, so the type it
had just computed was irrelevant to the decision. It now resolves the
transport first. ClientURIsWithCredentials also merged the listener-level
flow onto every user on protocol alone, which reintroduced the value the
form had dropped.
- protocol: the share URI and the client YAML both wrote spec.Flow unguarded,
even though TransportSpec knows the network. Same for the server-side
compile, which pushed the listener default onto users — Mihomo rejects that
combination outright, so this one takes the listener down.
- mui: BuildVLESSShare set flow unconditionally, and bridge.go injected the
listener default into users before it was known to be applicable.
- converter: copyTransport resolved `network` but left a stale flow behind.
The rule is now one predicate per representation: TransportSpec.CarriesFlow,
VLESSHandlerSpec.CarriesFlow, and protocol.TransportCarriesFlow for raw configs.
TCP/raw keeps Vision; ws, grpc, xhttp, mkcp and mekya never emit it.
Tests pin each path in both directions — a stale flow disappears on ws/grpc/
xhttp, and a genuine TCP listener keeps its flow. The frontend one runs the real
form builder and fails, in three of five cases, against the previous ordering.
* fix(uri): carry vmess ws Host and align hysteria2 TLS flags across user shapes
Auditing the remaining exporters after the flow fix turned up two more places
where the link disagreed with the listener.
vmessURIs built its JSON by hand instead of going through transportParams, so a
ws listener that pins a Host header exported only the path: the client had
nothing to send as Host and the listener was unreachable through the link, even
though the client YAML built from the same config worked. It also dropped
allowInsecure, which vless and trojan both carry — a listener whose certificate
the client cannot verify exported a link that failed the handshake. The repeated
tlsParams() calls were hoisted while touching this.
hysteria2URIs reads users either as rows or as a map, and the two branches
disagreed: one asked certutil.ShouldSkipCertVerify and emitted only `insecure`,
the other asked clientSkipCert and emitted `insecure` plus `allowInsecure`. The
same listener therefore produced different links depending on how its users were
stored. Both now use clientSkipCert, the helper intended for share URIs, and
emit both keys.
Not fixed, for the record: an SS listener with simple-obfs exports a bare ss://
link with no `plugin=` entry, so the obfs configuration is lost. The client YAML
does translate it (plugin: obfs). Fixing the URI means committing to a SIP002
plugin encoding that varies between clients, which is worth deciding separately
rather than guessing at here.
* i18n: complete zh-CN and guard against the locale regressions already seen
zh-CN was missing seven listener hints that en.ts declares, so those tooltips
rendered in English for the primary UI language. Translated: client_auth_cert,
client_auth_type, ech_key, enable_pure_downlink, mux, padding and table_type.
The rest of the catalogue is deliberately left alone. Seventeen locales each lack
65 keys that en.ts has; the provider already resolves those from en.ts, so they
degrade to English rather than breaking, and filling 1100 strings with
unreviewed machine translation would be worse than the fallback. The test prints
the per-locale gap so it stays visible.
The new locale test guards the regressions this directory has actually hit: a
duplicated key inside one object (which breaks the type check), and a key renamed
in en.ts leaving an orphan behind. It also asserts zh-CN stays complete, since a
gap there reads as a defect rather than as translation lag.
* i18n: declare the 24 keys the UI asks for but no locale ever defined
Twenty-four keys were referenced from components without being declared in any
locale, so t() fell through to its hardcoded fallback. Where a fallback existed
that merely meant the string could never be translated; where none existed —
protocolHint, access_sniHint, jlsHint, shadowTlsHint and the rest — the tooltip
either rendered the raw key, "listeners.protocolHint", or disappeared entirely,
because fieldTip() suppresses a value that equals the key it looked up.
en.ts takes the text the code was already falling back to, so nothing changes on
screen; the strings just become translatable. The few without a fallback were
written from the surrounding context — the field they annotate, its placeholder,
and the wording of the neighbouring hints. trafficMultiplierHint is the exception:
its fallback was Chinese, which is wrong for the English catalogue, so it now has
a real English string.
zh-CN gets all twenty-four, which keeps it complete against en.ts.
The locale test now reads through the TypeScript AST. It previously parsed line by
line and these files contain minified regions — several "key":"value" pairs on one
line — so a whole block of keys was invisible to it, which is how this gap stayed
hidden. It also gained the assertion that would have caught the bug directly:
every dotted key handed to t() or fieldTip() must be declared in en.ts.
The remaining languages still lag, now by 89 keys rather than 65, and still fall
back to English; the test prints the per-locale figure.
* fix(uri): drop the vmess allowInsecure field the reference client ignores
The vmess skip-certificate field was added on internal consistency alone —
vless and trojan carry allowInsecure in their query strings, so vmess looked
like an oversight. Checked against the client that actually parses these links,
it is not one.
v2rayN's VmessQRCode model declares exactly: v, ps, add, port, id, aid, scy,
net, type, host, path, tls, sni, alpn, fp. There is no skip-certificate field
at all — neither allowInsecure nor insecure — so the field was dead weight in a
payload that v2rayN reads field by field. v2rayNG reads `insecure` instead, and
Xray-core has removed allowInsecure in favour of pinnedPeerCertSha256, so there
is no spelling that the reference clients agree on. Emitting one would have
produced a link that looks configured but is not.
The ws `host` fix stands: v2rayN's own documentation lists "ws host" as the
meaning of that field, so dropping it broke ws listeners that pin a Host.
The conformance test now pins the emitted payload to v2rayN's field set, so a
future field copied over from another protocol fails instead of being silently
ignored by every client.
Deliberately left alone, both confirmed against the official scheme and worth
deciding separately:
- hysteria2 links carry up/down. The Hysteria 2 URI specification is explicit
that the parameters "should never include client modes ... or bandwidth
values". Removing them changes behaviour for anyone relying on the link, so
it is flagged rather than removed. `insecure` is the official parameter;
allowInsecure is kept alongside it purely for clients that read that name.
- an SS listener with simple-obfs still exports a bare ss:// link with no
plugin entry, losing the obfuscation the client YAML does translate.
Also noted while checking: tuicURIs defaults congestion_control to bbr where
sing-box documents cubic as its default. Ours is set explicitly, which is
valid, but the comment claiming it is what "most TUIC clients" use is not
sourced.
* fix(uri): keep vmess allowInsecure after all
Reverts the revert. Dropping it was correct on the evidence and wrong on the
consequences: v2rayN ignores the field, v2rayNG spells it `insecure`, and
Xray-core has moved to pinnedPeerCertSha256 — but the clients that do read
allowInsecure are precisely the ones whose handshake fails without it, and an
ignored extra key costs the others nothing. Removing it would have traded a
working link for a tidy payload.
The conformance test keeps its guard, with allowInsecure recorded as an
explicit, justified exception rather than a silent inclusion: the field set is
still pinned to v2rayN's model, so any further field copied over from another
protocol has to argue its way in.
* fix(uri): restore the vmess ws Host in registry YAML, and stop dropping share links
Two leftovers from auditing the exporters end to end.
The registry's VMess compiler built ws-opts with the path only, so a listener
that pins a Host exported a client config with nothing to send as Host. The
vless YAML path in the same file already nests it under `headers` per the mihomo
wiki, so this just mirrors that — and the converter has always carried it, which
is how the difference went unnoticed.
The node URI share endpoint also had a tier that could not be reached. It walks
three tiers — m-ui, the registry, then the legacy builders — and the second one
gates the third on having neither a URI nor client YAML. But the registry
implements URI export for a different protocol set than the legacy builders: it
compiles vmess, snell, shadowquic, mieru, sudoku and trusttunnel to client YAML
without ever emitting a URI, so those listeners arrived at the third tier with a
YAML in hand and the condition already false, and reported no link at all even
though ClientURIsWithCredentials exports one. The third tier now runs whenever
the URI is still missing, and only fills in YAML that earlier tiers did not
already provide.
---------
Co-authored-by: 風雪 <kazeyukiro@users.noreply.github.com>
fix(vless): stop exporting flow on transports that cannot carry it (#87)
* fix(vless): stop exporting flow on transports that cannot carry it
The form promised that flow is cleared for ws/grpc/xhttp and the tooltip still
says so, but exported links kept carrying flow=xtls-rprx-vision. Two separate
reasons, both fixed.
The clear came too late to matter. formValuesToConfig wrote `flow` into the
config inside the protocol switch, and only afterwards did the "Vision is
TCP-only" rule run — where it mutated the form state, not the config it had
already been copied into. The transport is now resolved before anything writes,
and the drop also removes a flow inherited from the previous config, so changing
a listener's transport actually takes effect.
The URI generators never asked. Every one of them emitted whatever flow it found:
- node: vlessURIs decided flow before resolving the transport, so the type it
had just computed was irrelevant to the decision. It now resolves the
transport first. ClientURIsWithCredentials also merged the listener-level
flow onto every user on protocol alone, which reintroduced the value the
form had dropped.
- protocol: the share URI and the client YAML both wrote spec.Flow unguarded,
even though TransportSpec knows the network. Same for the server-side
compile, which pushed the listener default onto users — Mihomo rejects that
combination outright, so this one takes the listener down.
- mui: BuildVLESSShare set flow unconditionally, and bridge.go injected the
listener default into users before it was known to be applicable.
- converter: copyTransport resolved `network` but left a stale flow behind.
The rule is now one predicate per representation: TransportSpec.CarriesFlow,
VLESSHandlerSpec.CarriesFlow, and protocol.TransportCarriesFlow for raw configs.
TCP/raw keeps Vision; ws, grpc, xhttp, mkcp and mekya never emit it.
Tests pin each path in both directions — a stale flow disappears on ws/grpc/
xhttp, and a genuine TCP listener keeps its flow. The frontend one runs the real
form builder and fails, in three of five cases, against the previous ordering.
* fix(uri): carry vmess ws Host and align hysteria2 TLS flags across user shapes
Auditing the remaining exporters after the flow fix turned up two more places
where the link disagreed with the listener.
vmessURIs built its JSON by hand instead of going through transportParams, so a
ws listener that pins a Host header exported only the path: the client had
nothing to send as Host and the listener was unreachable through the link, even
though the client YAML built from the same config worked. It also dropped
allowInsecure, which vless and trojan both carry — a listener whose certificate
the client cannot verify exported a link that failed the handshake. The repeated
tlsParams() calls were hoisted while touching this.
hysteria2URIs reads users either as rows or as a map, and the two branches
disagreed: one asked certutil.ShouldSkipCertVerify and emitted only `insecure`,
the other asked clientSkipCert and emitted `insecure` plus `allowInsecure`. The
same listener therefore produced different links depending on how its users were
stored. Both now use clientSkipCert, the helper intended for share URIs, and
emit both keys.
Not fixed, for the record: an SS listener with simple-obfs exports a bare ss://
link with no `plugin=` entry, so the obfs configuration is lost. The client YAML
does translate it (plugin: obfs). Fixing the URI means committing to a SIP002
plugin encoding that varies between clients, which is worth deciding separately
rather than guessing at here.
* i18n: complete zh-CN and guard against the locale regressions already seen
zh-CN was missing seven listener hints that en.ts declares, so those tooltips
rendered in English for the primary UI language. Translated: client_auth_cert,
client_auth_type, ech_key, enable_pure_downlink, mux, padding and table_type.
The rest of the catalogue is deliberately left alone. Seventeen locales each lack
65 keys that en.ts has; the provider already resolves those from en.ts, so they
degrade to English rather than breaking, and filling 1100 strings with
unreviewed machine translation would be worse than the fallback. The test prints
the per-locale gap so it stays visible.
The new locale test guards the regressions this directory has actually hit: a
duplicated key inside one object (which breaks the type check), and a key renamed
in en.ts leaving an orphan behind. It also asserts zh-CN stays complete, since a
gap there reads as a defect rather than as translation lag.
* i18n: declare the 24 keys the UI asks for but no locale ever defined
Twenty-four keys were referenced from components without being declared in any
locale, so t() fell through to its hardcoded fallback. Where a fallback existed
that merely meant the string could never be translated; where none existed —
protocolHint, access_sniHint, jlsHint, shadowTlsHint and the rest — the tooltip
either rendered the raw key, "listeners.protocolHint", or disappeared entirely,
because fieldTip() suppresses a value that equals the key it looked up.
en.ts takes the text the code was already falling back to, so nothing changes on
screen; the strings just become translatable. The few without a fallback were
written from the surrounding context — the field they annotate, its placeholder,
and the wording of the neighbouring hints. trafficMultiplierHint is the exception:
its fallback was Chinese, which is wrong for the English catalogue, so it now has
a real English string.
zh-CN gets all twenty-four, which keeps it complete against en.ts.
The locale test now reads through the TypeScript AST. It previously parsed line by
line and these files contain minified regions — several "key":"value" pairs on one
line — so a whole block of keys was invisible to it, which is how this gap stayed
hidden. It also gained the assertion that would have caught the bug directly:
every dotted key handed to t() or fieldTip() must be declared in en.ts.
The remaining languages still lag, now by 89 keys rather than 65, and still fall
back to English; the test prints the per-locale figure.
* fix(uri): drop the vmess allowInsecure field the reference client ignores
The vmess skip-certificate field was added on internal consistency alone —
vless and trojan carry allowInsecure in their query strings, so vmess looked
like an oversight. Checked against the client that actually parses these links,
it is not one.
v2rayN's VmessQRCode model declares exactly: v, ps, add, port, id, aid, scy,
net, type, host, path, tls, sni, alpn, fp. There is no skip-certificate field
at all — neither allowInsecure nor insecure — so the field was dead weight in a
payload that v2rayN reads field by field. v2rayNG reads `insecure` instead, and
Xray-core has removed allowInsecure in favour of pinnedPeerCertSha256, so there
is no spelling that the reference clients agree on. Emitting one would have
produced a link that looks configured but is not.
The ws `host` fix stands: v2rayN's own documentation lists "ws host" as the
meaning of that field, so dropping it broke ws listeners that pin a Host.
The conformance test now pins the emitted payload to v2rayN's field set, so a
future field copied over from another protocol fails instead of being silently
ignored by every client.
Deliberately left alone, both confirmed against the official scheme and worth
deciding separately:
- hysteria2 links carry up/down. The Hysteria 2 URI specification is explicit
that the parameters "should never include client modes ... or bandwidth
values". Removing them changes behaviour for anyone relying on the link, so
it is flagged rather than removed. `insecure` is the official parameter;
allowInsecure is kept alongside it purely for clients that read that name.
- an SS listener with simple-obfs still exports a bare ss:// link with no
plugin entry, losing the obfuscation the client YAML does translate.
Also noted while checking: tuicURIs defaults congestion_control to bbr where
sing-box documents cubic as its default. Ours is set explicitly, which is
valid, but the comment claiming it is what "most TUIC clients" use is not
sourced.
* fix(uri): keep vmess allowInsecure after all
Reverts the revert. Dropping it was correct on the evidence and wrong on the
consequences: v2rayN ignores the field, v2rayNG spells it `insecure`, and
Xray-core has moved to pinnedPeerCertSha256 — but the clients that do read
allowInsecure are precisely the ones whose handshake fails without it, and an
ignored extra key costs the others nothing. Removing it would have traded a
working link for a tidy payload.
The conformance test keeps its guard, with allowInsecure recorded as an
explicit, justified exception rather than a silent inclusion: the field set is
still pinned to v2rayN's model, so any further field copied over from another
protocol has to argue its way in.
* fix(uri): restore the vmess ws Host in registry YAML, and stop dropping share links
Two leftovers from auditing the exporters end to end.
The registry's VMess compiler built ws-opts with the path only, so a listener
that pins a Host exported a client config with nothing to send as Host. The
vless YAML path in the same file already nests it under `headers` per the mihomo
wiki, so this just mirrors that — and the converter has always carried it, which
is how the difference went unnoticed.
The node URI share endpoint also had a tier that could not be reached. It walks
three tiers — m-ui, the registry, then the legacy builders — and the second one
gates the third on having neither a URI nor client YAML. But the registry
implements URI export for a different protocol set than the legacy builders: it
compiles vmess, snell, shadowquic, mieru, sudoku and trusttunnel to client YAML
without ever emitting a URI, so those listeners arrived at the third tier with a
YAML in hand and the condition already false, and reported no link at all even
though ClientURIsWithCredentials exports one. The third tier now runs whenever
the URI is still missing, and only fills in YAML that earlier tiers did not
already provide.
---------
Co-authored-by: 風雪 <kazeyukiro@users.noreply.github.com>
fix(vless): stop exporting flow on transports that cannot carry it (#87)
* fix(vless): stop exporting flow on transports that cannot carry it
The form promised that flow is cleared for ws/grpc/xhttp and the tooltip still
says so, but exported links kept carrying flow=xtls-rprx-vision. Two separate
reasons, both fixed.
The clear came too late to matter. formValuesToConfig wrote `flow` into the
config inside the protocol switch, and only afterwards did the "Vision is
TCP-only" rule run — where it mutated the form state, not the config it had
already been copied into. The transport is now resolved before anything writes,
and the drop also removes a flow inherited from the previous config, so changing
a listener's transport actually takes effect.
The URI generators never asked. Every one of them emitted whatever flow it found:
- node: vlessURIs decided flow before resolving the transport, so the type it
had just computed was irrelevant to the decision. It now resolves the
transport first. ClientURIsWithCredentials also merged the listener-level
flow onto every user on protocol alone, which reintroduced the value the
form had dropped.
- protocol: the share URI and the client YAML both wrote spec.Flow unguarded,
even though TransportSpec knows the network. Same for the server-side
compile, which pushed the listener default onto users — Mihomo rejects that
combination outright, so this one takes the listener down.
- mui: BuildVLESSShare set flow unconditionally, and bridge.go injected the
listener default into users before it was known to be applicable.
- converter: copyTransport resolved `network` but left a stale flow behind.
The rule is now one predicate per representation: TransportSpec.CarriesFlow,
VLESSHandlerSpec.CarriesFlow, and protocol.TransportCarriesFlow for raw configs.
TCP/raw keeps Vision; ws, grpc, xhttp, mkcp and mekya never emit it.
Tests pin each path in both directions — a stale flow disappears on ws/grpc/
xhttp, and a genuine TCP listener keeps its flow. The frontend one runs the real
form builder and fails, in three of five cases, against the previous ordering.
* fix(uri): carry vmess ws Host and align hysteria2 TLS flags across user shapes
Auditing the remaining exporters after the flow fix turned up two more places
where the link disagreed with the listener.
vmessURIs built its JSON by hand instead of going through transportParams, so a
ws listener that pins a Host header exported only the path: the client had
nothing to send as Host and the listener was unreachable through the link, even
though the client YAML built from the same config worked. It also dropped
allowInsecure, which vless and trojan both carry — a listener whose certificate
the client cannot verify exported a link that failed the handshake. The repeated
tlsParams() calls were hoisted while touching this.
hysteria2URIs reads users either as rows or as a map, and the two branches
disagreed: one asked certutil.ShouldSkipCertVerify and emitted only `insecure`,
the other asked clientSkipCert and emitted `insecure` plus `allowInsecure`. The
same listener therefore produced different links depending on how its users were
stored. Both now use clientSkipCert, the helper intended for share URIs, and
emit both keys.
Not fixed, for the record: an SS listener with simple-obfs exports a bare ss://
link with no `plugin=` entry, so the obfs configuration is lost. The client YAML
does translate it (plugin: obfs). Fixing the URI means committing to a SIP002
plugin encoding that varies between clients, which is worth deciding separately
rather than guessing at here.
* i18n: complete zh-CN and guard against the locale regressions already seen
zh-CN was missing seven listener hints that en.ts declares, so those tooltips
rendered in English for the primary UI language. Translated: client_auth_cert,
client_auth_type, ech_key, enable_pure_downlink, mux, padding and table_type.
The rest of the catalogue is deliberately left alone. Seventeen locales each lack
65 keys that en.ts has; the provider already resolves those from en.ts, so they
degrade to English rather than breaking, and filling 1100 strings with
unreviewed machine translation would be worse than the fallback. The test prints
the per-locale gap so it stays visible.
The new locale test guards the regressions this directory has actually hit: a
duplicated key inside one object (which breaks the type check), and a key renamed
in en.ts leaving an orphan behind. It also asserts zh-CN stays complete, since a
gap there reads as a defect rather than as translation lag.
* i18n: declare the 24 keys the UI asks for but no locale ever defined
Twenty-four keys were referenced from components without being declared in any
locale, so t() fell through to its hardcoded fallback. Where a fallback existed
that merely meant the string could never be translated; where none existed —
protocolHint, access_sniHint, jlsHint, shadowTlsHint and the rest — the tooltip
either rendered the raw key, "listeners.protocolHint", or disappeared entirely,
because fieldTip() suppresses a value that equals the key it looked up.
en.ts takes the text the code was already falling back to, so nothing changes on
screen; the strings just become translatable. The few without a fallback were
written from the surrounding context — the field they annotate, its placeholder,
and the wording of the neighbouring hints. trafficMultiplierHint is the exception:
its fallback was Chinese, which is wrong for the English catalogue, so it now has
a real English string.
zh-CN gets all twenty-four, which keeps it complete against en.ts.
The locale test now reads through the TypeScript AST. It previously parsed line by
line and these files contain minified regions — several "key":"value" pairs on one
line — so a whole block of keys was invisible to it, which is how this gap stayed
hidden. It also gained the assertion that would have caught the bug directly:
every dotted key handed to t() or fieldTip() must be declared in en.ts.
The remaining languages still lag, now by 89 keys rather than 65, and still fall
back to English; the test prints the per-locale figure.
* fix(uri): drop the vmess allowInsecure field the reference client ignores
The vmess skip-certificate field was added on internal consistency alone —
vless and trojan carry allowInsecure in their query strings, so vmess looked
like an oversight. Checked against the client that actually parses these links,
it is not one.
v2rayN's VmessQRCode model declares exactly: v, ps, add, port, id, aid, scy,
net, type, host, path, tls, sni, alpn, fp. There is no skip-certificate field
at all — neither allowInsecure nor insecure — so the field was dead weight in a
payload that v2rayN reads field by field. v2rayNG reads `insecure` instead, and
Xray-core has removed allowInsecure in favour of pinnedPeerCertSha256, so there
is no spelling that the reference clients agree on. Emitting one would have
produced a link that looks configured but is not.
The ws `host` fix stands: v2rayN's own documentation lists "ws host" as the
meaning of that field, so dropping it broke ws listeners that pin a Host.
The conformance test now pins the emitted payload to v2rayN's field set, so a
future field copied over from another protocol fails instead of being silently
ignored by every client.
Deliberately left alone, both confirmed against the official scheme and worth
deciding separately:
- hysteria2 links carry up/down. The Hysteria 2 URI specification is explicit
that the parameters "should never include client modes ... or bandwidth
values". Removing them changes behaviour for anyone relying on the link, so
it is flagged rather than removed. `insecure` is the official parameter;
allowInsecure is kept alongside it purely for clients that read that name.
- an SS listener with simple-obfs still exports a bare ss:// link with no
plugin entry, losing the obfuscation the client YAML does translate.
Also noted while checking: tuicURIs defaults congestion_control to bbr where
sing-box documents cubic as its default. Ours is set explicitly, which is
valid, but the comment claiming it is what "most TUIC clients" use is not
sourced.
* fix(uri): keep vmess allowInsecure after all
Reverts the revert. Dropping it was correct on the evidence and wrong on the
consequences: v2rayN ignores the field, v2rayNG spells it `insecure`, and
Xray-core has moved to pinnedPeerCertSha256 — but the clients that do read
allowInsecure are precisely the ones whose handshake fails without it, and an
ignored extra key costs the others nothing. Removing it would have traded a
working link for a tidy payload.
The conformance test keeps its guard, with allowInsecure recorded as an
explicit, justified exception rather than a silent inclusion: the field set is
still pinned to v2rayN's model, so any further field copied over from another
protocol has to argue its way in.
* fix(uri): restore the vmess ws Host in registry YAML, and stop dropping share links
Two leftovers from auditing the exporters end to end.
The registry's VMess compiler built ws-opts with the path only, so a listener
that pins a Host exported a client config with nothing to send as Host. The
vless YAML path in the same file already nests it under `headers` per the mihomo
wiki, so this just mirrors that — and the converter has always carried it, which
is how the difference went unnoticed.
The node URI share endpoint also had a tier that could not be reached. It walks
three tiers — m-ui, the registry, then the legacy builders — and the second one
gates the third on having neither a URI nor client YAML. But the registry
implements URI export for a different protocol set than the legacy builders: it
compiles vmess, snell, shadowquic, mieru, sudoku and trusttunnel to client YAML
without ever emitting a URI, so those listeners arrived at the third tier with a
YAML in hand and the condition already false, and reported no link at all even
though ClientURIsWithCredentials exports one. The third tier now runs whenever
the URI is still missing, and only fills in YAML that earlier tiers did not
already provide.
---------
Co-authored-by: 風雪 <kazeyukiro@users.noreply.github.com>
fix(vless): stop exporting flow on transports that cannot carry it (#87)
* fix(vless): stop exporting flow on transports that cannot carry it
The form promised that flow is cleared for ws/grpc/xhttp and the tooltip still
says so, but exported links kept carrying flow=xtls-rprx-vision. Two separate
reasons, both fixed.
The clear came too late to matter. formValuesToConfig wrote `flow` into the
config inside the protocol switch, and only afterwards did the "Vision is
TCP-only" rule run — where it mutated the form state, not the config it had
already been copied into. The transport is now resolved before anything writes,
and the drop also removes a flow inherited from the previous config, so changing
a listener's transport actually takes effect.
The URI generators never asked. Every one of them emitted whatever flow it found:
- node: vlessURIs decided flow before resolving the transport, so the type it
had just computed was irrelevant to the decision. It now resolves the
transport first. ClientURIsWithCredentials also merged the listener-level
flow onto every user on protocol alone, which reintroduced the value the
form had dropped.
- protocol: the share URI and the client YAML both wrote spec.Flow unguarded,
even though TransportSpec knows the network. Same for the server-side
compile, which pushed the listener default onto users — Mihomo rejects that
combination outright, so this one takes the listener down.
- mui: BuildVLESSShare set flow unconditionally, and bridge.go injected the
listener default into users before it was known to be applicable.
- converter: copyTransport resolved `network` but left a stale flow behind.
The rule is now one predicate per representation: TransportSpec.CarriesFlow,
VLESSHandlerSpec.CarriesFlow, and protocol.TransportCarriesFlow for raw configs.
TCP/raw keeps Vision; ws, grpc, xhttp, mkcp and mekya never emit it.
Tests pin each path in both directions — a stale flow disappears on ws/grpc/
xhttp, and a genuine TCP listener keeps its flow. The frontend one runs the real
form builder and fails, in three of five cases, against the previous ordering.
* fix(uri): carry vmess ws Host and align hysteria2 TLS flags across user shapes
Auditing the remaining exporters after the flow fix turned up two more places
where the link disagreed with the listener.
vmessURIs built its JSON by hand instead of going through transportParams, so a
ws listener that pins a Host header exported only the path: the client had
nothing to send as Host and the listener was unreachable through the link, even
though the client YAML built from the same config worked. It also dropped
allowInsecure, which vless and trojan both carry — a listener whose certificate
the client cannot verify exported a link that failed the handshake. The repeated
tlsParams() calls were hoisted while touching this.
hysteria2URIs reads users either as rows or as a map, and the two branches
disagreed: one asked certutil.ShouldSkipCertVerify and emitted only `insecure`,
the other asked clientSkipCert and emitted `insecure` plus `allowInsecure`. The
same listener therefore produced different links depending on how its users were
stored. Both now use clientSkipCert, the helper intended for share URIs, and
emit both keys.
Not fixed, for the record: an SS listener with simple-obfs exports a bare ss://
link with no `plugin=` entry, so the obfs configuration is lost. The client YAML
does translate it (plugin: obfs). Fixing the URI means committing to a SIP002
plugin encoding that varies between clients, which is worth deciding separately
rather than guessing at here.
* i18n: complete zh-CN and guard against the locale regressions already seen
zh-CN was missing seven listener hints that en.ts declares, so those tooltips
rendered in English for the primary UI language. Translated: client_auth_cert,
client_auth_type, ech_key, enable_pure_downlink, mux, padding and table_type.
The rest of the catalogue is deliberately left alone. Seventeen locales each lack
65 keys that en.ts has; the provider already resolves those from en.ts, so they
degrade to English rather than breaking, and filling 1100 strings with
unreviewed machine translation would be worse than the fallback. The test prints
the per-locale gap so it stays visible.
The new locale test guards the regressions this directory has actually hit: a
duplicated key inside one object (which breaks the type check), and a key renamed
in en.ts leaving an orphan behind. It also asserts zh-CN stays complete, since a
gap there reads as a defect rather than as translation lag.
* i18n: declare the 24 keys the UI asks for but no locale ever defined
Twenty-four keys were referenced from components without being declared in any
locale, so t() fell through to its hardcoded fallback. Where a fallback existed
that merely meant the string could never be translated; where none existed —
protocolHint, access_sniHint, jlsHint, shadowTlsHint and the rest — the tooltip
either rendered the raw key, "listeners.protocolHint", or disappeared entirely,
because fieldTip() suppresses a value that equals the key it looked up.
en.ts takes the text the code was already falling back to, so nothing changes on
screen; the strings just become translatable. The few without a fallback were
written from the surrounding context — the field they annotate, its placeholder,
and the wording of the neighbouring hints. trafficMultiplierHint is the exception:
its fallback was Chinese, which is wrong for the English catalogue, so it now has
a real English string.
zh-CN gets all twenty-four, which keeps it complete against en.ts.
The locale test now reads through the TypeScript AST. It previously parsed line by
line and these files contain minified regions — several "key":"value" pairs on one
line — so a whole block of keys was invisible to it, which is how this gap stayed
hidden. It also gained the assertion that would have caught the bug directly:
every dotted key handed to t() or fieldTip() must be declared in en.ts.
The remaining languages still lag, now by 89 keys rather than 65, and still fall
back to English; the test prints the per-locale figure.
* fix(uri): drop the vmess allowInsecure field the reference client ignores
The vmess skip-certificate field was added on internal consistency alone —
vless and trojan carry allowInsecure in their query strings, so vmess looked
like an oversight. Checked against the client that actually parses these links,
it is not one.
v2rayN's VmessQRCode model declares exactly: v, ps, add, port, id, aid, scy,
net, type, host, path, tls, sni, alpn, fp. There is no skip-certificate field
at all — neither allowInsecure nor insecure — so the field was dead weight in a
payload that v2rayN reads field by field. v2rayNG reads `insecure` instead, and
Xray-core has removed allowInsecure in favour of pinnedPeerCertSha256, so there
is no spelling that the reference clients agree on. Emitting one would have
produced a link that looks configured but is not.
The ws `host` fix stands: v2rayN's own documentation lists "ws host" as the
meaning of that field, so dropping it broke ws listeners that pin a Host.
The conformance test now pins the emitted payload to v2rayN's field set, so a
future field copied over from another protocol fails instead of being silently
ignored by every client.
Deliberately left alone, both confirmed against the official scheme and worth
deciding separately:
- hysteria2 links carry up/down. The Hysteria 2 URI specification is explicit
that the parameters "should never include client modes ... or bandwidth
values". Removing them changes behaviour for anyone relying on the link, so
it is flagged rather than removed. `insecure` is the official parameter;
allowInsecure is kept alongside it purely for clients that read that name.
- an SS listener with simple-obfs still exports a bare ss:// link with no
plugin entry, losing the obfuscation the client YAML does translate.
Also noted while checking: tuicURIs defaults congestion_control to bbr where
sing-box documents cubic as its default. Ours is set explicitly, which is
valid, but the comment claiming it is what "most TUIC clients" use is not
sourced.
* fix(uri): keep vmess allowInsecure after all
Reverts the revert. Dropping it was correct on the evidence and wrong on the
consequences: v2rayN ignores the field, v2rayNG spells it `insecure`, and
Xray-core has moved to pinnedPeerCertSha256 — but the clients that do read
allowInsecure are precisely the ones whose handshake fails without it, and an
ignored extra key costs the others nothing. Removing it would have traded a
working link for a tidy payload.
The conformance test keeps its guard, with allowInsecure recorded as an
explicit, justified exception rather than a silent inclusion: the field set is
still pinned to v2rayN's model, so any further field copied over from another
protocol has to argue its way in.
* fix(uri): restore the vmess ws Host in registry YAML, and stop dropping share links
Two leftovers from auditing the exporters end to end.
The registry's VMess compiler built ws-opts with the path only, so a listener
that pins a Host exported a client config with nothing to send as Host. The
vless YAML path in the same file already nests it under `headers` per the mihomo
wiki, so this just mirrors that — and the converter has always carried it, which
is how the difference went unnoticed.
The node URI share endpoint also had a tier that could not be reached. It walks
three tiers — m-ui, the registry, then the legacy builders — and the second one
gates the third on having neither a URI nor client YAML. But the registry
implements URI export for a different protocol set than the legacy builders: it
compiles vmess, snell, shadowquic, mieru, sudoku and trusttunnel to client YAML
without ever emitting a URI, so those listeners arrived at the third tier with a
YAML in hand and the condition already false, and reported no link at all even
though ClientURIsWithCredentials exports one. The third tier now runs whenever
the URI is still missing, and only fills in YAML that earlier tiers did not
already provide.
---------
Co-authored-by: 風雪 <kazeyukiro@users.noreply.github.com>
fix(vless): stop exporting flow on transports that cannot carry it (#87)
* fix(vless): stop exporting flow on transports that cannot carry it
The form promised that flow is cleared for ws/grpc/xhttp and the tooltip still
says so, but exported links kept carrying flow=xtls-rprx-vision. Two separate
reasons, both fixed.
The clear came too late to matter. formValuesToConfig wrote `flow` into the
config inside the protocol switch, and only afterwards did the "Vision is
TCP-only" rule run — where it mutated the form state, not the config it had
already been copied into. The transport is now resolved before anything writes,
and the drop also removes a flow inherited from the previous config, so changing
a listener's transport actually takes effect.
The URI generators never asked. Every one of them emitted whatever flow it found:
- node: vlessURIs decided flow before resolving the transport, so the type it
had just computed was irrelevant to the decision. It now resolves the
transport first. ClientURIsWithCredentials also merged the listener-level
flow onto every user on protocol alone, which reintroduced the value the
form had dropped.
- protocol: the share URI and the client YAML both wrote spec.Flow unguarded,
even though TransportSpec knows the network. Same for the server-side
compile, which pushed the listener default onto users — Mihomo rejects that
combination outright, so this one takes the listener down.
- mui: BuildVLESSShare set flow unconditionally, and bridge.go injected the
listener default into users before it was known to be applicable.
- converter: copyTransport resolved `network` but left a stale flow behind.
The rule is now one predicate per representation: TransportSpec.CarriesFlow,
VLESSHandlerSpec.CarriesFlow, and protocol.TransportCarriesFlow for raw configs.
TCP/raw keeps Vision; ws, grpc, xhttp, mkcp and mekya never emit it.
Tests pin each path in both directions — a stale flow disappears on ws/grpc/
xhttp, and a genuine TCP listener keeps its flow. The frontend one runs the real
form builder and fails, in three of five cases, against the previous ordering.
* fix(uri): carry vmess ws Host and align hysteria2 TLS flags across user shapes
Auditing the remaining exporters after the flow fix turned up two more places
where the link disagreed with the listener.
vmessURIs built its JSON by hand instead of going through transportParams, so a
ws listener that pins a Host header exported only the path: the client had
nothing to send as Host and the listener was unreachable through the link, even
though the client YAML built from the same config worked. It also dropped
allowInsecure, which vless and trojan both carry — a listener whose certificate
the client cannot verify exported a link that failed the handshake. The repeated
tlsParams() calls were hoisted while touching this.
hysteria2URIs reads users either as rows or as a map, and the two branches
disagreed: one asked certutil.ShouldSkipCertVerify and emitted only `insecure`,
the other asked clientSkipCert and emitted `insecure` plus `allowInsecure`. The
same listener therefore produced different links depending on how its users were
stored. Both now use clientSkipCert, the helper intended for share URIs, and
emit both keys.
Not fixed, for the record: an SS listener with simple-obfs exports a bare ss://
link with no `plugin=` entry, so the obfs configuration is lost. The client YAML
does translate it (plugin: obfs). Fixing the URI means committing to a SIP002
plugin encoding that varies between clients, which is worth deciding separately
rather than guessing at here.
* i18n: complete zh-CN and guard against the locale regressions already seen
zh-CN was missing seven listener hints that en.ts declares, so those tooltips
rendered in English for the primary UI language. Translated: client_auth_cert,
client_auth_type, ech_key, enable_pure_downlink, mux, padding and table_type.
The rest of the catalogue is deliberately left alone. Seventeen locales each lack
65 keys that en.ts has; the provider already resolves those from en.ts, so they
degrade to English rather than breaking, and filling 1100 strings with
unreviewed machine translation would be worse than the fallback. The test prints
the per-locale gap so it stays visible.
The new locale test guards the regressions this directory has actually hit: a
duplicated key inside one object (which breaks the type check), and a key renamed
in en.ts leaving an orphan behind. It also asserts zh-CN stays complete, since a
gap there reads as a defect rather than as translation lag.
* i18n: declare the 24 keys the UI asks for but no locale ever defined
Twenty-four keys were referenced from components without being declared in any
locale, so t() fell through to its hardcoded fallback. Where a fallback existed
that merely meant the string could never be translated; where none existed —
protocolHint, access_sniHint, jlsHint, shadowTlsHint and the rest — the tooltip
either rendered the raw key, "listeners.protocolHint", or disappeared entirely,
because fieldTip() suppresses a value that equals the key it looked up.
en.ts takes the text the code was already falling back to, so nothing changes on
screen; the strings just become translatable. The few without a fallback were
written from the surrounding context — the field they annotate, its placeholder,
and the wording of the neighbouring hints. trafficMultiplierHint is the exception:
its fallback was Chinese, which is wrong for the English catalogue, so it now has
a real English string.
zh-CN gets all twenty-four, which keeps it complete against en.ts.
The locale test now reads through the TypeScript AST. It previously parsed line by
line and these files contain minified regions — several "key":"value" pairs on one
line — so a whole block of keys was invisible to it, which is how this gap stayed
hidden. It also gained the assertion that would have caught the bug directly:
every dotted key handed to t() or fieldTip() must be declared in en.ts.
The remaining languages still lag, now by 89 keys rather than 65, and still fall
back to English; the test prints the per-locale figure.
* fix(uri): drop the vmess allowInsecure field the reference client ignores
The vmess skip-certificate field was added on internal consistency alone —
vless and trojan carry allowInsecure in their query strings, so vmess looked
like an oversight. Checked against the client that actually parses these links,
it is not one.
v2rayN's VmessQRCode model declares exactly: v, ps, add, port, id, aid, scy,
net, type, host, path, tls, sni, alpn, fp. There is no skip-certificate field
at all — neither allowInsecure nor insecure — so the field was dead weight in a
payload that v2rayN reads field by field. v2rayNG reads `insecure` instead, and
Xray-core has removed allowInsecure in favour of pinnedPeerCertSha256, so there
is no spelling that the reference clients agree on. Emitting one would have
produced a link that looks configured but is not.
The ws `host` fix stands: v2rayN's own documentation lists "ws host" as the
meaning of that field, so dropping it broke ws listeners that pin a Host.
The conformance test now pins the emitted payload to v2rayN's field set, so a
future field copied over from another protocol fails instead of being silently
ignored by every client.
Deliberately left alone, both confirmed against the official scheme and worth
deciding separately:
- hysteria2 links carry up/down. The Hysteria 2 URI specification is explicit
that the parameters "should never include client modes ... or bandwidth
values". Removing them changes behaviour for anyone relying on the link, so
it is flagged rather than removed. `insecure` is the official parameter;
allowInsecure is kept alongside it purely for clients that read that name.
- an SS listener with simple-obfs still exports a bare ss:// link with no
plugin entry, losing the obfuscation the client YAML does translate.
Also noted while checking: tuicURIs defaults congestion_control to bbr where
sing-box documents cubic as its default. Ours is set explicitly, which is
valid, but the comment claiming it is what "most TUIC clients" use is not
sourced.
* fix(uri): keep vmess allowInsecure after all
Reverts the revert. Dropping it was correct on the evidence and wrong on the
consequences: v2rayN ignores the field, v2rayNG spells it `insecure`, and
Xray-core has moved to pinnedPeerCertSha256 — but the clients that do read
allowInsecure are precisely the ones whose handshake fails without it, and an
ignored extra key costs the others nothing. Removing it would have traded a
working link for a tidy payload.
The conformance test keeps its guard, with allowInsecure recorded as an
explicit, justified exception rather than a silent inclusion: the field set is
still pinned to v2rayN's model, so any further field copied over from another
protocol has to argue its way in.
* fix(uri): restore the vmess ws Host in registry YAML, and stop dropping share links
Two leftovers from auditing the exporters end to end.
The registry's VMess compiler built ws-opts with the path only, so a listener
that pins a Host exported a client config with nothing to send as Host. The
vless YAML path in the same file already nests it under `headers` per the mihomo
wiki, so this just mirrors that — and the converter has always carried it, which
is how the difference went unnoticed.
The node URI share endpoint also had a tier that could not be reached. It walks
three tiers — m-ui, the registry, then the legacy builders — and the second one
gates the third on having neither a URI nor client YAML. But the registry
implements URI export for a different protocol set than the legacy builders: it
compiles vmess, snell, shadowquic, mieru, sudoku and trusttunnel to client YAML
without ever emitting a URI, so those listeners arrived at the third tier with a
YAML in hand and the condition already false, and reported no link at all even
though ClientURIsWithCredentials exports one. The third tier now runs whenever
the URI is still missing, and only fills in YAML that earlier tiers did not
already provide.
---------
Co-authored-by: 風雪 <kazeyukiro@users.noreply.github.com>
2026-09-28 00:36:07 +08:00
風雪and風雪 232a4432d8 fix(metrics): resolve the real cgroup on LXC for memory and CPU (#85)
* fix(metrics): resolve the real cgroup on LXC, not just the hierarchy root
On LXC the memory cards still disagreed. Path resolution assumed
/sys/fs/cgroup is the whole hierarchy and simply appended the path from
/proc/self/cgroup to it. LXC's default (cgroup:mixed) mounts a tmpfs there and
bind-mounts only the container's own cgroup into a directory below it, so that
path names directories relative to the host hierarchy and does not exist from
inside the container. Resolution then failed, readServiceCgroupMemory() had no
fallback, and the per-process card silently degraded to raw RSS while the
system card read a container-scoped cgroup — two different sources, which is
exactly the inconsistency being reported.
The path is now translated through the mount's root from /proc/self/mountinfo:
if /sys/fs/cgroup exposes only a subtree, the part of the reported path that
the mount hides is dropped. That resolves both the container's own cgroup and
any unit nested inside it, so the two cards land on one accounting domain
instead of "working by accident" on hosts where the mount happens to be the
root.
readServiceCgroupMemory() also gained the root fallback it was missing, so a
layout that cannot be resolved at all still reads the domain the process is
accounted in rather than falling back to RSS. Adopting the root is gated on a
finite cap being enforced there: on a bare-metal host the root is unbounded and
attributing the machine's whole memory to two processes would be a
fabrication, so those hosts keep the previous RSS behaviour.
Both readers now share cgroupMemoryFiles(), so the v2/v1 field probing exists
once instead of being copied per caller — the duplication is how the missing
fallback went unnoticed. Memory path constants are dropped with it, and the
v1 PAGE_COUNTER_MAX sentinel is named rather than open-coded.
Tests cover the layouts directly, with /proc and /sys pointed at fixtures
through named path variables: plain hierarchy root, bind-mounted container
cgroup, a unit nested below it, cgroup v1, the bounded and unbounded fallback,
sentinel rejection, and the invariant that both cards resolve to the same
usage. A malformed mountinfo degrades to "unresolved", never to a wrong path.
* fix(metrics): read CPU accounting from the resolved cgroup
cpu.stat, cpu.max and cpuset.cpus.effective were read from hard-coded
hierarchy-root paths, so they had the same flaw the memory files had: in a
container the accounting for this process is not at the root. On LXC the root
is a tmpfs holding nothing readable, so the CPU card silently fell back to
host-wide /proc/stat and reported the machine instead of the container — the
container could be fully throttled while the bar sat near idle.
They now go through cpuAccountingDir(), which picks the process' own cgroup
when the kernel enforces a cap there (a memory limit or a CPU quota) and
otherwise keeps the hierarchy root. That preserves bare-metal semantics, where
the card is meant to describe the machine rather than the panel's own unit,
while a container — the case where the root is simply the wrong place to look —
is read from the cgroup that actually confines it.
Usage is taken from the first directory that has it, never mixed across two:
a delta between two different cgroups would be meaningless. Caps are probed
across the directories instead, since a quota on a broader domain still applies
to us, and cpuCapacity() already narrows to the tightest limit found.
The three fixed CPU path constants go away with the hard-coded reads; every
accounting file is now read relative to a resolved directory.
* test(system): stop comparing memory samples taken at different times
Two tests guarded the invariant that the system and process cards read the same
memory pool, and both failed intermittently — reproducibly under -race, and
occasionally in a plain run, which the backend job executes:
system memory used (2874142720) is smaller than this process' RSS (2875170816)
panel memory 2874384384 exceeds system used 2874142720
They compared a possibly 500ms-stale cached system reading against a live
process reading of the test binary itself. Where the panel is most of its own
accounting domain — a container, or a sandbox running the test alone — those two
numbers are nearly identical, so the comparison has no margin of its own and any
allocation in between flips it.
The process is now sampled first, so the reference reading is the newer one, and
a tolerance absorbs the remaining skew. What these tests guard is a mismatch of
sources, which was out by multiples (host /proc/meminfo against the cgroup), so
a percent-wide tolerance keeps the assertion meaningful.
---------
Co-authored-by: 風雪 <kazeyukiro@users.noreply.github.com>
fix(metrics): resolve the real cgroup on LXC for memory and CPU (#85)
* fix(metrics): resolve the real cgroup on LXC, not just the hierarchy root
On LXC the memory cards still disagreed. Path resolution assumed
/sys/fs/cgroup is the whole hierarchy and simply appended the path from
/proc/self/cgroup to it. LXC's default (cgroup:mixed) mounts a tmpfs there and
bind-mounts only the container's own cgroup into a directory below it, so that
path names directories relative to the host hierarchy and does not exist from
inside the container. Resolution then failed, readServiceCgroupMemory() had no
fallback, and the per-process card silently degraded to raw RSS while the
system card read a container-scoped cgroup — two different sources, which is
exactly the inconsistency being reported.
The path is now translated through the mount's root from /proc/self/mountinfo:
if /sys/fs/cgroup exposes only a subtree, the part of the reported path that
the mount hides is dropped. That resolves both the container's own cgroup and
any unit nested inside it, so the two cards land on one accounting domain
instead of "working by accident" on hosts where the mount happens to be the
root.
readServiceCgroupMemory() also gained the root fallback it was missing, so a
layout that cannot be resolved at all still reads the domain the process is
accounted in rather than falling back to RSS. Adopting the root is gated on a
finite cap being enforced there: on a bare-metal host the root is unbounded and
attributing the machine's whole memory to two processes would be a
fabrication, so those hosts keep the previous RSS behaviour.
Both readers now share cgroupMemoryFiles(), so the v2/v1 field probing exists
once instead of being copied per caller — the duplication is how the missing
fallback went unnoticed. Memory path constants are dropped with it, and the
v1 PAGE_COUNTER_MAX sentinel is named rather than open-coded.
Tests cover the layouts directly, with /proc and /sys pointed at fixtures
through named path variables: plain hierarchy root, bind-mounted container
cgroup, a unit nested below it, cgroup v1, the bounded and unbounded fallback,
sentinel rejection, and the invariant that both cards resolve to the same
usage. A malformed mountinfo degrades to "unresolved", never to a wrong path.
* fix(metrics): read CPU accounting from the resolved cgroup
cpu.stat, cpu.max and cpuset.cpus.effective were read from hard-coded
hierarchy-root paths, so they had the same flaw the memory files had: in a
container the accounting for this process is not at the root. On LXC the root
is a tmpfs holding nothing readable, so the CPU card silently fell back to
host-wide /proc/stat and reported the machine instead of the container — the
container could be fully throttled while the bar sat near idle.
They now go through cpuAccountingDir(), which picks the process' own cgroup
when the kernel enforces a cap there (a memory limit or a CPU quota) and
otherwise keeps the hierarchy root. That preserves bare-metal semantics, where
the card is meant to describe the machine rather than the panel's own unit,
while a container — the case where the root is simply the wrong place to look —
is read from the cgroup that actually confines it.
Usage is taken from the first directory that has it, never mixed across two:
a delta between two different cgroups would be meaningless. Caps are probed
across the directories instead, since a quota on a broader domain still applies
to us, and cpuCapacity() already narrows to the tightest limit found.
The three fixed CPU path constants go away with the hard-coded reads; every
accounting file is now read relative to a resolved directory.
* test(system): stop comparing memory samples taken at different times
Two tests guarded the invariant that the system and process cards read the same
memory pool, and both failed intermittently — reproducibly under -race, and
occasionally in a plain run, which the backend job executes:
system memory used (2874142720) is smaller than this process' RSS (2875170816)
panel memory 2874384384 exceeds system used 2874142720
They compared a possibly 500ms-stale cached system reading against a live
process reading of the test binary itself. Where the panel is most of its own
accounting domain — a container, or a sandbox running the test alone — those two
numbers are nearly identical, so the comparison has no margin of its own and any
allocation in between flips it.
The process is now sampled first, so the reference reading is the newer one, and
a tolerance absorbs the remaining skew. What these tests guard is a mismatch of
sources, which was out by multiples (host /proc/meminfo against the cgroup), so
a percent-wide tolerance keeps the assertion meaningful.
---------
Co-authored-by: 風雪 <kazeyukiro@users.noreply.github.com>
fix(metrics): resolve the real cgroup on LXC for memory and CPU (#85)
* fix(metrics): resolve the real cgroup on LXC, not just the hierarchy root
On LXC the memory cards still disagreed. Path resolution assumed
/sys/fs/cgroup is the whole hierarchy and simply appended the path from
/proc/self/cgroup to it. LXC's default (cgroup:mixed) mounts a tmpfs there and
bind-mounts only the container's own cgroup into a directory below it, so that
path names directories relative to the host hierarchy and does not exist from
inside the container. Resolution then failed, readServiceCgroupMemory() had no
fallback, and the per-process card silently degraded to raw RSS while the
system card read a container-scoped cgroup — two different sources, which is
exactly the inconsistency being reported.
The path is now translated through the mount's root from /proc/self/mountinfo:
if /sys/fs/cgroup exposes only a subtree, the part of the reported path that
the mount hides is dropped. That resolves both the container's own cgroup and
any unit nested inside it, so the two cards land on one accounting domain
instead of "working by accident" on hosts where the mount happens to be the
root.
readServiceCgroupMemory() also gained the root fallback it was missing, so a
layout that cannot be resolved at all still reads the domain the process is
accounted in rather than falling back to RSS. Adopting the root is gated on a
finite cap being enforced there: on a bare-metal host the root is unbounded and
attributing the machine's whole memory to two processes would be a
fabrication, so those hosts keep the previous RSS behaviour.
Both readers now share cgroupMemoryFiles(), so the v2/v1 field probing exists
once instead of being copied per caller — the duplication is how the missing
fallback went unnoticed. Memory path constants are dropped with it, and the
v1 PAGE_COUNTER_MAX sentinel is named rather than open-coded.
Tests cover the layouts directly, with /proc and /sys pointed at fixtures
through named path variables: plain hierarchy root, bind-mounted container
cgroup, a unit nested below it, cgroup v1, the bounded and unbounded fallback,
sentinel rejection, and the invariant that both cards resolve to the same
usage. A malformed mountinfo degrades to "unresolved", never to a wrong path.
* fix(metrics): read CPU accounting from the resolved cgroup
cpu.stat, cpu.max and cpuset.cpus.effective were read from hard-coded
hierarchy-root paths, so they had the same flaw the memory files had: in a
container the accounting for this process is not at the root. On LXC the root
is a tmpfs holding nothing readable, so the CPU card silently fell back to
host-wide /proc/stat and reported the machine instead of the container — the
container could be fully throttled while the bar sat near idle.
They now go through cpuAccountingDir(), which picks the process' own cgroup
when the kernel enforces a cap there (a memory limit or a CPU quota) and
otherwise keeps the hierarchy root. That preserves bare-metal semantics, where
the card is meant to describe the machine rather than the panel's own unit,
while a container — the case where the root is simply the wrong place to look —
is read from the cgroup that actually confines it.
Usage is taken from the first directory that has it, never mixed across two:
a delta between two different cgroups would be meaningless. Caps are probed
across the directories instead, since a quota on a broader domain still applies
to us, and cpuCapacity() already narrows to the tightest limit found.
The three fixed CPU path constants go away with the hard-coded reads; every
accounting file is now read relative to a resolved directory.
* test(system): stop comparing memory samples taken at different times
Two tests guarded the invariant that the system and process cards read the same
memory pool, and both failed intermittently — reproducibly under -race, and
occasionally in a plain run, which the backend job executes:
system memory used (2874142720) is smaller than this process' RSS (2875170816)
panel memory 2874384384 exceeds system used 2874142720
They compared a possibly 500ms-stale cached system reading against a live
process reading of the test binary itself. Where the panel is most of its own
accounting domain — a container, or a sandbox running the test alone — those two
numbers are nearly identical, so the comparison has no margin of its own and any
allocation in between flips it.
The process is now sampled first, so the reference reading is the newer one, and
a tolerance absorbs the remaining skew. What these tests guard is a mismatch of
sources, which was out by multiples (host /proc/meminfo against the cgroup), so
a percent-wide tolerance keeps the assertion meaningful.
---------
Co-authored-by: 風雪 <kazeyukiro@users.noreply.github.com>
fix(metrics): resolve the real cgroup on LXC for memory and CPU (#85)
* fix(metrics): resolve the real cgroup on LXC, not just the hierarchy root
On LXC the memory cards still disagreed. Path resolution assumed
/sys/fs/cgroup is the whole hierarchy and simply appended the path from
/proc/self/cgroup to it. LXC's default (cgroup:mixed) mounts a tmpfs there and
bind-mounts only the container's own cgroup into a directory below it, so that
path names directories relative to the host hierarchy and does not exist from
inside the container. Resolution then failed, readServiceCgroupMemory() had no
fallback, and the per-process card silently degraded to raw RSS while the
system card read a container-scoped cgroup — two different sources, which is
exactly the inconsistency being reported.
The path is now translated through the mount's root from /proc/self/mountinfo:
if /sys/fs/cgroup exposes only a subtree, the part of the reported path that
the mount hides is dropped. That resolves both the container's own cgroup and
any unit nested inside it, so the two cards land on one accounting domain
instead of "working by accident" on hosts where the mount happens to be the
root.
readServiceCgroupMemory() also gained the root fallback it was missing, so a
layout that cannot be resolved at all still reads the domain the process is
accounted in rather than falling back to RSS. Adopting the root is gated on a
finite cap being enforced there: on a bare-metal host the root is unbounded and
attributing the machine's whole memory to two processes would be a
fabrication, so those hosts keep the previous RSS behaviour.
Both readers now share cgroupMemoryFiles(), so the v2/v1 field probing exists
once instead of being copied per caller — the duplication is how the missing
fallback went unnoticed. Memory path constants are dropped with it, and the
v1 PAGE_COUNTER_MAX sentinel is named rather than open-coded.
Tests cover the layouts directly, with /proc and /sys pointed at fixtures
through named path variables: plain hierarchy root, bind-mounted container
cgroup, a unit nested below it, cgroup v1, the bounded and unbounded fallback,
sentinel rejection, and the invariant that both cards resolve to the same
usage. A malformed mountinfo degrades to "unresolved", never to a wrong path.
* fix(metrics): read CPU accounting from the resolved cgroup
cpu.stat, cpu.max and cpuset.cpus.effective were read from hard-coded
hierarchy-root paths, so they had the same flaw the memory files had: in a
container the accounting for this process is not at the root. On LXC the root
is a tmpfs holding nothing readable, so the CPU card silently fell back to
host-wide /proc/stat and reported the machine instead of the container — the
container could be fully throttled while the bar sat near idle.
They now go through cpuAccountingDir(), which picks the process' own cgroup
when the kernel enforces a cap there (a memory limit or a CPU quota) and
otherwise keeps the hierarchy root. That preserves bare-metal semantics, where
the card is meant to describe the machine rather than the panel's own unit,
while a container — the case where the root is simply the wrong place to look —
is read from the cgroup that actually confines it.
Usage is taken from the first directory that has it, never mixed across two:
a delta between two different cgroups would be meaningless. Caps are probed
across the directories instead, since a quota on a broader domain still applies
to us, and cpuCapacity() already narrows to the tightest limit found.
The three fixed CPU path constants go away with the hard-coded reads; every
accounting file is now read relative to a resolved directory.
* test(system): stop comparing memory samples taken at different times
Two tests guarded the invariant that the system and process cards read the same
memory pool, and both failed intermittently — reproducibly under -race, and
occasionally in a plain run, which the backend job executes:
system memory used (2874142720) is smaller than this process' RSS (2875170816)
panel memory 2874384384 exceeds system used 2874142720
They compared a possibly 500ms-stale cached system reading against a live
process reading of the test binary itself. Where the panel is most of its own
accounting domain — a container, or a sandbox running the test alone — those two
numbers are nearly identical, so the comparison has no margin of its own and any
allocation in between flips it.
The process is now sampled first, so the reference reading is the newer one, and
a tolerance absorbs the remaining skew. What these tests guard is a mismatch of
sources, which was out by multiples (host /proc/meminfo against the cgroup), so
a percent-wide tolerance keeps the assertion meaningful.
---------
Co-authored-by: 風雪 <kazeyukiro@users.noreply.github.com>
fix(metrics): resolve the real cgroup on LXC for memory and CPU (#85)
* fix(metrics): resolve the real cgroup on LXC, not just the hierarchy root
On LXC the memory cards still disagreed. Path resolution assumed
/sys/fs/cgroup is the whole hierarchy and simply appended the path from
/proc/self/cgroup to it. LXC's default (cgroup:mixed) mounts a tmpfs there and
bind-mounts only the container's own cgroup into a directory below it, so that
path names directories relative to the host hierarchy and does not exist from
inside the container. Resolution then failed, readServiceCgroupMemory() had no
fallback, and the per-process card silently degraded to raw RSS while the
system card read a container-scoped cgroup — two different sources, which is
exactly the inconsistency being reported.
The path is now translated through the mount's root from /proc/self/mountinfo:
if /sys/fs/cgroup exposes only a subtree, the part of the reported path that
the mount hides is dropped. That resolves both the container's own cgroup and
any unit nested inside it, so the two cards land on one accounting domain
instead of "working by accident" on hosts where the mount happens to be the
root.
readServiceCgroupMemory() also gained the root fallback it was missing, so a
layout that cannot be resolved at all still reads the domain the process is
accounted in rather than falling back to RSS. Adopting the root is gated on a
finite cap being enforced there: on a bare-metal host the root is unbounded and
attributing the machine's whole memory to two processes would be a
fabrication, so those hosts keep the previous RSS behaviour.
Both readers now share cgroupMemoryFiles(), so the v2/v1 field probing exists
once instead of being copied per caller — the duplication is how the missing
fallback went unnoticed. Memory path constants are dropped with it, and the
v1 PAGE_COUNTER_MAX sentinel is named rather than open-coded.
Tests cover the layouts directly, with /proc and /sys pointed at fixtures
through named path variables: plain hierarchy root, bind-mounted container
cgroup, a unit nested below it, cgroup v1, the bounded and unbounded fallback,
sentinel rejection, and the invariant that both cards resolve to the same
usage. A malformed mountinfo degrades to "unresolved", never to a wrong path.
* fix(metrics): read CPU accounting from the resolved cgroup
cpu.stat, cpu.max and cpuset.cpus.effective were read from hard-coded
hierarchy-root paths, so they had the same flaw the memory files had: in a
container the accounting for this process is not at the root. On LXC the root
is a tmpfs holding nothing readable, so the CPU card silently fell back to
host-wide /proc/stat and reported the machine instead of the container — the
container could be fully throttled while the bar sat near idle.
They now go through cpuAccountingDir(), which picks the process' own cgroup
when the kernel enforces a cap there (a memory limit or a CPU quota) and
otherwise keeps the hierarchy root. That preserves bare-metal semantics, where
the card is meant to describe the machine rather than the panel's own unit,
while a container — the case where the root is simply the wrong place to look —
is read from the cgroup that actually confines it.
Usage is taken from the first directory that has it, never mixed across two:
a delta between two different cgroups would be meaningless. Caps are probed
across the directories instead, since a quota on a broader domain still applies
to us, and cpuCapacity() already narrows to the tightest limit found.
The three fixed CPU path constants go away with the hard-coded reads; every
accounting file is now read relative to a resolved directory.
* test(system): stop comparing memory samples taken at different times
Two tests guarded the invariant that the system and process cards read the same
memory pool, and both failed intermittently — reproducibly under -race, and
occasionally in a plain run, which the backend job executes:
system memory used (2874142720) is smaller than this process' RSS (2875170816)
panel memory 2874384384 exceeds system used 2874142720
They compared a possibly 500ms-stale cached system reading against a live
process reading of the test binary itself. Where the panel is most of its own
accounting domain — a container, or a sandbox running the test alone — those two
numbers are nearly identical, so the comparison has no margin of its own and any
allocation in between flips it.
The process is now sampled first, so the reference reading is the newer one, and
a tolerance absorbs the remaining skew. What these tests guard is a mismatch of
sources, which was out by multiples (host /proc/meminfo against the cgroup), so
a percent-wide tolerance keeps the assertion meaningful.
---------
Co-authored-by: 風雪 <kazeyukiro@users.noreply.github.com>
fix(metrics): resolve the real cgroup on LXC for memory and CPU (#85)
* fix(metrics): resolve the real cgroup on LXC, not just the hierarchy root
On LXC the memory cards still disagreed. Path resolution assumed
/sys/fs/cgroup is the whole hierarchy and simply appended the path from
/proc/self/cgroup to it. LXC's default (cgroup:mixed) mounts a tmpfs there and
bind-mounts only the container's own cgroup into a directory below it, so that
path names directories relative to the host hierarchy and does not exist from
inside the container. Resolution then failed, readServiceCgroupMemory() had no
fallback, and the per-process card silently degraded to raw RSS while the
system card read a container-scoped cgroup — two different sources, which is
exactly the inconsistency being reported.
The path is now translated through the mount's root from /proc/self/mountinfo:
if /sys/fs/cgroup exposes only a subtree, the part of the reported path that
the mount hides is dropped. That resolves both the container's own cgroup and
any unit nested inside it, so the two cards land on one accounting domain
instead of "working by accident" on hosts where the mount happens to be the
root.
readServiceCgroupMemory() also gained the root fallback it was missing, so a
layout that cannot be resolved at all still reads the domain the process is
accounted in rather than falling back to RSS. Adopting the root is gated on a
finite cap being enforced there: on a bare-metal host the root is unbounded and
attributing the machine's whole memory to two processes would be a
fabrication, so those hosts keep the previous RSS behaviour.
Both readers now share cgroupMemoryFiles(), so the v2/v1 field probing exists
once instead of being copied per caller — the duplication is how the missing
fallback went unnoticed. Memory path constants are dropped with it, and the
v1 PAGE_COUNTER_MAX sentinel is named rather than open-coded.
Tests cover the layouts directly, with /proc and /sys pointed at fixtures
through named path variables: plain hierarchy root, bind-mounted container
cgroup, a unit nested below it, cgroup v1, the bounded and unbounded fallback,
sentinel rejection, and the invariant that both cards resolve to the same
usage. A malformed mountinfo degrades to "unresolved", never to a wrong path.
* fix(metrics): read CPU accounting from the resolved cgroup
cpu.stat, cpu.max and cpuset.cpus.effective were read from hard-coded
hierarchy-root paths, so they had the same flaw the memory files had: in a
container the accounting for this process is not at the root. On LXC the root
is a tmpfs holding nothing readable, so the CPU card silently fell back to
host-wide /proc/stat and reported the machine instead of the container — the
container could be fully throttled while the bar sat near idle.
They now go through cpuAccountingDir(), which picks the process' own cgroup
when the kernel enforces a cap there (a memory limit or a CPU quota) and
otherwise keeps the hierarchy root. That preserves bare-metal semantics, where
the card is meant to describe the machine rather than the panel's own unit,
while a container — the case where the root is simply the wrong place to look —
is read from the cgroup that actually confines it.
Usage is taken from the first directory that has it, never mixed across two:
a delta between two different cgroups would be meaningless. Caps are probed
across the directories instead, since a quota on a broader domain still applies
to us, and cpuCapacity() already narrows to the tightest limit found.
The three fixed CPU path constants go away with the hard-coded reads; every
accounting file is now read relative to a resolved directory.
* test(system): stop comparing memory samples taken at different times
Two tests guarded the invariant that the system and process cards read the same
memory pool, and both failed intermittently — reproducibly under -race, and
occasionally in a plain run, which the backend job executes:
system memory used (2874142720) is smaller than this process' RSS (2875170816)
panel memory 2874384384 exceeds system used 2874142720
They compared a possibly 500ms-stale cached system reading against a live
process reading of the test binary itself. Where the panel is most of its own
accounting domain — a container, or a sandbox running the test alone — those two
numbers are nearly identical, so the comparison has no margin of its own and any
allocation in between flips it.
The process is now sampled first, so the reference reading is the newer one, and
a tolerance absorbs the remaining skew. What these tests guard is a mismatch of
sources, which was out by multiples (host /proc/meminfo against the cgroup), so
a percent-wide tolerance keeps the assertion meaningful.
---------
Co-authored-by: 風雪 <kazeyukiro@users.noreply.github.com>
fix(metrics): resolve the real cgroup on LXC for memory and CPU (#85)
* fix(metrics): resolve the real cgroup on LXC, not just the hierarchy root
On LXC the memory cards still disagreed. Path resolution assumed
/sys/fs/cgroup is the whole hierarchy and simply appended the path from
/proc/self/cgroup to it. LXC's default (cgroup:mixed) mounts a tmpfs there and
bind-mounts only the container's own cgroup into a directory below it, so that
path names directories relative to the host hierarchy and does not exist from
inside the container. Resolution then failed, readServiceCgroupMemory() had no
fallback, and the per-process card silently degraded to raw RSS while the
system card read a container-scoped cgroup — two different sources, which is
exactly the inconsistency being reported.
The path is now translated through the mount's root from /proc/self/mountinfo:
if /sys/fs/cgroup exposes only a subtree, the part of the reported path that
the mount hides is dropped. That resolves both the container's own cgroup and
any unit nested inside it, so the two cards land on one accounting domain
instead of "working by accident" on hosts where the mount happens to be the
root.
readServiceCgroupMemory() also gained the root fallback it was missing, so a
layout that cannot be resolved at all still reads the domain the process is
accounted in rather than falling back to RSS. Adopting the root is gated on a
finite cap being enforced there: on a bare-metal host the root is unbounded and
attributing the machine's whole memory to two processes would be a
fabrication, so those hosts keep the previous RSS behaviour.
Both readers now share cgroupMemoryFiles(), so the v2/v1 field probing exists
once instead of being copied per caller — the duplication is how the missing
fallback went unnoticed. Memory path constants are dropped with it, and the
v1 PAGE_COUNTER_MAX sentinel is named rather than open-coded.
Tests cover the layouts directly, with /proc and /sys pointed at fixtures
through named path variables: plain hierarchy root, bind-mounted container
cgroup, a unit nested below it, cgroup v1, the bounded and unbounded fallback,
sentinel rejection, and the invariant that both cards resolve to the same
usage. A malformed mountinfo degrades to "unresolved", never to a wrong path.
* fix(metrics): read CPU accounting from the resolved cgroup
cpu.stat, cpu.max and cpuset.cpus.effective were read from hard-coded
hierarchy-root paths, so they had the same flaw the memory files had: in a
container the accounting for this process is not at the root. On LXC the root
is a tmpfs holding nothing readable, so the CPU card silently fell back to
host-wide /proc/stat and reported the machine instead of the container — the
container could be fully throttled while the bar sat near idle.
They now go through cpuAccountingDir(), which picks the process' own cgroup
when the kernel enforces a cap there (a memory limit or a CPU quota) and
otherwise keeps the hierarchy root. That preserves bare-metal semantics, where
the card is meant to describe the machine rather than the panel's own unit,
while a container — the case where the root is simply the wrong place to look —
is read from the cgroup that actually confines it.
Usage is taken from the first directory that has it, never mixed across two:
a delta between two different cgroups would be meaningless. Caps are probed
across the directories instead, since a quota on a broader domain still applies
to us, and cpuCapacity() already narrows to the tightest limit found.
The three fixed CPU path constants go away with the hard-coded reads; every
accounting file is now read relative to a resolved directory.
* test(system): stop comparing memory samples taken at different times
Two tests guarded the invariant that the system and process cards read the same
memory pool, and both failed intermittently — reproducibly under -race, and
occasionally in a plain run, which the backend job executes:
system memory used (2874142720) is smaller than this process' RSS (2875170816)
panel memory 2874384384 exceeds system used 2874142720
They compared a possibly 500ms-stale cached system reading against a live
process reading of the test binary itself. Where the panel is most of its own
accounting domain — a container, or a sandbox running the test alone — those two
numbers are nearly identical, so the comparison has no margin of its own and any
allocation in between flips it.
The process is now sampled first, so the reference reading is the newer one, and
a tolerance absorbs the remaining skew. What these tests guard is a mismatch of
sources, which was out by multiples (host /proc/meminfo against the cgroup), so
a percent-wide tolerance keeps the assertion meaningful.
---------
Co-authored-by: 風雪 <kazeyukiro@users.noreply.github.com>
fix(metrics): resolve the real cgroup on LXC for memory and CPU (#85)
* fix(metrics): resolve the real cgroup on LXC, not just the hierarchy root
On LXC the memory cards still disagreed. Path resolution assumed
/sys/fs/cgroup is the whole hierarchy and simply appended the path from
/proc/self/cgroup to it. LXC's default (cgroup:mixed) mounts a tmpfs there and
bind-mounts only the container's own cgroup into a directory below it, so that
path names directories relative to the host hierarchy and does not exist from
inside the container. Resolution then failed, readServiceCgroupMemory() had no
fallback, and the per-process card silently degraded to raw RSS while the
system card read a container-scoped cgroup — two different sources, which is
exactly the inconsistency being reported.
The path is now translated through the mount's root from /proc/self/mountinfo:
if /sys/fs/cgroup exposes only a subtree, the part of the reported path that
the mount hides is dropped. That resolves both the container's own cgroup and
any unit nested inside it, so the two cards land on one accounting domain
instead of "working by accident" on hosts where the mount happens to be the
root.
readServiceCgroupMemory() also gained the root fallback it was missing, so a
layout that cannot be resolved at all still reads the domain the process is
accounted in rather than falling back to RSS. Adopting the root is gated on a
finite cap being enforced there: on a bare-metal host the root is unbounded and
attributing the machine's whole memory to two processes would be a
fabrication, so those hosts keep the previous RSS behaviour.
Both readers now share cgroupMemoryFiles(), so the v2/v1 field probing exists
once instead of being copied per caller — the duplication is how the missing
fallback went unnoticed. Memory path constants are dropped with it, and the
v1 PAGE_COUNTER_MAX sentinel is named rather than open-coded.
Tests cover the layouts directly, with /proc and /sys pointed at fixtures
through named path variables: plain hierarchy root, bind-mounted container
cgroup, a unit nested below it, cgroup v1, the bounded and unbounded fallback,
sentinel rejection, and the invariant that both cards resolve to the same
usage. A malformed mountinfo degrades to "unresolved", never to a wrong path.
* fix(metrics): read CPU accounting from the resolved cgroup
cpu.stat, cpu.max and cpuset.cpus.effective were read from hard-coded
hierarchy-root paths, so they had the same flaw the memory files had: in a
container the accounting for this process is not at the root. On LXC the root
is a tmpfs holding nothing readable, so the CPU card silently fell back to
host-wide /proc/stat and reported the machine instead of the container — the
container could be fully throttled while the bar sat near idle.
They now go through cpuAccountingDir(), which picks the process' own cgroup
when the kernel enforces a cap there (a memory limit or a CPU quota) and
otherwise keeps the hierarchy root. That preserves bare-metal semantics, where
the card is meant to describe the machine rather than the panel's own unit,
while a container — the case where the root is simply the wrong place to look —
is read from the cgroup that actually confines it.
Usage is taken from the first directory that has it, never mixed across two:
a delta between two different cgroups would be meaningless. Caps are probed
across the directories instead, since a quota on a broader domain still applies
to us, and cpuCapacity() already narrows to the tightest limit found.
The three fixed CPU path constants go away with the hard-coded reads; every
accounting file is now read relative to a resolved directory.
* test(system): stop comparing memory samples taken at different times
Two tests guarded the invariant that the system and process cards read the same
memory pool, and both failed intermittently — reproducibly under -race, and
occasionally in a plain run, which the backend job executes:
system memory used (2874142720) is smaller than this process' RSS (2875170816)
panel memory 2874384384 exceeds system used 2874142720
They compared a possibly 500ms-stale cached system reading against a live
process reading of the test binary itself. Where the panel is most of its own
accounting domain — a container, or a sandbox running the test alone — those two
numbers are nearly identical, so the comparison has no margin of its own and any
allocation in between flips it.
The process is now sampled first, so the reference reading is the newer one, and
a tolerance absorbs the remaining skew. What these tests guard is a mismatch of
sources, which was out by multiples (host /proc/meminfo against the cgroup), so
a percent-wide tolerance keeps the assertion meaningful.
---------
Co-authored-by: 風雪 <kazeyukiro@users.noreply.github.com>
fix(metrics): resolve the real cgroup on LXC for memory and CPU (#85)
* fix(metrics): resolve the real cgroup on LXC, not just the hierarchy root
On LXC the memory cards still disagreed. Path resolution assumed
/sys/fs/cgroup is the whole hierarchy and simply appended the path from
/proc/self/cgroup to it. LXC's default (cgroup:mixed) mounts a tmpfs there and
bind-mounts only the container's own cgroup into a directory below it, so that
path names directories relative to the host hierarchy and does not exist from
inside the container. Resolution then failed, readServiceCgroupMemory() had no
fallback, and the per-process card silently degraded to raw RSS while the
system card read a container-scoped cgroup — two different sources, which is
exactly the inconsistency being reported.
The path is now translated through the mount's root from /proc/self/mountinfo:
if /sys/fs/cgroup exposes only a subtree, the part of the reported path that
the mount hides is dropped. That resolves both the container's own cgroup and
any unit nested inside it, so the two cards land on one accounting domain
instead of "working by accident" on hosts where the mount happens to be the
root.
readServiceCgroupMemory() also gained the root fallback it was missing, so a
layout that cannot be resolved at all still reads the domain the process is
accounted in rather than falling back to RSS. Adopting the root is gated on a
finite cap being enforced there: on a bare-metal host the root is unbounded and
attributing the machine's whole memory to two processes would be a
fabrication, so those hosts keep the previous RSS behaviour.
Both readers now share cgroupMemoryFiles(), so the v2/v1 field probing exists
once instead of being copied per caller — the duplication is how the missing
fallback went unnoticed. Memory path constants are dropped with it, and the
v1 PAGE_COUNTER_MAX sentinel is named rather than open-coded.
Tests cover the layouts directly, with /proc and /sys pointed at fixtures
through named path variables: plain hierarchy root, bind-mounted container
cgroup, a unit nested below it, cgroup v1, the bounded and unbounded fallback,
sentinel rejection, and the invariant that both cards resolve to the same
usage. A malformed mountinfo degrades to "unresolved", never to a wrong path.
* fix(metrics): read CPU accounting from the resolved cgroup
cpu.stat, cpu.max and cpuset.cpus.effective were read from hard-coded
hierarchy-root paths, so they had the same flaw the memory files had: in a
container the accounting for this process is not at the root. On LXC the root
is a tmpfs holding nothing readable, so the CPU card silently fell back to
host-wide /proc/stat and reported the machine instead of the container — the
container could be fully throttled while the bar sat near idle.
They now go through cpuAccountingDir(), which picks the process' own cgroup
when the kernel enforces a cap there (a memory limit or a CPU quota) and
otherwise keeps the hierarchy root. That preserves bare-metal semantics, where
the card is meant to describe the machine rather than the panel's own unit,
while a container — the case where the root is simply the wrong place to look —
is read from the cgroup that actually confines it.
Usage is taken from the first directory that has it, never mixed across two:
a delta between two different cgroups would be meaningless. Caps are probed
across the directories instead, since a quota on a broader domain still applies
to us, and cpuCapacity() already narrows to the tightest limit found.
The three fixed CPU path constants go away with the hard-coded reads; every
accounting file is now read relative to a resolved directory.
* test(system): stop comparing memory samples taken at different times
Two tests guarded the invariant that the system and process cards read the same
memory pool, and both failed intermittently — reproducibly under -race, and
occasionally in a plain run, which the backend job executes:
system memory used (2874142720) is smaller than this process' RSS (2875170816)
panel memory 2874384384 exceeds system used 2874142720
They compared a possibly 500ms-stale cached system reading against a live
process reading of the test binary itself. Where the panel is most of its own
accounting domain — a container, or a sandbox running the test alone — those two
numbers are nearly identical, so the comparison has no margin of its own and any
allocation in between flips it.
The process is now sampled first, so the reference reading is the newer one, and
a tolerance absorbs the remaining skew. What these tests guard is a mismatch of
sources, which was out by multiples (host /proc/meminfo against the cgroup), so
a percent-wide tolerance keeps the assertion meaningful.
---------
Co-authored-by: 風雪 <kazeyukiro@users.noreply.github.com>
fix(metrics): resolve the real cgroup on LXC for memory and CPU (#85)
* fix(metrics): resolve the real cgroup on LXC, not just the hierarchy root
On LXC the memory cards still disagreed. Path resolution assumed
/sys/fs/cgroup is the whole hierarchy and simply appended the path from
/proc/self/cgroup to it. LXC's default (cgroup:mixed) mounts a tmpfs there and
bind-mounts only the container's own cgroup into a directory below it, so that
path names directories relative to the host hierarchy and does not exist from
inside the container. Resolution then failed, readServiceCgroupMemory() had no
fallback, and the per-process card silently degraded to raw RSS while the
system card read a container-scoped cgroup — two different sources, which is
exactly the inconsistency being reported.
The path is now translated through the mount's root from /proc/self/mountinfo:
if /sys/fs/cgroup exposes only a subtree, the part of the reported path that
the mount hides is dropped. That resolves both the container's own cgroup and
any unit nested inside it, so the two cards land on one accounting domain
instead of "working by accident" on hosts where the mount happens to be the
root.
readServiceCgroupMemory() also gained the root fallback it was missing, so a
layout that cannot be resolved at all still reads the domain the process is
accounted in rather than falling back to RSS. Adopting the root is gated on a
finite cap being enforced there: on a bare-metal host the root is unbounded and
attributing the machine's whole memory to two processes would be a
fabrication, so those hosts keep the previous RSS behaviour.
Both readers now share cgroupMemoryFiles(), so the v2/v1 field probing exists
once instead of being copied per caller — the duplication is how the missing
fallback went unnoticed. Memory path constants are dropped with it, and the
v1 PAGE_COUNTER_MAX sentinel is named rather than open-coded.
Tests cover the layouts directly, with /proc and /sys pointed at fixtures
through named path variables: plain hierarchy root, bind-mounted container
cgroup, a unit nested below it, cgroup v1, the bounded and unbounded fallback,
sentinel rejection, and the invariant that both cards resolve to the same
usage. A malformed mountinfo degrades to "unresolved", never to a wrong path.
* fix(metrics): read CPU accounting from the resolved cgroup
cpu.stat, cpu.max and cpuset.cpus.effective were read from hard-coded
hierarchy-root paths, so they had the same flaw the memory files had: in a
container the accounting for this process is not at the root. On LXC the root
is a tmpfs holding nothing readable, so the CPU card silently fell back to
host-wide /proc/stat and reported the machine instead of the container — the
container could be fully throttled while the bar sat near idle.
They now go through cpuAccountingDir(), which picks the process' own cgroup
when the kernel enforces a cap there (a memory limit or a CPU quota) and
otherwise keeps the hierarchy root. That preserves bare-metal semantics, where
the card is meant to describe the machine rather than the panel's own unit,
while a container — the case where the root is simply the wrong place to look —
is read from the cgroup that actually confines it.
Usage is taken from the first directory that has it, never mixed across two:
a delta between two different cgroups would be meaningless. Caps are probed
across the directories instead, since a quota on a broader domain still applies
to us, and cpuCapacity() already narrows to the tightest limit found.
The three fixed CPU path constants go away with the hard-coded reads; every
accounting file is now read relative to a resolved directory.
* test(system): stop comparing memory samples taken at different times
Two tests guarded the invariant that the system and process cards read the same
memory pool, and both failed intermittently — reproducibly under -race, and
occasionally in a plain run, which the backend job executes:
system memory used (2874142720) is smaller than this process' RSS (2875170816)
panel memory 2874384384 exceeds system used 2874142720
They compared a possibly 500ms-stale cached system reading against a live
process reading of the test binary itself. Where the panel is most of its own
accounting domain — a container, or a sandbox running the test alone — those two
numbers are nearly identical, so the comparison has no margin of its own and any
allocation in between flips it.
The process is now sampled first, so the reference reading is the newer one, and
a tolerance absorbs the remaining skew. What these tests guard is a mismatch of
sources, which was out by multiples (host /proc/meminfo against the cgroup), so
a percent-wide tolerance keeps the assertion meaningful.
---------
Co-authored-by: 風雪 <kazeyukiro@users.noreply.github.com>
fix(metrics): resolve the real cgroup on LXC for memory and CPU (#85)
* fix(metrics): resolve the real cgroup on LXC, not just the hierarchy root
On LXC the memory cards still disagreed. Path resolution assumed
/sys/fs/cgroup is the whole hierarchy and simply appended the path from
/proc/self/cgroup to it. LXC's default (cgroup:mixed) mounts a tmpfs there and
bind-mounts only the container's own cgroup into a directory below it, so that
path names directories relative to the host hierarchy and does not exist from
inside the container. Resolution then failed, readServiceCgroupMemory() had no
fallback, and the per-process card silently degraded to raw RSS while the
system card read a container-scoped cgroup — two different sources, which is
exactly the inconsistency being reported.
The path is now translated through the mount's root from /proc/self/mountinfo:
if /sys/fs/cgroup exposes only a subtree, the part of the reported path that
the mount hides is dropped. That resolves both the container's own cgroup and
any unit nested inside it, so the two cards land on one accounting domain
instead of "working by accident" on hosts where the mount happens to be the
root.
readServiceCgroupMemory() also gained the root fallback it was missing, so a
layout that cannot be resolved at all still reads the domain the process is
accounted in rather than falling back to RSS. Adopting the root is gated on a
finite cap being enforced there: on a bare-metal host the root is unbounded and
attributing the machine's whole memory to two processes would be a
fabrication, so those hosts keep the previous RSS behaviour.
Both readers now share cgroupMemoryFiles(), so the v2/v1 field probing exists
once instead of being copied per caller — the duplication is how the missing
fallback went unnoticed. Memory path constants are dropped with it, and the
v1 PAGE_COUNTER_MAX sentinel is named rather than open-coded.
Tests cover the layouts directly, with /proc and /sys pointed at fixtures
through named path variables: plain hierarchy root, bind-mounted container
cgroup, a unit nested below it, cgroup v1, the bounded and unbounded fallback,
sentinel rejection, and the invariant that both cards resolve to the same
usage. A malformed mountinfo degrades to "unresolved", never to a wrong path.
* fix(metrics): read CPU accounting from the resolved cgroup
cpu.stat, cpu.max and cpuset.cpus.effective were read from hard-coded
hierarchy-root paths, so they had the same flaw the memory files had: in a
container the accounting for this process is not at the root. On LXC the root
is a tmpfs holding nothing readable, so the CPU card silently fell back to
host-wide /proc/stat and reported the machine instead of the container — the
container could be fully throttled while the bar sat near idle.
They now go through cpuAccountingDir(), which picks the process' own cgroup
when the kernel enforces a cap there (a memory limit or a CPU quota) and
otherwise keeps the hierarchy root. That preserves bare-metal semantics, where
the card is meant to describe the machine rather than the panel's own unit,
while a container — the case where the root is simply the wrong place to look —
is read from the cgroup that actually confines it.
Usage is taken from the first directory that has it, never mixed across two:
a delta between two different cgroups would be meaningless. Caps are probed
across the directories instead, since a quota on a broader domain still applies
to us, and cpuCapacity() already narrows to the tightest limit found.
The three fixed CPU path constants go away with the hard-coded reads; every
accounting file is now read relative to a resolved directory.
* test(system): stop comparing memory samples taken at different times
Two tests guarded the invariant that the system and process cards read the same
memory pool, and both failed intermittently — reproducibly under -race, and
occasionally in a plain run, which the backend job executes:
system memory used (2874142720) is smaller than this process' RSS (2875170816)
panel memory 2874384384 exceeds system used 2874142720
They compared a possibly 500ms-stale cached system reading against a live
process reading of the test binary itself. Where the panel is most of its own
accounting domain — a container, or a sandbox running the test alone — those two
numbers are nearly identical, so the comparison has no margin of its own and any
allocation in between flips it.
The process is now sampled first, so the reference reading is the newer one, and
a tolerance absorbs the remaining skew. What these tests guard is a mismatch of
sources, which was out by multiples (host /proc/meminfo against the cgroup), so
a percent-wide tolerance keeps the assertion meaningful.
---------
Co-authored-by: 風雪 <kazeyukiro@users.noreply.github.com>
fix(metrics): resolve the real cgroup on LXC for memory and CPU (#85)
* fix(metrics): resolve the real cgroup on LXC, not just the hierarchy root
On LXC the memory cards still disagreed. Path resolution assumed
/sys/fs/cgroup is the whole hierarchy and simply appended the path from
/proc/self/cgroup to it. LXC's default (cgroup:mixed) mounts a tmpfs there and
bind-mounts only the container's own cgroup into a directory below it, so that
path names directories relative to the host hierarchy and does not exist from
inside the container. Resolution then failed, readServiceCgroupMemory() had no
fallback, and the per-process card silently degraded to raw RSS while the
system card read a container-scoped cgroup — two different sources, which is
exactly the inconsistency being reported.
The path is now translated through the mount's root from /proc/self/mountinfo:
if /sys/fs/cgroup exposes only a subtree, the part of the reported path that
the mount hides is dropped. That resolves both the container's own cgroup and
any unit nested inside it, so the two cards land on one accounting domain
instead of "working by accident" on hosts where the mount happens to be the
root.
readServiceCgroupMemory() also gained the root fallback it was missing, so a
layout that cannot be resolved at all still reads the domain the process is
accounted in rather than falling back to RSS. Adopting the root is gated on a
finite cap being enforced there: on a bare-metal host the root is unbounded and
attributing the machine's whole memory to two processes would be a
fabrication, so those hosts keep the previous RSS behaviour.
Both readers now share cgroupMemoryFiles(), so the v2/v1 field probing exists
once instead of being copied per caller — the duplication is how the missing
fallback went unnoticed. Memory path constants are dropped with it, and the
v1 PAGE_COUNTER_MAX sentinel is named rather than open-coded.
Tests cover the layouts directly, with /proc and /sys pointed at fixtures
through named path variables: plain hierarchy root, bind-mounted container
cgroup, a unit nested below it, cgroup v1, the bounded and unbounded fallback,
sentinel rejection, and the invariant that both cards resolve to the same
usage. A malformed mountinfo degrades to "unresolved", never to a wrong path.
* fix(metrics): read CPU accounting from the resolved cgroup
cpu.stat, cpu.max and cpuset.cpus.effective were read from hard-coded
hierarchy-root paths, so they had the same flaw the memory files had: in a
container the accounting for this process is not at the root. On LXC the root
is a tmpfs holding nothing readable, so the CPU card silently fell back to
host-wide /proc/stat and reported the machine instead of the container — the
container could be fully throttled while the bar sat near idle.
They now go through cpuAccountingDir(), which picks the process' own cgroup
when the kernel enforces a cap there (a memory limit or a CPU quota) and
otherwise keeps the hierarchy root. That preserves bare-metal semantics, where
the card is meant to describe the machine rather than the panel's own unit,
while a container — the case where the root is simply the wrong place to look —
is read from the cgroup that actually confines it.
Usage is taken from the first directory that has it, never mixed across two:
a delta between two different cgroups would be meaningless. Caps are probed
across the directories instead, since a quota on a broader domain still applies
to us, and cpuCapacity() already narrows to the tightest limit found.
The three fixed CPU path constants go away with the hard-coded reads; every
accounting file is now read relative to a resolved directory.
* test(system): stop comparing memory samples taken at different times
Two tests guarded the invariant that the system and process cards read the same
memory pool, and both failed intermittently — reproducibly under -race, and
occasionally in a plain run, which the backend job executes:
system memory used (2874142720) is smaller than this process' RSS (2875170816)
panel memory 2874384384 exceeds system used 2874142720
They compared a possibly 500ms-stale cached system reading against a live
process reading of the test binary itself. Where the panel is most of its own
accounting domain — a container, or a sandbox running the test alone — those two
numbers are nearly identical, so the comparison has no margin of its own and any
allocation in between flips it.
The process is now sampled first, so the reference reading is the newer one, and
a tolerance absorbs the remaining skew. What these tests guard is a mismatch of
sources, which was out by multiples (host /proc/meminfo against the cgroup), so
a percent-wide tolerance keeps the assertion meaningful.
---------
Co-authored-by: 風雪 <kazeyukiro@users.noreply.github.com>
2026-09-27 15:58:47 +08:00
kazeyukiro 19ace66aef fix(i18n): add listeners.quickCreateHint for all locales 2026-09-27 04:17:02 +00:00
kazeyukiro eb78dec2d8 ci(release): restore Changes-since notes like v1.3.7
Build formal release body from conventional commits between the
previous SemVer tag and this release, then append install notes.
ci(release): restore Changes-since notes like v1.3.7
Build formal release body from conventional commits between the
previous SemVer tag and this release, then append install notes.
ci(release): restore Changes-since notes like v1.3.7
Build formal release body from conventional commits between the
previous SemVer tag and this release, then append install notes.
ci(release): restore Changes-since notes like v1.3.7
Build formal release body from conventional commits between the
previous SemVer tag and this release, then append install notes.
ci(release): restore Changes-since notes like v1.3.7
Build formal release body from conventional commits between the
previous SemVer tag and this release, then append install notes.
ci(release): restore Changes-since notes like v1.3.7
Build formal release body from conventional commits between the
previous SemVer tag and this release, then append install notes.
ci(release): restore Changes-since notes like v1.3.7
Build formal release body from conventional commits between the
previous SemVer tag and this release, then append install notes.
ci(release): restore Changes-since notes like v1.3.7
Build formal release body from conventional commits between the
previous SemVer tag and this release, then append install notes.
ci(release): restore Changes-since notes like v1.3.7
Build formal release body from conventional commits between the
previous SemVer tag and this release, then append install notes.
ci(release): restore Changes-since notes like v1.3.7
Build formal release body from conventional commits between the
previous SemVer tag and this release, then append install notes.
ci(release): restore Changes-since notes like v1.3.7
Build formal release body from conventional commits between the
previous SemVer tag and this release, then append install notes.
ci(release): restore Changes-since notes like v1.3.7
Build formal release body from conventional commits between the
previous SemVer tag and this release, then append install notes.
2026-09-27 03:46:22 +00:00
kazeyukiro aa1b51b28c fix(ui): drop useless generic wiki tooltip stubs
Remove fallback *Hint strings that only said "see wiki". Tooltips now
use fieldTip() and render only when real copy exists.
fix(ui): drop useless generic wiki tooltip stubs
Remove fallback *Hint strings that only said "see wiki". Tooltips now
use fieldTip() and render only when real copy exists.
fix(ui): drop useless generic wiki tooltip stubs
Remove fallback *Hint strings that only said "see wiki". Tooltips now
use fieldTip() and render only when real copy exists.
fix(ui): drop useless generic wiki tooltip stubs
Remove fallback *Hint strings that only said "see wiki". Tooltips now
use fieldTip() and render only when real copy exists.
fix(ui): drop useless generic wiki tooltip stubs
Remove fallback *Hint strings that only said "see wiki". Tooltips now
use fieldTip() and render only when real copy exists.
fix(ui): drop useless generic wiki tooltip stubs
Remove fallback *Hint strings that only said "see wiki". Tooltips now
use fieldTip() and render only when real copy exists.
fix(ui): drop useless generic wiki tooltip stubs
Remove fallback *Hint strings that only said "see wiki". Tooltips now
use fieldTip() and render only when real copy exists.
fix(ui): drop useless generic wiki tooltip stubs
Remove fallback *Hint strings that only said "see wiki". Tooltips now
use fieldTip() and render only when real copy exists.
fix(ui): drop useless generic wiki tooltip stubs
Remove fallback *Hint strings that only said "see wiki". Tooltips now
use fieldTip() and render only when real copy exists.
fix(ui): drop useless generic wiki tooltip stubs
Remove fallback *Hint strings that only said "see wiki". Tooltips now
use fieldTip() and render only when real copy exists.
fix(ui): drop useless generic wiki tooltip stubs
Remove fallback *Hint strings that only said "see wiki". Tooltips now
use fieldTip() and render only when real copy exists.
fix(ui): drop useless generic wiki tooltip stubs
Remove fallback *Hint strings that only said "see wiki". Tooltips now
use fieldTip() and render only when real copy exists.
2026-09-27 03:36:52 +00:00
kazeyukiro 8c5b0692ad fix(ui): pass t into capability renderFields (TS2304) 2026-09-27 03:08:34 +00:00
kazeyukiro 1622893434 fix(i18n): restore missing commas after locale key dedupe 2026-09-27 03:02:50 +00:00
kazeyukiro 2349bd0cb8 fix(i18n): remove duplicate locale object keys breaking tsc 2026-09-27 02:59:09 +00:00