* 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>
384 lines
12 KiB
Go
384 lines
12 KiB
Go
package node
|
|
|
|
import (
|
|
"crypto/rand"
|
|
"encoding/hex"
|
|
"log"
|
|
"net"
|
|
"net/http"
|
|
"net/url"
|
|
"strconv"
|
|
"strings"
|
|
|
|
"github.com/gin-gonic/gin"
|
|
"github.com/kazeyukiro/3m-ui/backend/internal/config"
|
|
"github.com/kazeyukiro/3m-ui/backend/internal/converter"
|
|
"github.com/kazeyukiro/3m-ui/backend/internal/database/models"
|
|
"github.com/kazeyukiro/3m-ui/backend/internal/mui"
|
|
"github.com/kazeyukiro/3m-ui/backend/internal/netutil"
|
|
"github.com/kazeyukiro/3m-ui/backend/internal/protocol"
|
|
"github.com/kazeyukiro/3m-ui/backend/internal/user"
|
|
"gorm.io/gorm"
|
|
)
|
|
|
|
// Handler serves node/listener HTTP endpoints with injected dependencies.
|
|
type Handler struct {
|
|
svc *Service
|
|
user *user.Service
|
|
db *gorm.DB
|
|
}
|
|
|
|
// NewHandler constructs a node HTTP handler.
|
|
func NewHandler(svc *Service, userSvc *user.Service, db *gorm.DB) *Handler {
|
|
return &Handler{svc: svc, user: userSvc, db: db}
|
|
}
|
|
|
|
// RegisterRoutes registers node routes on the provided group.
|
|
func (h *Handler) RegisterRoutes(rg *gin.RouterGroup) {
|
|
rg.POST("/:id/connection-check", h.CheckConnection)
|
|
rg.GET("", h.ListNodes)
|
|
rg.POST("", h.CreateNode)
|
|
rg.GET("/:id", h.GetNode)
|
|
rg.GET("/:id/uri", h.ExportNodeURI)
|
|
rg.PUT("/:id", h.UpdateNode)
|
|
rg.DELETE("/:id", h.DeleteNode)
|
|
rg.POST("/:id/reload", h.ReloadNode)
|
|
rg.POST("/:id/client-access", h.CreateClientAccess)
|
|
}
|
|
|
|
// RegisterClientRoutes registers only routes that are unique to the node
|
|
// handler. The listener handler already owns the shared CRUD/reload routes.
|
|
func (h *Handler) RegisterClientRoutes(rg *gin.RouterGroup) {
|
|
rg.POST("/:id/connection-check", h.CheckConnection)
|
|
rg.GET("/:id/uri", h.ExportNodeURI)
|
|
rg.POST("/:id/client-access", h.CreateClientAccess)
|
|
}
|
|
|
|
func (h *Handler) ListNodes(c *gin.Context) {
|
|
list, err := h.svc.GetAll()
|
|
if err != nil {
|
|
log.Printf("node list failed: %v", err)
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": "internal server error"})
|
|
return
|
|
}
|
|
c.JSON(http.StatusOK, list)
|
|
}
|
|
|
|
func (h *Handler) CreateNode(c *gin.Context) {
|
|
var l models.Listener
|
|
if err := c.ShouldBindJSON(&l); err != nil {
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
|
return
|
|
}
|
|
if err := h.svc.Create(&l); err != nil {
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
|
return
|
|
}
|
|
c.JSON(http.StatusCreated, l)
|
|
}
|
|
|
|
func (h *Handler) GetNode(c *gin.Context) {
|
|
id, err := strconv.ParseUint(c.Param("id"), 10, 32)
|
|
if err != nil {
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid node id"})
|
|
return
|
|
}
|
|
l, err := h.svc.GetByID(uint(id))
|
|
if err != nil {
|
|
c.JSON(http.StatusNotFound, gin.H{"error": "listener not found"})
|
|
return
|
|
}
|
|
c.JSON(http.StatusOK, l)
|
|
}
|
|
|
|
func (h *Handler) isTrustedHost(host string) bool {
|
|
host = strings.TrimSpace(host)
|
|
if host == "" {
|
|
return false
|
|
}
|
|
if h, _, err := net.SplitHostPort(host); err == nil {
|
|
host = h
|
|
}
|
|
host = strings.Trim(host, "[]")
|
|
if ip := net.ParseIP(host); ip != nil {
|
|
return ip.IsLoopback() || ip.IsPrivate() || ip.IsLinkLocalUnicast()
|
|
}
|
|
return strings.EqualFold(host, "localhost")
|
|
}
|
|
|
|
// ExportNodeURI returns share links (m-ui style: uri / uris / qr / client_yaml).
|
|
func (h *Handler) ExportNodeURI(c *gin.Context) {
|
|
id, err := strconv.ParseUint(c.Param("id"), 10, 32)
|
|
if err != nil {
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid node id"})
|
|
return
|
|
}
|
|
listener, err := h.svc.GetByID(uint(id))
|
|
if err != nil {
|
|
c.JSON(http.StatusNotFound, gin.H{"error": "listener not found"})
|
|
return
|
|
}
|
|
if !listener.Enabled {
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": "listener is disabled"})
|
|
return
|
|
}
|
|
|
|
publicURL := ""
|
|
if config.GlobalConfig != nil {
|
|
publicURL = strings.TrimSpace(config.GlobalConfig.Server.PublicURL)
|
|
}
|
|
ap := protocol.LoadAccessProfile(h.db)
|
|
if ap.PublicHost != "" {
|
|
publicURL = ap.PublicHost
|
|
}
|
|
// Apply global access-profile fields as fallbacks when the per-listener
|
|
// values are empty. Per-listener values always take precedence so admins
|
|
// can still override a panel-wide default for an individual node.
|
|
if ap.PublicPort != "" && strings.TrimSpace(listener.PublicPort) == "" {
|
|
listener.PublicPort = ap.PublicPort
|
|
}
|
|
if ap.SNI != "" && strings.TrimSpace(listener.AccessSNI) == "" {
|
|
listener.AccessSNI = ap.SNI
|
|
}
|
|
if ap.ClientFingerprint != "" && strings.TrimSpace(listener.ClientFingerprint) == "" {
|
|
listener.ClientFingerprint = ap.ClientFingerprint
|
|
}
|
|
if len(ap.ALPN) > 0 && strings.TrimSpace(listener.AccessALPN) == "" {
|
|
listener.AccessALPN = strings.Join(ap.ALPN, ",")
|
|
}
|
|
if host := strings.TrimSpace(listener.PublicHost); host != "" {
|
|
publicURL = host
|
|
}
|
|
host := c.GetHeader("X-Forwarded-Host")
|
|
if host != "" && !h.isTrustedHost(host) {
|
|
host = ""
|
|
}
|
|
if host == "" {
|
|
host = c.Request.Host
|
|
}
|
|
host = netutil.NormalizeHost(host)
|
|
host = normalizeExportHostPrefer(host, listener.BindAddress, listener.Listen, publicURL)
|
|
host = netutil.NormalizeHost(host)
|
|
// If publicURL was explicitly resolved (config / access profile / listener
|
|
// PublicHost), it is authoritative and must override any client-controlled
|
|
// Host header (X-Forwarded-Host / Request.Host) used above. This prevents
|
|
// share links from advertising an attacker-supplied host.
|
|
if publicURL != "" {
|
|
if u, perr := url.Parse(publicURL); perr == nil && u.Host != "" {
|
|
host = netutil.NormalizeHost(u.Host)
|
|
}
|
|
}
|
|
|
|
credentials := []user.Credential{}
|
|
if h.user != nil {
|
|
byListener, err := h.user.ActiveCredentialsByListener()
|
|
if err != nil {
|
|
log.Printf("export-node-uri load credentials failed: %v", err)
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": "internal server error"})
|
|
return
|
|
}
|
|
credentials = byListener[listener.ID]
|
|
}
|
|
|
|
// Three-tier share export: m-ui protocol port -> 3m-ui registry -> legacy URIs.
|
|
//
|
|
// Follow-up (P3-9 / X-MUI-2): the three tiers produce inconsistent output
|
|
// shapes (m-ui emits URIs + client YAML from the protocol port; the
|
|
// 3m-ui registry emits its own URI format; the legacy builder emits
|
|
// plain URIs only). When the first tier succeeds the client gets the
|
|
// m-ui shape, otherwise it silently degrades to a different shape for
|
|
// the same listener. Unifying the output contract across tiers is
|
|
// tracked as a separate design task.
|
|
var uris []string
|
|
var clientYAML, primary string
|
|
muiCreds := make([]mui.Cred, 0, len(credentials))
|
|
for _, c := range credentials {
|
|
muiCreds = append(muiCreds, mui.Cred{Username: c.Username, Password: c.Password, UUID: c.UUID})
|
|
}
|
|
creds := make([]protocol.UserCred, 0, len(credentials))
|
|
for _, c := range credentials {
|
|
creds = append(creds, protocol.UserCred{Username: c.Username, Password: c.Password, UUID: c.UUID})
|
|
}
|
|
if shares, err := mui.BuildShares(*listener, host, muiCreds); err == nil && len(shares) > 0 {
|
|
uris = make([]string, 0, len(shares))
|
|
for _, s := range shares {
|
|
if s.URI != "" {
|
|
uris = append(uris, s.URI)
|
|
}
|
|
if clientYAML == "" && len(s.ClientYAML) > 0 {
|
|
clientYAML = string(s.ClientYAML)
|
|
}
|
|
}
|
|
if len(uris) > 0 {
|
|
primary = uris[0]
|
|
}
|
|
}
|
|
// Fall through tiers when m-ui produced neither a URI nor client YAML.
|
|
if primary == "" && clientYAML == "" {
|
|
if shares, err := protocol.ExportShares(*listener, host, creds); err == nil && len(shares) > 0 {
|
|
uris = make([]string, 0, len(shares))
|
|
for _, s := range shares {
|
|
if s.URI != "" {
|
|
uris = append(uris, s.URI)
|
|
}
|
|
if clientYAML == "" && s.ClientYAML != "" {
|
|
clientYAML = s.ClientYAML
|
|
}
|
|
}
|
|
if len(uris) > 0 {
|
|
primary = uris[0]
|
|
}
|
|
}
|
|
}
|
|
// Tier 2 covers a different protocol set than the legacy builders: vmess,
|
|
// snell, shadowquic, mieru, sudoku and trusttunnel compile to client YAML
|
|
// but never produce a share URI there. Gating this tier on the YAML too
|
|
// would leave those listeners reporting no link at all, even though
|
|
// ClientURIsWithCredentials exports one — so it runs whenever the URI is
|
|
// still missing, and only replaces the YAML if none arrived.
|
|
if primary == "" {
|
|
legacy, lerr := ClientURIsWithCredentials(*listener, host, credentials)
|
|
if lerr != nil && clientYAML == "" {
|
|
c.JSON(http.StatusUnprocessableEntity, gin.H{"error": lerr.Error()})
|
|
return
|
|
}
|
|
if lerr == nil {
|
|
uris = legacy
|
|
if uris == nil {
|
|
uris = []string{}
|
|
}
|
|
if len(uris) > 0 {
|
|
primary = uris[0]
|
|
}
|
|
}
|
|
if clientYAML == "" {
|
|
if y, err := converter.ExportClientYAML(*listener, host, credentials); err == nil {
|
|
clientYAML = y
|
|
}
|
|
}
|
|
}
|
|
if clientYAML == "" {
|
|
if y, err := converter.ExportClientYAML(*listener, host, credentials); err == nil {
|
|
clientYAML = y
|
|
}
|
|
}
|
|
c.JSON(http.StatusOK, gin.H{
|
|
"name": listener.Name,
|
|
"protocol": listener.Protocol,
|
|
"uri": primary,
|
|
"uris": uris,
|
|
"qr_content": primary,
|
|
"client_yaml": clientYAML,
|
|
"hint": emptyURIHint(len(uris), len(credentials)),
|
|
"typed": true,
|
|
})
|
|
}
|
|
|
|
func emptyURIHint(uriCount, credCount int) string {
|
|
if uriCount > 0 {
|
|
return ""
|
|
}
|
|
if credCount == 0 {
|
|
return "no active users bound to this listener; bind users in User Management"
|
|
}
|
|
return "could not build share links for this protocol/config"
|
|
}
|
|
|
|
func (h *Handler) UpdateNode(c *gin.Context) {
|
|
id, err := strconv.ParseUint(c.Param("id"), 10, 32)
|
|
if err != nil {
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid node id"})
|
|
return
|
|
}
|
|
var l models.Listener
|
|
if err := c.ShouldBindJSON(&l); err != nil {
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
|
return
|
|
}
|
|
l.ID = uint(id)
|
|
if err := h.svc.Update(&l); err != nil {
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
|
return
|
|
}
|
|
c.JSON(http.StatusOK, l)
|
|
}
|
|
|
|
func (h *Handler) DeleteNode(c *gin.Context) {
|
|
id, err := strconv.ParseUint(c.Param("id"), 10, 32)
|
|
if err != nil {
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid node id"})
|
|
return
|
|
}
|
|
if err := h.svc.Delete(uint(id)); err != nil {
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
|
return
|
|
}
|
|
c.Status(http.StatusNoContent)
|
|
}
|
|
|
|
func (h *Handler) ReloadNode(c *gin.Context) {
|
|
id, err := strconv.ParseUint(c.Param("id"), 10, 32)
|
|
if err != nil {
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid node id"})
|
|
return
|
|
}
|
|
if err := h.svc.TriggerReload(uint(id)); err != nil {
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
|
return
|
|
}
|
|
c.JSON(http.StatusOK, gin.H{"ok": true})
|
|
}
|
|
|
|
func (h *Handler) CreateClientAccess(c *gin.Context) {
|
|
id, err := strconv.ParseUint(c.Param("id"), 10, 32)
|
|
if err != nil {
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid node id"})
|
|
return
|
|
}
|
|
listener, err := h.svc.GetByID(uint(id))
|
|
if err != nil {
|
|
c.JSON(http.StatusNotFound, gin.H{"error": "listener not found"})
|
|
return
|
|
}
|
|
var body struct {
|
|
Name string `json:"name"`
|
|
}
|
|
_ = c.ShouldBindJSON(&body)
|
|
name := strings.TrimSpace(body.Name)
|
|
if name == "" {
|
|
name = listener.Name + "-access"
|
|
}
|
|
buf := make([]byte, 24)
|
|
if _, err := rand.Read(buf); err != nil {
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to generate token"})
|
|
return
|
|
}
|
|
db := h.db
|
|
if db == nil {
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": "database unavailable"})
|
|
return
|
|
}
|
|
token := models.AccessToken{Name: name, Token: hex.EncodeToString(buf), Enabled: true, ListenerID: listener.ID}
|
|
if err := db.Create(&token).Error; err != nil {
|
|
log.Printf("node client-access create failed: %v", err)
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": "internal server error"})
|
|
return
|
|
}
|
|
c.JSON(http.StatusCreated, h.clientAccessResponse(c, token))
|
|
}
|
|
|
|
func (h *Handler) clientAccessResponse(c *gin.Context, token models.AccessToken) gin.H {
|
|
cfg := config.GlobalConfig
|
|
return gin.H{
|
|
"id": token.ID,
|
|
"name": token.Name,
|
|
"token": token.Token,
|
|
"type": "listener",
|
|
"listener_id": token.ListenerID,
|
|
"mihomo_link": converter.GetSubscriptionURL(cfg, c.Request, token.Token, "mihomo"),
|
|
"clash_link": converter.GetSubscriptionURL(cfg, c.Request, token.Token, "clash"),
|
|
"singbox_link": converter.GetSubscriptionURL(cfg, c.Request, token.Token, "singbox"),
|
|
"shadowrocket_link": converter.GetSubscriptionURL(cfg, c.Request, token.Token, "shadowrocket"),
|
|
}
|
|
}
|