Files
3m-ui/backend/internal/node/validator.go
T
Z User b221a2d731 fix(tuic): unblock v5 listener creation + complete v4 URI params
Audit against official MetaCubeX wiki found 6 TUIC issues. Fixes:
CRITICAL-1 (node/validator.go:150): mapUserProtocols missing 'tuic-v5'
tuic-v5 listeners with wiki-standard 'users: {UUID: pwd}' map were
rejected with 'field users does not accept an object'. This ran
BEFORE validateProtocolSpecific (which would pass), so creating a
tuic-v5 listener via the panel failed at validation time.
Fix: add 'tuic-v5' to mapUserProtocols. tuic-v4 stays out (v4 uses
token, any users on v4 is rejected by schema).
HIGH-2 (node/uri_builders.go:299-313): v4 URI emitted zero query params
tuicURIs v4 path returned 'tuic://token@host:port#name' with NO
alpn / sni / congestion_control / udp_relay_mode / allow_insecure.
Fix: build shared params map ONCE (covers v4 token + v5 uuid paths).
HIGH-3 (protocol/compile_modules.go:141-148): v4 compiler dropped
extra tokens
'token: [T1, T2, T3]' was reduced to 'token: [T1]' at compile time,
silently dropping T2/T3 even though mihomo accepts multi-token v4.
Fix: new allStringTokens() helper preserves every non-empty token.
MEDIUM-4 (node/uri_builders.go): URI query param naming
tuicURIs used hyphen-case (congestion-controller, udp-relay-mode)
but mihomo URI parser expects snake_case. BuildShare already used
snake_case — only the user_sub URI path was wrong.
Fix: map hyphen-case listener keys to snake_case URI keys
(congestion_control, udp_relay_mode, max_udp_relay_packet_size,
bbr_profile, allow_insecure).
MEDIUM-6 (protocol/share_modules.go:633-634): BuildShare skipCert
too aggressive
any cert + empty SNI forced skipCert=true, breaking formal
(Let's Encrypt) cert listeners when operator forgot SNI.
Fix: default SNI to server host; only skipCert for panel self-signed
PEMs (O=3m-ui marker) or explicit skip-cert-verify: true.
LOW-7 (mihomo/config/listener_schema_registry.go:178-182): tuic-v4
schema whitelisted 'users'
v4 is token-only per wiki; symmetric to tuic-v5 omitting 'token'.
Fix: remove 'users' from tuic-v4 Fields.
Tests: 4 new regression tests in node/validator_test.go covering
tuic-v5 map users (accept), tuic-v4 token array (accept),
tuic-v4-with-users (reject), tuic-v5-with-token (reject).
Full suite 23/23 packages PASS.
fix(tuic): unblock v5 listener creation + complete v4 URI params
Audit against official MetaCubeX wiki found 6 TUIC issues. Fixes:
CRITICAL-1 (node/validator.go:150): mapUserProtocols missing 'tuic-v5'
tuic-v5 listeners with wiki-standard 'users: {UUID: pwd}' map were
rejected with 'field users does not accept an object'. This ran
BEFORE validateProtocolSpecific (which would pass), so creating a
tuic-v5 listener via the panel failed at validation time.
Fix: add 'tuic-v5' to mapUserProtocols. tuic-v4 stays out (v4 uses
token, any users on v4 is rejected by schema).
HIGH-2 (node/uri_builders.go:299-313): v4 URI emitted zero query params
tuicURIs v4 path returned 'tuic://token@host:port#name' with NO
alpn / sni / congestion_control / udp_relay_mode / allow_insecure.
Fix: build shared params map ONCE (covers v4 token + v5 uuid paths).
HIGH-3 (protocol/compile_modules.go:141-148): v4 compiler dropped
extra tokens
'token: [T1, T2, T3]' was reduced to 'token: [T1]' at compile time,
silently dropping T2/T3 even though mihomo accepts multi-token v4.
Fix: new allStringTokens() helper preserves every non-empty token.
MEDIUM-4 (node/uri_builders.go): URI query param naming
tuicURIs used hyphen-case (congestion-controller, udp-relay-mode)
but mihomo URI parser expects snake_case. BuildShare already used
snake_case — only the user_sub URI path was wrong.
Fix: map hyphen-case listener keys to snake_case URI keys
(congestion_control, udp_relay_mode, max_udp_relay_packet_size,
bbr_profile, allow_insecure).
MEDIUM-6 (protocol/share_modules.go:633-634): BuildShare skipCert
too aggressive
any cert + empty SNI forced skipCert=true, breaking formal
(Let's Encrypt) cert listeners when operator forgot SNI.
Fix: default SNI to server host; only skipCert for panel self-signed
PEMs (O=3m-ui marker) or explicit skip-cert-verify: true.
LOW-7 (mihomo/config/listener_schema_registry.go:178-182): tuic-v4
schema whitelisted 'users'
v4 is token-only per wiki; symmetric to tuic-v5 omitting 'token'.
Fix: remove 'users' from tuic-v4 Fields.
Tests: 4 new regression tests in node/validator_test.go covering
tuic-v5 map users (accept), tuic-v4 token array (accept),
tuic-v4-with-users (reject), tuic-v5-with-token (reject).
Full suite 23/23 packages PASS.
fix(tuic): unblock v5 listener creation + complete v4 URI params
Audit against official MetaCubeX wiki found 6 TUIC issues. Fixes:
CRITICAL-1 (node/validator.go:150): mapUserProtocols missing 'tuic-v5'
tuic-v5 listeners with wiki-standard 'users: {UUID: pwd}' map were
rejected with 'field users does not accept an object'. This ran
BEFORE validateProtocolSpecific (which would pass), so creating a
tuic-v5 listener via the panel failed at validation time.
Fix: add 'tuic-v5' to mapUserProtocols. tuic-v4 stays out (v4 uses
token, any users on v4 is rejected by schema).
HIGH-2 (node/uri_builders.go:299-313): v4 URI emitted zero query params
tuicURIs v4 path returned 'tuic://token@host:port#name' with NO
alpn / sni / congestion_control / udp_relay_mode / allow_insecure.
Fix: build shared params map ONCE (covers v4 token + v5 uuid paths).
HIGH-3 (protocol/compile_modules.go:141-148): v4 compiler dropped
extra tokens
'token: [T1, T2, T3]' was reduced to 'token: [T1]' at compile time,
silently dropping T2/T3 even though mihomo accepts multi-token v4.
Fix: new allStringTokens() helper preserves every non-empty token.
MEDIUM-4 (node/uri_builders.go): URI query param naming
tuicURIs used hyphen-case (congestion-controller, udp-relay-mode)
but mihomo URI parser expects snake_case. BuildShare already used
snake_case — only the user_sub URI path was wrong.
Fix: map hyphen-case listener keys to snake_case URI keys
(congestion_control, udp_relay_mode, max_udp_relay_packet_size,
bbr_profile, allow_insecure).
MEDIUM-6 (protocol/share_modules.go:633-634): BuildShare skipCert
too aggressive
any cert + empty SNI forced skipCert=true, breaking formal
(Let's Encrypt) cert listeners when operator forgot SNI.
Fix: default SNI to server host; only skipCert for panel self-signed
PEMs (O=3m-ui marker) or explicit skip-cert-verify: true.
LOW-7 (mihomo/config/listener_schema_registry.go:178-182): tuic-v4
schema whitelisted 'users'
v4 is token-only per wiki; symmetric to tuic-v5 omitting 'token'.
Fix: remove 'users' from tuic-v4 Fields.
Tests: 4 new regression tests in node/validator_test.go covering
tuic-v5 map users (accept), tuic-v4 token array (accept),
tuic-v4-with-users (reject), tuic-v5-with-token (reject).
Full suite 23/23 packages PASS.
fix(tuic): unblock v5 listener creation + complete v4 URI params
Audit against official MetaCubeX wiki found 6 TUIC issues. Fixes:
CRITICAL-1 (node/validator.go:150): mapUserProtocols missing 'tuic-v5'
tuic-v5 listeners with wiki-standard 'users: {UUID: pwd}' map were
rejected with 'field users does not accept an object'. This ran
BEFORE validateProtocolSpecific (which would pass), so creating a
tuic-v5 listener via the panel failed at validation time.
Fix: add 'tuic-v5' to mapUserProtocols. tuic-v4 stays out (v4 uses
token, any users on v4 is rejected by schema).
HIGH-2 (node/uri_builders.go:299-313): v4 URI emitted zero query params
tuicURIs v4 path returned 'tuic://token@host:port#name' with NO
alpn / sni / congestion_control / udp_relay_mode / allow_insecure.
Fix: build shared params map ONCE (covers v4 token + v5 uuid paths).
HIGH-3 (protocol/compile_modules.go:141-148): v4 compiler dropped
extra tokens
'token: [T1, T2, T3]' was reduced to 'token: [T1]' at compile time,
silently dropping T2/T3 even though mihomo accepts multi-token v4.
Fix: new allStringTokens() helper preserves every non-empty token.
MEDIUM-4 (node/uri_builders.go): URI query param naming
tuicURIs used hyphen-case (congestion-controller, udp-relay-mode)
but mihomo URI parser expects snake_case. BuildShare already used
snake_case — only the user_sub URI path was wrong.
Fix: map hyphen-case listener keys to snake_case URI keys
(congestion_control, udp_relay_mode, max_udp_relay_packet_size,
bbr_profile, allow_insecure).
MEDIUM-6 (protocol/share_modules.go:633-634): BuildShare skipCert
too aggressive
any cert + empty SNI forced skipCert=true, breaking formal
(Let's Encrypt) cert listeners when operator forgot SNI.
Fix: default SNI to server host; only skipCert for panel self-signed
PEMs (O=3m-ui marker) or explicit skip-cert-verify: true.
LOW-7 (mihomo/config/listener_schema_registry.go:178-182): tuic-v4
schema whitelisted 'users'
v4 is token-only per wiki; symmetric to tuic-v5 omitting 'token'.
Fix: remove 'users' from tuic-v4 Fields.
Tests: 4 new regression tests in node/validator_test.go covering
tuic-v5 map users (accept), tuic-v4 token array (accept),
tuic-v4-with-users (reject), tuic-v5-with-token (reject).
Full suite 23/23 packages PASS.
fix(tuic): unblock v5 listener creation + complete v4 URI params
Audit against official MetaCubeX wiki found 6 TUIC issues. Fixes:
CRITICAL-1 (node/validator.go:150): mapUserProtocols missing 'tuic-v5'
tuic-v5 listeners with wiki-standard 'users: {UUID: pwd}' map were
rejected with 'field users does not accept an object'. This ran
BEFORE validateProtocolSpecific (which would pass), so creating a
tuic-v5 listener via the panel failed at validation time.
Fix: add 'tuic-v5' to mapUserProtocols. tuic-v4 stays out (v4 uses
token, any users on v4 is rejected by schema).
HIGH-2 (node/uri_builders.go:299-313): v4 URI emitted zero query params
tuicURIs v4 path returned 'tuic://token@host:port#name' with NO
alpn / sni / congestion_control / udp_relay_mode / allow_insecure.
Fix: build shared params map ONCE (covers v4 token + v5 uuid paths).
HIGH-3 (protocol/compile_modules.go:141-148): v4 compiler dropped
extra tokens
'token: [T1, T2, T3]' was reduced to 'token: [T1]' at compile time,
silently dropping T2/T3 even though mihomo accepts multi-token v4.
Fix: new allStringTokens() helper preserves every non-empty token.
MEDIUM-4 (node/uri_builders.go): URI query param naming
tuicURIs used hyphen-case (congestion-controller, udp-relay-mode)
but mihomo URI parser expects snake_case. BuildShare already used
snake_case — only the user_sub URI path was wrong.
Fix: map hyphen-case listener keys to snake_case URI keys
(congestion_control, udp_relay_mode, max_udp_relay_packet_size,
bbr_profile, allow_insecure).
MEDIUM-6 (protocol/share_modules.go:633-634): BuildShare skipCert
too aggressive
any cert + empty SNI forced skipCert=true, breaking formal
(Let's Encrypt) cert listeners when operator forgot SNI.
Fix: default SNI to server host; only skipCert for panel self-signed
PEMs (O=3m-ui marker) or explicit skip-cert-verify: true.
LOW-7 (mihomo/config/listener_schema_registry.go:178-182): tuic-v4
schema whitelisted 'users'
v4 is token-only per wiki; symmetric to tuic-v5 omitting 'token'.
Fix: remove 'users' from tuic-v4 Fields.
Tests: 4 new regression tests in node/validator_test.go covering
tuic-v5 map users (accept), tuic-v4 token array (accept),
tuic-v4-with-users (reject), tuic-v5-with-token (reject).
Full suite 23/23 packages PASS.
fix(tuic): unblock v5 listener creation + complete v4 URI params
Audit against official MetaCubeX wiki found 6 TUIC issues. Fixes:
CRITICAL-1 (node/validator.go:150): mapUserProtocols missing 'tuic-v5'
tuic-v5 listeners with wiki-standard 'users: {UUID: pwd}' map were
rejected with 'field users does not accept an object'. This ran
BEFORE validateProtocolSpecific (which would pass), so creating a
tuic-v5 listener via the panel failed at validation time.
Fix: add 'tuic-v5' to mapUserProtocols. tuic-v4 stays out (v4 uses
token, any users on v4 is rejected by schema).
HIGH-2 (node/uri_builders.go:299-313): v4 URI emitted zero query params
tuicURIs v4 path returned 'tuic://token@host:port#name' with NO
alpn / sni / congestion_control / udp_relay_mode / allow_insecure.
Fix: build shared params map ONCE (covers v4 token + v5 uuid paths).
HIGH-3 (protocol/compile_modules.go:141-148): v4 compiler dropped
extra tokens
'token: [T1, T2, T3]' was reduced to 'token: [T1]' at compile time,
silently dropping T2/T3 even though mihomo accepts multi-token v4.
Fix: new allStringTokens() helper preserves every non-empty token.
MEDIUM-4 (node/uri_builders.go): URI query param naming
tuicURIs used hyphen-case (congestion-controller, udp-relay-mode)
but mihomo URI parser expects snake_case. BuildShare already used
snake_case — only the user_sub URI path was wrong.
Fix: map hyphen-case listener keys to snake_case URI keys
(congestion_control, udp_relay_mode, max_udp_relay_packet_size,
bbr_profile, allow_insecure).
MEDIUM-6 (protocol/share_modules.go:633-634): BuildShare skipCert
too aggressive
any cert + empty SNI forced skipCert=true, breaking formal
(Let's Encrypt) cert listeners when operator forgot SNI.
Fix: default SNI to server host; only skipCert for panel self-signed
PEMs (O=3m-ui marker) or explicit skip-cert-verify: true.
LOW-7 (mihomo/config/listener_schema_registry.go:178-182): tuic-v4
schema whitelisted 'users'
v4 is token-only per wiki; symmetric to tuic-v5 omitting 'token'.
Fix: remove 'users' from tuic-v4 Fields.
Tests: 4 new regression tests in node/validator_test.go covering
tuic-v5 map users (accept), tuic-v4 token array (accept),
tuic-v4-with-users (reject), tuic-v5-with-token (reject).
Full suite 23/23 packages PASS.
fix(tuic): unblock v5 listener creation + complete v4 URI params
Audit against official MetaCubeX wiki found 6 TUIC issues. Fixes:
CRITICAL-1 (node/validator.go:150): mapUserProtocols missing 'tuic-v5'
tuic-v5 listeners with wiki-standard 'users: {UUID: pwd}' map were
rejected with 'field users does not accept an object'. This ran
BEFORE validateProtocolSpecific (which would pass), so creating a
tuic-v5 listener via the panel failed at validation time.
Fix: add 'tuic-v5' to mapUserProtocols. tuic-v4 stays out (v4 uses
token, any users on v4 is rejected by schema).
HIGH-2 (node/uri_builders.go:299-313): v4 URI emitted zero query params
tuicURIs v4 path returned 'tuic://token@host:port#name' with NO
alpn / sni / congestion_control / udp_relay_mode / allow_insecure.
Fix: build shared params map ONCE (covers v4 token + v5 uuid paths).
HIGH-3 (protocol/compile_modules.go:141-148): v4 compiler dropped
extra tokens
'token: [T1, T2, T3]' was reduced to 'token: [T1]' at compile time,
silently dropping T2/T3 even though mihomo accepts multi-token v4.
Fix: new allStringTokens() helper preserves every non-empty token.
MEDIUM-4 (node/uri_builders.go): URI query param naming
tuicURIs used hyphen-case (congestion-controller, udp-relay-mode)
but mihomo URI parser expects snake_case. BuildShare already used
snake_case — only the user_sub URI path was wrong.
Fix: map hyphen-case listener keys to snake_case URI keys
(congestion_control, udp_relay_mode, max_udp_relay_packet_size,
bbr_profile, allow_insecure).
MEDIUM-6 (protocol/share_modules.go:633-634): BuildShare skipCert
too aggressive
any cert + empty SNI forced skipCert=true, breaking formal
(Let's Encrypt) cert listeners when operator forgot SNI.
Fix: default SNI to server host; only skipCert for panel self-signed
PEMs (O=3m-ui marker) or explicit skip-cert-verify: true.
LOW-7 (mihomo/config/listener_schema_registry.go:178-182): tuic-v4
schema whitelisted 'users'
v4 is token-only per wiki; symmetric to tuic-v5 omitting 'token'.
Fix: remove 'users' from tuic-v4 Fields.
Tests: 4 new regression tests in node/validator_test.go covering
tuic-v5 map users (accept), tuic-v4 token array (accept),
tuic-v4-with-users (reject), tuic-v5-with-token (reject).
Full suite 23/23 packages PASS.
fix(tuic): unblock v5 listener creation + complete v4 URI params
Audit against official MetaCubeX wiki found 6 TUIC issues. Fixes:
CRITICAL-1 (node/validator.go:150): mapUserProtocols missing 'tuic-v5'
tuic-v5 listeners with wiki-standard 'users: {UUID: pwd}' map were
rejected with 'field users does not accept an object'. This ran
BEFORE validateProtocolSpecific (which would pass), so creating a
tuic-v5 listener via the panel failed at validation time.
Fix: add 'tuic-v5' to mapUserProtocols. tuic-v4 stays out (v4 uses
token, any users on v4 is rejected by schema).
HIGH-2 (node/uri_builders.go:299-313): v4 URI emitted zero query params
tuicURIs v4 path returned 'tuic://token@host:port#name' with NO
alpn / sni / congestion_control / udp_relay_mode / allow_insecure.
Fix: build shared params map ONCE (covers v4 token + v5 uuid paths).
HIGH-3 (protocol/compile_modules.go:141-148): v4 compiler dropped
extra tokens
'token: [T1, T2, T3]' was reduced to 'token: [T1]' at compile time,
silently dropping T2/T3 even though mihomo accepts multi-token v4.
Fix: new allStringTokens() helper preserves every non-empty token.
MEDIUM-4 (node/uri_builders.go): URI query param naming
tuicURIs used hyphen-case (congestion-controller, udp-relay-mode)
but mihomo URI parser expects snake_case. BuildShare already used
snake_case — only the user_sub URI path was wrong.
Fix: map hyphen-case listener keys to snake_case URI keys
(congestion_control, udp_relay_mode, max_udp_relay_packet_size,
bbr_profile, allow_insecure).
MEDIUM-6 (protocol/share_modules.go:633-634): BuildShare skipCert
too aggressive
any cert + empty SNI forced skipCert=true, breaking formal
(Let's Encrypt) cert listeners when operator forgot SNI.
Fix: default SNI to server host; only skipCert for panel self-signed
PEMs (O=3m-ui marker) or explicit skip-cert-verify: true.
LOW-7 (mihomo/config/listener_schema_registry.go:178-182): tuic-v4
schema whitelisted 'users'
v4 is token-only per wiki; symmetric to tuic-v5 omitting 'token'.
Fix: remove 'users' from tuic-v4 Fields.
Tests: 4 new regression tests in node/validator_test.go covering
tuic-v5 map users (accept), tuic-v4 token array (accept),
tuic-v4-with-users (reject), tuic-v5-with-token (reject).
Full suite 23/23 packages PASS.
fix(tuic): unblock v5 listener creation + complete v4 URI params
Audit against official MetaCubeX wiki found 6 TUIC issues. Fixes:
CRITICAL-1 (node/validator.go:150): mapUserProtocols missing 'tuic-v5'
tuic-v5 listeners with wiki-standard 'users: {UUID: pwd}' map were
rejected with 'field users does not accept an object'. This ran
BEFORE validateProtocolSpecific (which would pass), so creating a
tuic-v5 listener via the panel failed at validation time.
Fix: add 'tuic-v5' to mapUserProtocols. tuic-v4 stays out (v4 uses
token, any users on v4 is rejected by schema).
HIGH-2 (node/uri_builders.go:299-313): v4 URI emitted zero query params
tuicURIs v4 path returned 'tuic://token@host:port#name' with NO
alpn / sni / congestion_control / udp_relay_mode / allow_insecure.
Fix: build shared params map ONCE (covers v4 token + v5 uuid paths).
HIGH-3 (protocol/compile_modules.go:141-148): v4 compiler dropped
extra tokens
'token: [T1, T2, T3]' was reduced to 'token: [T1]' at compile time,
silently dropping T2/T3 even though mihomo accepts multi-token v4.
Fix: new allStringTokens() helper preserves every non-empty token.
MEDIUM-4 (node/uri_builders.go): URI query param naming
tuicURIs used hyphen-case (congestion-controller, udp-relay-mode)
but mihomo URI parser expects snake_case. BuildShare already used
snake_case — only the user_sub URI path was wrong.
Fix: map hyphen-case listener keys to snake_case URI keys
(congestion_control, udp_relay_mode, max_udp_relay_packet_size,
bbr_profile, allow_insecure).
MEDIUM-6 (protocol/share_modules.go:633-634): BuildShare skipCert
too aggressive
any cert + empty SNI forced skipCert=true, breaking formal
(Let's Encrypt) cert listeners when operator forgot SNI.
Fix: default SNI to server host; only skipCert for panel self-signed
PEMs (O=3m-ui marker) or explicit skip-cert-verify: true.
LOW-7 (mihomo/config/listener_schema_registry.go:178-182): tuic-v4
schema whitelisted 'users'
v4 is token-only per wiki; symmetric to tuic-v5 omitting 'token'.
Fix: remove 'users' from tuic-v4 Fields.
Tests: 4 new regression tests in node/validator_test.go covering
tuic-v5 map users (accept), tuic-v4 token array (accept),
tuic-v4-with-users (reject), tuic-v5-with-token (reject).
Full suite 23/23 packages PASS.
fix(tuic): unblock v5 listener creation + complete v4 URI params
Audit against official MetaCubeX wiki found 6 TUIC issues. Fixes:
CRITICAL-1 (node/validator.go:150): mapUserProtocols missing 'tuic-v5'
tuic-v5 listeners with wiki-standard 'users: {UUID: pwd}' map were
rejected with 'field users does not accept an object'. This ran
BEFORE validateProtocolSpecific (which would pass), so creating a
tuic-v5 listener via the panel failed at validation time.
Fix: add 'tuic-v5' to mapUserProtocols. tuic-v4 stays out (v4 uses
token, any users on v4 is rejected by schema).
HIGH-2 (node/uri_builders.go:299-313): v4 URI emitted zero query params
tuicURIs v4 path returned 'tuic://token@host:port#name' with NO
alpn / sni / congestion_control / udp_relay_mode / allow_insecure.
Fix: build shared params map ONCE (covers v4 token + v5 uuid paths).
HIGH-3 (protocol/compile_modules.go:141-148): v4 compiler dropped
extra tokens
'token: [T1, T2, T3]' was reduced to 'token: [T1]' at compile time,
silently dropping T2/T3 even though mihomo accepts multi-token v4.
Fix: new allStringTokens() helper preserves every non-empty token.
MEDIUM-4 (node/uri_builders.go): URI query param naming
tuicURIs used hyphen-case (congestion-controller, udp-relay-mode)
but mihomo URI parser expects snake_case. BuildShare already used
snake_case — only the user_sub URI path was wrong.
Fix: map hyphen-case listener keys to snake_case URI keys
(congestion_control, udp_relay_mode, max_udp_relay_packet_size,
bbr_profile, allow_insecure).
MEDIUM-6 (protocol/share_modules.go:633-634): BuildShare skipCert
too aggressive
any cert + empty SNI forced skipCert=true, breaking formal
(Let's Encrypt) cert listeners when operator forgot SNI.
Fix: default SNI to server host; only skipCert for panel self-signed
PEMs (O=3m-ui marker) or explicit skip-cert-verify: true.
LOW-7 (mihomo/config/listener_schema_registry.go:178-182): tuic-v4
schema whitelisted 'users'
v4 is token-only per wiki; symmetric to tuic-v5 omitting 'token'.
Fix: remove 'users' from tuic-v4 Fields.
Tests: 4 new regression tests in node/validator_test.go covering
tuic-v5 map users (accept), tuic-v4 token array (accept),
tuic-v4-with-users (reject), tuic-v5-with-token (reject).
Full suite 23/23 packages PASS.
fix(tuic): unblock v5 listener creation + complete v4 URI params
Audit against official MetaCubeX wiki found 6 TUIC issues. Fixes:
CRITICAL-1 (node/validator.go:150): mapUserProtocols missing 'tuic-v5'
tuic-v5 listeners with wiki-standard 'users: {UUID: pwd}' map were
rejected with 'field users does not accept an object'. This ran
BEFORE validateProtocolSpecific (which would pass), so creating a
tuic-v5 listener via the panel failed at validation time.
Fix: add 'tuic-v5' to mapUserProtocols. tuic-v4 stays out (v4 uses
token, any users on v4 is rejected by schema).
HIGH-2 (node/uri_builders.go:299-313): v4 URI emitted zero query params
tuicURIs v4 path returned 'tuic://token@host:port#name' with NO
alpn / sni / congestion_control / udp_relay_mode / allow_insecure.
Fix: build shared params map ONCE (covers v4 token + v5 uuid paths).
HIGH-3 (protocol/compile_modules.go:141-148): v4 compiler dropped
extra tokens
'token: [T1, T2, T3]' was reduced to 'token: [T1]' at compile time,
silently dropping T2/T3 even though mihomo accepts multi-token v4.
Fix: new allStringTokens() helper preserves every non-empty token.
MEDIUM-4 (node/uri_builders.go): URI query param naming
tuicURIs used hyphen-case (congestion-controller, udp-relay-mode)
but mihomo URI parser expects snake_case. BuildShare already used
snake_case — only the user_sub URI path was wrong.
Fix: map hyphen-case listener keys to snake_case URI keys
(congestion_control, udp_relay_mode, max_udp_relay_packet_size,
bbr_profile, allow_insecure).
MEDIUM-6 (protocol/share_modules.go:633-634): BuildShare skipCert
too aggressive
any cert + empty SNI forced skipCert=true, breaking formal
(Let's Encrypt) cert listeners when operator forgot SNI.
Fix: default SNI to server host; only skipCert for panel self-signed
PEMs (O=3m-ui marker) or explicit skip-cert-verify: true.
LOW-7 (mihomo/config/listener_schema_registry.go:178-182): tuic-v4
schema whitelisted 'users'
v4 is token-only per wiki; symmetric to tuic-v5 omitting 'token'.
Fix: remove 'users' from tuic-v4 Fields.
Tests: 4 new regression tests in node/validator_test.go covering
tuic-v5 map users (accept), tuic-v4 token array (accept),
tuic-v4-with-users (reject), tuic-v5-with-token (reject).
Full suite 23/23 packages PASS.
fix(tuic): unblock v5 listener creation + complete v4 URI params
Audit against official MetaCubeX wiki found 6 TUIC issues. Fixes:
CRITICAL-1 (node/validator.go:150): mapUserProtocols missing 'tuic-v5'
tuic-v5 listeners with wiki-standard 'users: {UUID: pwd}' map were
rejected with 'field users does not accept an object'. This ran
BEFORE validateProtocolSpecific (which would pass), so creating a
tuic-v5 listener via the panel failed at validation time.
Fix: add 'tuic-v5' to mapUserProtocols. tuic-v4 stays out (v4 uses
token, any users on v4 is rejected by schema).
HIGH-2 (node/uri_builders.go:299-313): v4 URI emitted zero query params
tuicURIs v4 path returned 'tuic://token@host:port#name' with NO
alpn / sni / congestion_control / udp_relay_mode / allow_insecure.
Fix: build shared params map ONCE (covers v4 token + v5 uuid paths).
HIGH-3 (protocol/compile_modules.go:141-148): v4 compiler dropped
extra tokens
'token: [T1, T2, T3]' was reduced to 'token: [T1]' at compile time,
silently dropping T2/T3 even though mihomo accepts multi-token v4.
Fix: new allStringTokens() helper preserves every non-empty token.
MEDIUM-4 (node/uri_builders.go): URI query param naming
tuicURIs used hyphen-case (congestion-controller, udp-relay-mode)
but mihomo URI parser expects snake_case. BuildShare already used
snake_case — only the user_sub URI path was wrong.
Fix: map hyphen-case listener keys to snake_case URI keys
(congestion_control, udp_relay_mode, max_udp_relay_packet_size,
bbr_profile, allow_insecure).
MEDIUM-6 (protocol/share_modules.go:633-634): BuildShare skipCert
too aggressive
any cert + empty SNI forced skipCert=true, breaking formal
(Let's Encrypt) cert listeners when operator forgot SNI.
Fix: default SNI to server host; only skipCert for panel self-signed
PEMs (O=3m-ui marker) or explicit skip-cert-verify: true.
LOW-7 (mihomo/config/listener_schema_registry.go:178-182): tuic-v4
schema whitelisted 'users'
v4 is token-only per wiki; symmetric to tuic-v5 omitting 'token'.
Fix: remove 'users' from tuic-v4 Fields.
Tests: 4 new regression tests in node/validator_test.go covering
tuic-v5 map users (accept), tuic-v4 token array (accept),
tuic-v4-with-users (reject), tuic-v5-with-token (reject).
Full suite 23/23 packages PASS.
2026-09-16 14:49:45 +00:00

436 lines
13 KiB
Go

package node
import (
"encoding/json"
"fmt"
"net"
"reflect"
"strconv"
"strings"
"github.com/kazeyukiro/3m-ui/backend/internal/credentials"
"github.com/kazeyukiro/3m-ui/backend/internal/database/models"
mihomoConfig "github.com/kazeyukiro/3m-ui/backend/internal/mihomo/config"
)
func ValidateNode(l *models.Listener) error {
l.Name = strings.TrimSpace(l.Name)
if l.Name == "" {
return fmt.Errorf("listener name cannot be empty")
}
proto := strings.ToLower(strings.TrimSpace(l.Protocol))
if proto == "" {
proto = strings.ToLower(strings.TrimSpace(l.Type))
}
if !mihomoConfig.IsMihomoListenerProtocol(proto) {
return fmt.Errorf("unsupported Mihomo listener protocol: %s", proto)
}
l.Protocol, l.Type = proto, proto
if !isValidPortString(l.Port) {
return fmt.Errorf("port must be a valid port number, range (e.g. 8080-8090), or comma-separated list")
}
l.BindAddress = strings.TrimSpace(l.BindAddress)
if l.BindAddress == "" {
l.BindAddress = strings.TrimSpace(l.Listen)
}
if l.BindAddress == "" {
l.BindAddress = "0.0.0.0"
}
// Strip brackets; accept IPv4 / IPv6 (including :: and 0.0.0.0).
l.BindAddress = strings.Trim(l.BindAddress, "[]")
if net.ParseIP(l.BindAddress) == nil {
return fmt.Errorf("bind address must be a valid IPv4 or IPv6 address (got %q)", l.BindAddress)
}
if h := strings.TrimSpace(l.PublicHost); h != "" {
// Public host may be domain or IP — normalize bracketed IPv6.
l.PublicHost = strings.Trim(h, "[]")
}
if l.RoutingMark < 0 {
return fmt.Errorf("routing-mark must be non-negative")
}
if err := credentials.EnsureListenerCredentials(l); err != nil {
return err
}
cfg, err := decodeConfig(l.Config)
if err != nil {
return err
}
if err := validateSchema(proto, cfg); err != nil {
return err
}
return validateProtocolSpecific(proto, cfg)
}
func isValidPortString(s string) bool {
s = strings.TrimSpace(s)
if s == "" {
return false
}
if strings.Contains(s, ",") {
parts := strings.Split(s, ",")
for _, p := range parts {
if !isValidPortString(strings.TrimSpace(p)) {
return false
}
}
return true
}
if strings.Contains(s, "-") {
parts := strings.SplitN(s, "-", 2)
if len(parts) != 2 {
return false
}
start, err1 := strconv.Atoi(strings.TrimSpace(parts[0]))
end, err2 := strconv.Atoi(strings.TrimSpace(parts[1]))
if err1 != nil || err2 != nil || start < 1 || end > 65535 || start > end {
return false
}
return true
}
port, err := strconv.Atoi(s)
if err != nil || port < 1 || port > 65535 {
return false
}
return true
}
func decodeConfig(raw string) (map[string]interface{}, error) {
if strings.TrimSpace(raw) == "" {
return map[string]interface{}{}, nil
}
var cfg map[string]interface{}
if err := json.Unmarshal([]byte(raw), &cfg); err != nil {
return nil, fmt.Errorf("invalid listener configuration: %w", err)
}
if cfg == nil {
return map[string]interface{}{}, nil
}
return cfg, nil
}
func validateSchema(proto string, cfg map[string]interface{}) error {
schema, ok := mihomoConfig.GetMihomoListenerSchema(proto)
if !ok {
return fmt.Errorf("no listener schema registered for protocol %s", proto)
}
return validateObject(proto, "", cfg, schema.Fields, schema.NestedFields)
}
func validateObject(proto, prefix string, value map[string]interface{}, allowed map[string]struct{}, nested map[string]map[string]struct{}) error {
for key, child := range value {
if _, ok := allowed[key]; !ok {
return fmt.Errorf("%s listener: unsupported field %q", proto, joinField(prefix, key))
}
if childMap, ok := child.(map[string]interface{}); ok {
if key == "users" && mapUserProtocols[proto] {
continue
}
childAllowed, hasNestedSchema := nested[key]
if !hasNestedSchema {
return fmt.Errorf("%s listener: field %q does not accept an object", proto, joinField(prefix, key))
}
childNested := make(map[string]map[string]struct{})
for nestedKey, nestedAllowed := range nested {
if strings.HasPrefix(nestedKey, key+".") {
childNested[strings.TrimPrefix(nestedKey, key+".")] = nestedAllowed
}
}
if err := validateObject(proto, joinField(prefix, key), childMap, childAllowed, childNested); err != nil {
return err
}
}
}
return nil
}
// mapUserProtocols lists protocols whose `users` field is a free-form map
// (UUID→password) or array of {username,password,...} objects, where the
// per-user keys must not be validated against the schema's NestedFields.
// - anytls / hysteria2 / mieru: map UUID→password
// - tuic (generic) / tuic-v5: v5 form is map UUID→password per wiki
// - tuic-v4 is intentionally NOT here — v4 uses `token: [string]`, and any
// `users` field on v4 is rejected by validateProtocolSpecific (line ~190).
var mapUserProtocols = map[string]bool{
"anytls": true,
"hysteria2": true,
"mieru": true,
"tuic": true,
"tuic-v5": true,
}
func joinField(prefix, field string) string {
if prefix == "" {
return field
}
return prefix + "." + field
}
func validateProtocolSpecific(proto string, cfg map[string]interface{}) error {
switch proto {
case "anytls":
return validateAnyTLS(cfg)
case "snell":
if value, ok := numeric(cfg["version"]); ok && (value < 1 || value > 5) {
return fmt.Errorf("snell version must be between 1 and 5")
}
case "hysteria2":
if obfs, ok := cfg["obfs"].(string); ok && obfs != "" && obfs != "salamander" {
return fmt.Errorf("hysteria2 obfs must be salamander")
}
// Official mihomo Hysteria2 inbound requires certificate/private-key; there is no allow-insecure field.
if !hasCertificatePair(cfg) {
return fmt.Errorf("hysteria2 listener requires certificate and private-key")
}
if users, ok := cfg["users"].(map[string]interface{}); !ok || len(users) == 0 {
return fmt.Errorf("hysteria2 listener requires at least one user")
}
case "trusttunnel":
if !hasCertificatePair(cfg) {
return fmt.Errorf("trusttunnel listener requires certificate and private-key")
}
case "tuic", "tuic-v4", "tuic-v5":
// Panel uses tuic-v4 / tuic-v5; Mihomo YAML type is always "tuic".
users, token := hasNonEmpty(cfg["users"]), hasNonEmpty(cfg["token"])
switch proto {
case "tuic-v4":
if !token {
return fmt.Errorf("tuic-v4 listener requires a non-empty token array")
}
if users {
return fmt.Errorf("tuic-v4 listener must not set users (token-only)")
}
case "tuic-v5":
if !users {
return fmt.Errorf("tuic-v5 listener requires users map (UUID → password)")
}
if token {
return fmt.Errorf("tuic-v5 listener must not set token (users-only)")
}
if values, ok := cfg["users"].(map[string]interface{}); !ok || len(values) == 0 {
return fmt.Errorf("tuic-v5 listener users must be a UUID-to-password map")
}
default: // "tuic" legacy: exactly one of token or users
if users == token {
return fmt.Errorf("tuic listener must configure exactly one of users (TUIC V5) or token (TUIC V4)")
}
if users {
if values, ok := cfg["users"].(map[string]interface{}); !ok || len(values) == 0 {
return fmt.Errorf("tuic V5 listener users must be a username-to-password map")
}
}
}
case "vless", "vmess":
if err := validateServerTLSModes(proto, cfg); err != nil {
return err
}
users, ok := cfg["users"].([]interface{})
if !ok || len(users) == 0 {
return fmt.Errorf("%s listener requires at least one user", proto)
}
for i, user := range users {
if err := validateUserRow(proto, i, user, true); err != nil {
return err
}
}
case "trojan":
if err := validateServerTLSModes(proto, cfg); err != nil {
return err
}
users, ok := cfg["users"].([]interface{})
if !ok || len(users) == 0 {
return fmt.Errorf("%s listener requires at least one user", proto)
}
for i, user := range users {
if err := validateUserRow(proto, i, user, false); err != nil {
return err
}
}
case "shadowquic":
users, ok := cfg["users"].([]interface{})
if !ok || len(users) == 0 {
return fmt.Errorf("%s listener requires at least one user", proto)
}
for i, user := range users {
if err := validateUserRow(proto, i, user, false); err != nil {
return err
}
}
case "mieru":
if users, ok := cfg["users"].(map[string]interface{}); !ok || len(users) == 0 {
return fmt.Errorf("mieru listener requires at least one user")
}
case "sudoku":
min, minOK := numeric(cfg["padding-min"])
max, maxOK := numeric(cfg["padding-max"])
if minOK && maxOK && max < min {
return fmt.Errorf("sudoku padding-max must be greater than or equal to padding-min")
}
}
return nil
}
func validateAnyTLS(cfg map[string]interface{}) error {
cert, key := hasString(cfg["certificate"]), hasString(cfg["private-key"])
if cert != key {
return fmt.Errorf("anytls listener requires certificate and private-key together")
}
shadow, res, jls := enabledObject(cfg["shadow-tls"]), enabledObject(cfg["res-tls"]), enabledObject(cfg["jls-config"])
secureModes := 0
if shadow {
secureModes++
}
if res {
secureModes++
}
if jls {
secureModes++
}
if cert {
secureModes++
}
if secureModes > 1 {
return fmt.Errorf("anytls listener TLS modes are mutually exclusive: use certificate/private-key, shadow-tls, res-tls, or jls-config")
}
if !boolValue(cfg["allow-insecure"]) && secureModes == 0 {
return fmt.Errorf("anytls listener requires certificate/private-key, shadow-tls, res-tls, jls-config, or allow-insecure=true")
}
if auth, ok := cfg["client-auth-type"].(string); ok && (auth == "verify-if-given" || auth == "require-and-verify") && !hasString(cfg["client-auth-cert"]) {
return fmt.Errorf("anytls listener client-auth-cert is required for %s", auth)
}
users, ok := cfg["users"].(map[string]interface{})
if !ok || len(users) == 0 {
return fmt.Errorf("anytls listener requires at least one user")
}
for username, password := range users {
if strings.TrimSpace(username) == "" || !hasString(password) {
return fmt.Errorf("anytls listener users must contain non-empty username/password pairs")
}
}
return nil
}
func enabledObject(v interface{}) bool {
m, ok := v.(map[string]interface{})
return ok && boolValue(m["enable"])
}
func validateUserRow(proto string, index int, raw interface{}, uuidMode bool) error {
row, ok := raw.(map[string]interface{})
if !ok {
return fmt.Errorf("%s listener users[%d] must be an object", proto, index)
}
// Protocol-specific field bans with explicit messages expected by tests
// and matching official Mihomo listener schemas.
if proto == "vmess" {
if _, ok := row["flow"]; ok {
return fmt.Errorf("vmess listener users[%d] does not support flow", index)
}
}
if proto == "vless" {
if _, ok := row["alterId"]; ok {
return fmt.Errorf("vless listener users[%d] does not support alterId", index)
}
}
allowed := map[string]struct{}{"username": {}}
if uuidMode {
allowed["uuid"] = struct{}{}
if proto == "vmess" {
allowed["alterId"] = struct{}{}
} else {
allowed["flow"] = struct{}{}
}
} else {
allowed["password"] = struct{}{}
}
for key := range row {
if _, ok := allowed[key]; !ok {
return fmt.Errorf("%s listener users[%d]: unsupported field %q", proto, index, key)
}
}
if !hasString(row["username"]) {
return fmt.Errorf("%s listener users[%d] requires username", proto, index)
}
if uuidMode && !hasString(row["uuid"]) {
return fmt.Errorf("%s listener users[%d] requires uuid", proto, index)
}
if !uuidMode && !hasString(row["password"]) {
return fmt.Errorf("%s listener users[%d] requires password", proto, index)
}
return nil
}
// validateServerTLSModes enforces official Mihomo listener TLS exclusivity:
// certificate/private-key must appear together, and must not be combined with
// reality-config (or other exclusive TLS modes when present).
func validateServerTLSModes(proto string, cfg map[string]interface{}) error {
cert, key := hasString(cfg["certificate"]), hasString(cfg["private-key"])
if cert != key {
return fmt.Errorf("%s listener requires certificate and private-key together", proto)
}
if cert {
if _, hasReality := cfg["reality-config"]; hasReality {
return fmt.Errorf("%s listener mutually exclusive TLS modes: certificate/private-key cannot be used with reality-config", proto)
}
if _, hasMirror := cfg["tlsmirror-config"]; hasMirror {
return fmt.Errorf("%s listener mutually exclusive TLS modes: certificate/private-key cannot be used with tlsmirror-config", proto)
}
}
return nil
}
func hasCertificatePair(cfg map[string]interface{}) bool {
return hasString(cfg["certificate"]) && hasString(cfg["private-key"])
}
func hasString(value interface{}) bool {
s, ok := value.(string)
return ok && strings.TrimSpace(s) != ""
}
func hasNonEmpty(value interface{}) bool {
if value == nil {
return false
}
if s, ok := value.(string); ok {
return strings.TrimSpace(s) != ""
}
v := reflect.ValueOf(value)
switch v.Kind() {
case reflect.Map:
return v.Len() > 0
case reflect.Slice:
if v.Len() == 0 {
return false
}
// token: [""] must not count as configured
allEmpty := true
for i := 0; i < v.Len(); i++ {
elem := v.Index(i).Interface()
if s, ok := elem.(string); ok {
if strings.TrimSpace(s) != "" {
return true
}
} else if elem != nil {
allEmpty = false
}
}
return !allEmpty && v.Len() > 0
default:
return true
}
}
func boolValue(v interface{}) bool {
b, _ := v.(bool)
return b
}
func numeric(v interface{}) (float64, bool) {
n, ok := v.(float64)
return n, ok
}