From 8e51a547b960192e0a0afb80c34ec097f20f6a6c Mon Sep 17 00:00:00 2001 From: XYenon Date: Wed, 12 Nov 2025 15:58:29 +0800 Subject: [PATCH] Add AnyTLS protocol support to Clash subscription parser MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Added parsing support for the AnyTLS proxy type in Clash subscriptions, including: - AnyTLS outbound options with server, password, and TLS configuration - ECH (Encrypted Client Hello) options parsing - UTLS (uTLS) fingerprint configuration - Idle session management parameters This allows Serenity to import AnyTLS proxies from Clash-format subscription sources. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- subscription/parser/clash.go | 73 ++++++++++++++++++++++++++++++++++++ 1 file changed, 73 insertions(+) diff --git a/subscription/parser/clash.go b/subscription/parser/clash.go index 9e43cf9..e856b99 100644 --- a/subscription/parser/clash.go +++ b/subscription/parser/clash.go @@ -2,7 +2,10 @@ package parser import ( "context" + "encoding/base64" + "encoding/pem" "strings" + "time" C "github.com/sagernet/sing-box/constant" "github.com/sagernet/sing-box/option" @@ -162,6 +165,41 @@ func ParseClashSubscription(_ context.Context, content string) ([]option.Outboun Username: httpOption.UserName, Password: httpOption.Password, } + case constant.AnyTLS: + anytlsOption := &clash_outbound.AnyTLSOption{} + err = decoder.Decode(proxyMapping, anytlsOption) + if err != nil { + return nil, err + } + echOptions, err := clashECH(anytlsOption.ECHOpts) + if err != nil { + return nil, E.Cause(err, "parse ECH options for proxy ", i) + } + outbound.Type = C.TypeAnyTLS + outbound.Options = &option.AnyTLSOutboundOptions{ + ServerOptions: option.ServerOptions{ + Server: anytlsOption.Server, + ServerPort: uint16(anytlsOption.Port), + }, + Password: anytlsOption.Password, + OutboundTLSOptionsContainer: option.OutboundTLSOptionsContainer{ + TLS: &option.OutboundTLSOptions{ + Enabled: true, + ALPN: anytlsOption.ALPN, + ServerName: anytlsOption.SNI, + Insecure: anytlsOption.SkipCertVerify, + ECH: echOptions, + UTLS: clashUTLS(anytlsOption.ClientFingerprint), + CertificatePath: anytlsOption.Fingerprint, + }, + }, + IdleSessionCheckInterval: badoption.Duration(time.Duration(anytlsOption.IdleSessionCheckInterval) * time.Second), + IdleSessionTimeout: badoption.Duration(time.Duration(anytlsOption.IdleSessionTimeout) * time.Second), + MinIdleSession: anytlsOption.MinIdleSession, + } + default: + // Skip unsupported proxy types + continue } outbounds = append(outbounds, outbound) } @@ -283,3 +321,38 @@ func clashStringList(list []string) string { } return "" } + +func clashECH(echOpts clash_outbound.ECHOptions) (*option.OutboundECHOptions, error) { + if !echOpts.Enable { + return nil, nil + } + echOptions := &option.OutboundECHOptions{ + Enabled: true, + } + if echOpts.Config != "" { + // Decode base64 ECH config from Clash + decoded, err := base64.StdEncoding.DecodeString(echOpts.Config) + if err != nil { + return nil, E.Cause(err, "decode ECH config from base64") + } + + // Convert to PEM format for sing-box + pemBlock := &pem.Block{ + Type: "ECH CONFIGS", + Bytes: decoded, + } + pemBytes := pem.EncodeToMemory(pemBlock) + echOptions.Config = []string{string(pemBytes)} + } + return echOptions, nil +} + +func clashUTLS(fingerprint string) *option.OutboundUTLSOptions { + if fingerprint == "" { + return nil + } + return &option.OutboundUTLSOptions{ + Enabled: true, + Fingerprint: fingerprint, + } +}