@@ -128,8 +128,9 @@ type ClashProxy struct {
128128 Plugin string `yaml:"plugin"`
129129 PluginOpts map [string ]interface {} `yaml:"plugin-opts"`
130130
131- AuthStr string `yaml:"auth-str"`
132- Auth string `yaml:"auth"`
131+ AuthStr string `yaml:"auth-str"`
132+ AuthStrAlt string `yaml:"auth_str"`
133+ Auth string `yaml:"auth"`
133134 Up interface {} `yaml:"up"`
134135 Down interface {} `yaml:"down"`
135136
@@ -945,6 +946,9 @@ func clashHyToURI(p ClashProxy) string {
945946 return ""
946947 }
947948 auth := p .AuthStr
949+ if auth == "" {
950+ auth = p .AuthStrAlt
951+ }
948952 if auth == "" {
949953 auth = p .Auth
950954 }
@@ -964,6 +968,12 @@ func clashHyToURI(p ClashProxy) string {
964968 if len (p .ALPN ) > 0 {
965969 q .Set ("alpn" , strings .Join (p .ALPN , "," ))
966970 }
971+ if p .Obfs != "" {
972+ q .Set ("obfs" , p .Obfs )
973+ }
974+ if p .Protocol != "" {
975+ q .Set ("protocol" , p .Protocol )
976+ }
967977
968978 return fmt .Sprintf ("hy://%s@%s:%s?%s#%s" ,
969979 url .PathEscape (auth ), p .Server , portStr , q .Encode (), url .PathEscape (p .Name ))
@@ -986,6 +996,10 @@ func clashTUICToURI(p ClashProxy) string {
986996 if len (p .ALPN ) > 0 {
987997 q .Set ("alpn" , strings .Join (p .ALPN , "," ))
988998 }
999+ // Encode congestion-controller if set (non-default)
1000+ if congestion , ok := p .PluginOpts ["congestion-controller" ].(string ); ok && congestion != "" {
1001+ q .Set ("congestion_control" , congestion )
1002+ }
9891003
9901004 return fmt .Sprintf ("tuic://%s:%s@%s:%s?%s#%s" ,
9911005 url .PathEscape (p .UUID ), url .PathEscape (password ),
@@ -2736,8 +2750,14 @@ func parseHysteria2(raw string) (string, string) {
27362750 return "" , "missing server"
27372751 }
27382752 q , _ := url .ParseQuery (queryStr )
2739- return fmt .Sprintf (`{"type":"hysteria2","tag":"proxy","server":%q,"server_port":%d,"password":%q,"tls":{"enabled":true,"insecure":true,"server_name":%q}}` ,
2740- server , port , password , first (q .Get ("sni" ), server )), ""
2753+ obfsJSON := ""
2754+ if obfs := q .Get ("obfs" ); obfs == "salamander" {
2755+ if obfsPwd := q .Get ("obfs-password" ); obfsPwd != "" {
2756+ obfsJSON = fmt .Sprintf (`,"obfs":{"type":"salamander","password":%q}` , obfsPwd )
2757+ }
2758+ }
2759+ return fmt .Sprintf (`{"type":"hysteria2","tag":"proxy","server":%q,"server_port":%d,"password":%q%s,"tls":{"enabled":true,"insecure":true,"server_name":%q}}` ,
2760+ server , port , password , obfsJSON , first (q .Get ("sni" ), server )), ""
27412761}
27422762
27432763func parseHysteria (raw string ) (string , string ) {
@@ -2766,8 +2786,13 @@ func parseHysteria(raw string) (string, string) {
27662786 if down <= 0 {
27672787 down = 50
27682788 }
2769- return fmt .Sprintf (`{"type":"hysteria","tag":"proxy","server":%q,"server_port":%d,"up_mbps":%d,"down_mbps":%d,"auth_str":%q,"tls":{"enabled":true,"insecure":true,"server_name":%q}}` ,
2770- server , port , up , down , auth , first (q .Get ("peer" ), q .Get ("sni" ), server )), ""
2789+ obfs := q .Get ("obfs" )
2790+ obfsJSON := ""
2791+ if obfs != "" {
2792+ obfsJSON = fmt .Sprintf (`,"obfs":%q` , obfs )
2793+ }
2794+ return fmt .Sprintf (`{"type":"hysteria","tag":"proxy","server":%q,"server_port":%d,"up_mbps":%d,"down_mbps":%d,"auth_str":%q%s,"tls":{"enabled":true,"insecure":true,"server_name":%q}}` ,
2795+ server , port , up , down , auth , obfsJSON , first (q .Get ("peer" ), q .Get ("sni" ), server )), ""
27712796}
27722797
27732798func parseTUIC (raw string ) (string , string ) {
@@ -2788,8 +2813,16 @@ func parseTUIC(raw string) (string, string) {
27882813 if err != nil {
27892814 return "" , "port: " + err .Error ()
27902815 }
2791- return fmt .Sprintf (`{"type":"tuic","tag":"proxy","server":%q,"server_port":%d,"uuid":%q,"password":%q,"tls":{"enabled":true,"insecure":true,"server_name":%q}}` ,
2792- server , port , uuid , password , first (u .Query ().Get ("sni" ), server )), ""
2816+ q := u .Query ()
2817+ sni := first (q .Get ("sni" ), server )
2818+ congestion := first (q .Get ("congestion_control" ), q .Get ("congestion-controller" ), "bbr" )
2819+ udpRelayMode := q .Get ("udp_relay_mode" )
2820+ udpJSON := ""
2821+ if udpRelayMode != "" {
2822+ udpJSON = fmt .Sprintf (`,"udp_relay_mode":%q` , udpRelayMode )
2823+ }
2824+ return fmt .Sprintf (`{"type":"tuic","tag":"proxy","server":%q,"server_port":%d,"uuid":%q,"password":%q,"congestion_control":%q%s,"tls":{"enabled":true,"insecure":true,"server_name":%q}}` ,
2825+ server , port , uuid , password , congestion , udpJSON , sni ), ""
27932826}
27942827
27952828func parseSSR (raw string ) (string , string ) {
@@ -3202,6 +3235,16 @@ func trojanClashYAML(raw, name string) (string, bool) {
32023235
32033236func ssClashYAML (raw , name string ) (string , bool ) {
32043237 trimmed := strings .TrimPrefix (raw , "ss://" )
3238+ // Capture query string before stripping fragment
3239+ queryStr := ""
3240+ if idx := strings .Index (trimmed , "?" ); idx != - 1 {
3241+ qEnd := len (trimmed )
3242+ if fragIdx := strings .Index (trimmed [idx :], "#" ); fragIdx != - 1 {
3243+ qEnd = idx + fragIdx
3244+ }
3245+ queryStr = trimmed [idx + 1 : qEnd ]
3246+ trimmed = trimmed [:idx ] + trimmed [qEnd :]
3247+ }
32053248 if idx := strings .Index (trimmed , "#" ); idx != - 1 {
32063249 trimmed = trimmed [:idx ]
32073250 }
@@ -3249,9 +3292,66 @@ func ssClashYAML(raw, name string) (string, bool) {
32493292 var sb strings.Builder
32503293 fmt .Fprintf (& sb , " - name: %s\n type: ss\n server: %s\n port: %d\n cipher: %s\n password: %s\n udp: true\n " ,
32513294 yamlQuote (name ), yamlQuote (server ), port , yamlQuote (parts [0 ]), yamlQuote (parts [1 ]))
3295+
3296+ // Parse plugin info from query string
3297+ if queryStr != "" {
3298+ q , _ := url .ParseQuery (queryStr )
3299+ if pluginParam := q .Get ("plugin" ); pluginParam != "" {
3300+ // pluginParam format: "pluginName;opt1=val1;opt2=val2"
3301+ pluginParts := strings .SplitN (pluginParam , ";" , 2 )
3302+ pluginName := pluginParts [0 ]
3303+ // Map URI plugin names to Clash plugin names
3304+ switch {
3305+ case pluginName == "obfs-local" || pluginName == "obfs" :
3306+ fmt .Fprintf (& sb , " plugin: obfs\n plugin-opts:\n " )
3307+ if len (pluginParts ) > 1 {
3308+ opts := parsePluginOpts (pluginParts [1 ])
3309+ if mode , ok := opts ["obfs" ]; ok {
3310+ fmt .Fprintf (& sb , " mode: %s\n " , yamlQuote (mode ))
3311+ }
3312+ if host , ok := opts ["obfs-host" ]; ok {
3313+ fmt .Fprintf (& sb , " host: %s\n " , yamlQuote (host ))
3314+ }
3315+ }
3316+ case pluginName == "v2ray-plugin" :
3317+ fmt .Fprintf (& sb , " plugin: v2ray-plugin\n plugin-opts:\n " )
3318+ if len (pluginParts ) > 1 {
3319+ opts := parsePluginOpts (pluginParts [1 ])
3320+ mode := first (opts ["mode" ], "websocket" )
3321+ fmt .Fprintf (& sb , " mode: %s\n " , yamlQuote (mode ))
3322+ if path , ok := opts ["path" ]; ok {
3323+ fmt .Fprintf (& sb , " path: %s\n " , yamlQuote (path ))
3324+ }
3325+ if host , ok := opts ["host" ]; ok {
3326+ fmt .Fprintf (& sb , " host: %s\n " , yamlQuote (host ))
3327+ }
3328+ if _ , hasTLS := opts ["tls" ]; hasTLS {
3329+ fmt .Fprintf (& sb , " tls: true\n " )
3330+ }
3331+ }
3332+ }
3333+ }
3334+ }
32523335 return sb .String (), true
32533336}
32543337
3338+ // parsePluginOpts parses "key=val;key2=val2;flag" into a map.
3339+ func parsePluginOpts (s string ) map [string ]string {
3340+ opts := make (map [string ]string )
3341+ for _ , part := range strings .Split (s , ";" ) {
3342+ part = strings .TrimSpace (part )
3343+ if part == "" {
3344+ continue
3345+ }
3346+ if idx := strings .Index (part , "=" ); idx != - 1 {
3347+ opts [part [:idx ]] = part [idx + 1 :]
3348+ } else {
3349+ opts [part ] = "" // flag without value (e.g. "tls")
3350+ }
3351+ }
3352+ return opts
3353+ }
3354+
32553355func hy2ClashYAML (raw , name string ) (string , bool ) {
32563356 trimmed := strings .TrimPrefix (raw , "hy2://" )
32573357 if i := strings .LastIndex (trimmed , "#" ); i != - 1 {
@@ -3287,6 +3387,12 @@ func hy2ClashYAML(raw, name string) (string, bool) {
32873387 var sb strings.Builder
32883388 fmt .Fprintf (& sb , " - name: %s\n type: hysteria2\n server: %s\n port: %d\n password: %s\n sni: %s\n skip-cert-verify: true\n udp: true\n " ,
32893389 yamlQuote (name ), yamlQuote (server ), port , yamlQuote (password ), yamlQuote (first (q .Get ("sni" ), server )))
3390+ if obfs := q .Get ("obfs" ); obfs != "" {
3391+ fmt .Fprintf (& sb , " obfs: %s\n " , yamlQuote (obfs ))
3392+ if obfsPwd := q .Get ("obfs-password" ); obfsPwd != "" {
3393+ fmt .Fprintf (& sb , " obfs-password: %s\n " , yamlQuote (obfsPwd ))
3394+ }
3395+ }
32903396 return sb .String (), true
32913397}
32923398
@@ -3320,6 +3426,20 @@ func hyClashYAML(raw, name string) (string, bool) {
33203426 fmt .Fprintf (& sb , " - name: %s\n type: hysteria\n server: %s\n port: %d\n auth-str: %s\n up: %d\n down: %d\n sni: %s\n skip-cert-verify: true\n udp: true\n " ,
33213427 yamlQuote (name ), yamlQuote (server ), port , yamlQuote (auth ), up , down ,
33223428 yamlQuote (first (q .Get ("peer" ), q .Get ("sni" ), server )))
3429+ if obfs := q .Get ("obfs" ); obfs != "" {
3430+ fmt .Fprintf (& sb , " obfs: %s\n " , yamlQuote (obfs ))
3431+ }
3432+ if proto := q .Get ("protocol" ); proto != "" {
3433+ fmt .Fprintf (& sb , " protocol: %s\n " , yamlQuote (proto ))
3434+ }
3435+ if alpnStr := q .Get ("alpn" ); alpnStr != "" {
3436+ parts := strings .Split (alpnStr , "," )
3437+ quoted := make ([]string , len (parts ))
3438+ for i , a := range parts {
3439+ quoted [i ] = yamlQuote (strings .TrimSpace (a ))
3440+ }
3441+ fmt .Fprintf (& sb , " alpn: [%s]\n " , strings .Join (quoted , ", " ))
3442+ }
33233443 return sb .String (), true
33243444}
33253445
@@ -3335,10 +3455,15 @@ func tuicClashYAML(raw, name string) (string, bool) {
33353455 if err != nil || uuid == "" || server == "" {
33363456 return "" , false
33373457 }
3458+ q := u .Query ()
3459+ congestion := first (q .Get ("congestion_control" ), q .Get ("congestion-controller" ), "bbr" )
33383460 var sb strings.Builder
3339- fmt .Fprintf (& sb , " - name: %s\n type: tuic\n server: %s\n port: %d\n uuid: %s\n password: %s\n sni: %s\n skip-cert-verify: true\n udp: true\n congestion-controller: bbr \n reduce-rtt: true\n " ,
3461+ fmt .Fprintf (& sb , " - name: %s\n type: tuic\n server: %s\n port: %d\n uuid: %s\n password: %s\n sni: %s\n skip-cert-verify: true\n udp: true\n congestion-controller: %s \n reduce-rtt: true\n " ,
33403462 yamlQuote (name ), yamlQuote (server ), port , yamlQuote (uuid ), yamlQuote (password ),
3341- yamlQuote (first (u .Query ().Get ("sni" ), server )))
3463+ yamlQuote (first (q .Get ("sni" ), server )), congestion )
3464+ if udpRelay := first (q .Get ("udp_relay_mode" ), q .Get ("udp-relay-mode" )); udpRelay != "" {
3465+ fmt .Fprintf (& sb , " udp-relay-mode: %s\n " , yamlQuote (udpRelay ))
3466+ }
33423467 return sb .String (), true
33433468}
33443469
0 commit comments