From 2fef00d2a8f6bd7b54de52f1b2e3f6de2f84a85f Mon Sep 17 00:00:00 2001 From: 3andne <52860475+3andne@users.noreply.github.com> Date: Mon, 13 Mar 2023 22:33:24 -0700 Subject: [PATCH] Support Restls-V1 in Clash.Meta (#441) * feat: impl restls * fix: don't break shadowtls' working * chores: correct restls-client-go version * feat: bump restls-client-go version * fix: consistent naming `client-fingerprint` * docs: update example config * chore: adjust client-fingerprint's snippet --------- Co-authored-by: wwqgtxx Co-authored-by: Larvan2 <78135608+Larvan2@users.noreply.github.com> --- adapter/outbound/shadowsocks.go | 64 ++++++++++++++++++++++++--------- adapter/parser.go | 5 +++ docs/config.yaml | 40 ++++++++++++++++++++- go.mod | 1 + go.sum | 2 ++ transport/restls/restls.go | 57 +++++++++++++++++++++++++++++ 6 files changed, 152 insertions(+), 17 deletions(-) create mode 100644 transport/restls/restls.go diff --git a/adapter/outbound/shadowsocks.go b/adapter/outbound/shadowsocks.go index aab5cf42..49ff45ed 100644 --- a/adapter/outbound/shadowsocks.go +++ b/adapter/outbound/shadowsocks.go @@ -12,11 +12,13 @@ import ( "github.com/Dreamacro/clash/common/structure" "github.com/Dreamacro/clash/component/dialer" C "github.com/Dreamacro/clash/constant" + "github.com/Dreamacro/clash/transport/restls" obfs "github.com/Dreamacro/clash/transport/simple-obfs" shadowtls "github.com/Dreamacro/clash/transport/sing-shadowtls" "github.com/Dreamacro/clash/transport/socks5" v2rayObfs "github.com/Dreamacro/clash/transport/v2ray-plugin" + restlsC "github.com/3andne/restls-client-go" shadowsocks "github.com/metacubex/sing-shadowsocks" "github.com/metacubex/sing-shadowsocks/shadowimpl" "github.com/sagernet/sing/common/bufio" @@ -34,19 +36,21 @@ type ShadowSocks struct { obfsOption *simpleObfsOption v2rayOption *v2rayObfs.Option shadowTLSOption *shadowtls.ShadowTLSOption + restlsConfig *restlsC.Config } type ShadowSocksOption struct { BasicOption - Name string `proxy:"name"` - Server string `proxy:"server"` - Port int `proxy:"port"` - Password string `proxy:"password"` - Cipher string `proxy:"cipher"` - UDP bool `proxy:"udp,omitempty"` - Plugin string `proxy:"plugin,omitempty"` - PluginOpts map[string]any `proxy:"plugin-opts,omitempty"` - UDPOverTCP bool `proxy:"udp-over-tcp,omitempty"` + Name string `proxy:"name"` + Server string `proxy:"server"` + Port int `proxy:"port"` + Password string `proxy:"password"` + Cipher string `proxy:"cipher"` + UDP bool `proxy:"udp,omitempty"` + Plugin string `proxy:"plugin,omitempty"` + PluginOpts map[string]any `proxy:"plugin-opts,omitempty"` + UDPOverTCP bool `proxy:"udp-over-tcp,omitempty"` + ClientFingerprint string `proxy:"client-fingerprint,omitempty"` } type simpleObfsOption struct { @@ -66,12 +70,18 @@ type v2rayObfsOption struct { } type shadowTLSOption struct { - Password string `obfs:"password"` - Host string `obfs:"host"` - Fingerprint string `obfs:"fingerprint,omitempty"` - ClientFingerprint string `obfs:"client-fingerprint,omitempty"` - SkipCertVerify bool `obfs:"skip-cert-verify,omitempty"` - Version int `obfs:"version,omitempty"` + Password string `obfs:"password"` + Host string `obfs:"host"` + Fingerprint string `obfs:"fingerprint,omitempty"` + SkipCertVerify bool `obfs:"skip-cert-verify,omitempty"` + Version int `obfs:"version,omitempty"` +} + +type restlsOption struct { + Password string `obfs:"password"` + Host string `obfs:"host"` + VersionHint string `obfs:"version-hint"` + RestlsScript string `obfs:"restls-script,omitempty"` } // StreamConn implements C.ProxyAdapter @@ -86,6 +96,7 @@ func (ss *ShadowSocks) StreamConn(c net.Conn, metadata *C.Metadata) (net.Conn, e if err != nil { return nil, err } + } return ss.streamConn(c, metadata) } @@ -103,6 +114,12 @@ func (ss *ShadowSocks) streamConn(c net.Conn, metadata *C.Metadata) (net.Conn, e if err != nil { return nil, fmt.Errorf("%s connect error: %w", ss.addr, err) } + case restls.Mode: + var err error + c, err = restls.NewRestls(c, ss.restlsConfig) + if err != nil { + return nil, fmt.Errorf("%s (restls) connect error: %w", ss.addr, err) + } } if metadata.NetWork == C.UDP && ss.option.UDPOverTCP { if N.NeedHandshake(c) { @@ -202,6 +219,7 @@ func NewShadowSocks(option ShadowSocksOption) (*ShadowSocks, error) { var v2rayOption *v2rayObfs.Option var obfsOption *simpleObfsOption var shadowTLSOpt *shadowtls.ShadowTLSOption + var restlsConfig *restlsC.Config obfsMode := "" decoder := structure.NewDecoder(structure.Option{TagName: "obfs", WeaklyTypedInput: true}) @@ -250,10 +268,23 @@ func NewShadowSocks(option ShadowSocksOption) (*ShadowSocks, error) { Password: opt.Password, Host: opt.Host, Fingerprint: opt.Fingerprint, - ClientFingerprint: opt.ClientFingerprint, + ClientFingerprint: option.ClientFingerprint, SkipCertVerify: opt.SkipCertVerify, Version: opt.Version, } + } else if option.Plugin == restls.Mode { + obfsMode = restls.Mode + restlsOpt := &restlsOption{} + if err := decoder.Decode(option.PluginOpts, restlsOpt); err != nil { + return nil, fmt.Errorf("ss %s initialize restls-plugin error: %w", addr, err) + } + + restlsConfig, err = restlsC.NewRestlsConfig(restlsOpt.Host, restlsOpt.Password, restlsOpt.VersionHint, restlsOpt.RestlsScript, option.ClientFingerprint) + restlsConfig.SessionTicketsDisabled = true + if err != nil { + return nil, fmt.Errorf("ss %s initialize restls-plugin error: %w", addr, err) + } + } return &ShadowSocks{ @@ -274,6 +305,7 @@ func NewShadowSocks(option ShadowSocksOption) (*ShadowSocks, error) { v2rayOption: v2rayOption, obfsOption: obfsOption, shadowTLSOption: shadowTLSOpt, + restlsConfig: restlsConfig, }, nil } diff --git a/adapter/parser.go b/adapter/parser.go index d9c18694..65db74c4 100644 --- a/adapter/parser.go +++ b/adapter/parser.go @@ -24,6 +24,11 @@ func ParseProxy(mapping map[string]any) (C.Proxy, error) { switch proxyType { case "ss": ssOption := &outbound.ShadowSocksOption{} + + if GlobalUtlsClient := tlsC.GetGlobalFingerprint(); len(GlobalUtlsClient) != 0 { + ssOption.ClientFingerprint = GlobalUtlsClient + } + err = decoder.Decode(mapping, ssOption) if err != nil { break diff --git a/docs/config.yaml b/docs/config.yaml index e08a6e33..3ef09342 100644 --- a/docs/config.yaml +++ b/docs/config.yaml @@ -331,17 +331,55 @@ proxies: # socks5 # headers: # custom: value - - name: "ss4" + - name: "ss4-shadow-tls" type: ss server: server port: 443 cipher: chacha20-ietf-poly1305 password: "password" plugin: shadow-tls + client-fingerprint: chrome plugin-opts: host: "cloud.tencent.com" password: "shadow_tls_password" version: 2 # support 1/2/3 + + - name: "ss-restls-tls13" + type: ss + server: [YOUR_SERVER_IP] + port: 443 + cipher: chacha20-ietf-poly1305 + password: [YOUR_SS_PASSWORD] + client-fingerprint: chrome # One of: chrome, ios, firefox or safari + # 可以是chrome, ios, firefox, safari中的一个 + plugin: restls + plugin-opts: + host: "www.microsoft.com" # Must be a TLS 1.3 server + # 应当是一个TLS 1.3 服务器 + password: [YOUR_RESTLS_PASSWORD] + version-hint: "tls13" + # Control your post-handshake traffic through restls-script + # Hide proxy behaviors like "tls in tls". + # see https://github.com/3andne/restls/blob/main/Restls-Script:%20Hide%20Your%20Proxy%20Traffic%20Behavior.md + # 用restls剧本来控制握手后的行为,隐藏"tls in tls"等特征 + # 详情:https://github.com/3andne/restls/blob/main/Restls-Script:%20%E9%9A%90%E8%97%8F%E4%BD%A0%E7%9A%84%E4%BB%A3%E7%90%86%E8%A1%8C%E4%B8%BA.md + restls-script: "300?100<1,400~100,350~100,600~100,300~200,300~100" + + - name: "ss-restls-tls12" + type: ss + server: [YOUR_SERVER_IP] + port: 443 + cipher: chacha20-ietf-poly1305 + password: [YOUR_SS_PASSWORD] + client-fingerprint: chrome # One of: chrome, ios, firefox or safari + # 可以是chrome, ios, firefox, safari中的一个 + plugin: restls + plugin-opts: + host: "vscode.dev" # Must be a TLS 1.2 server + # 应当是一个TLS 1.2 服务器 + password: [YOUR_RESTLS_PASSWORD] + version-hint: "tls12" + restls-script: "1000?100<1,500~100,350~100,600~100,400~200" # vmess # cipher支持 auto/aes-128-gcm/chacha20-poly1305/none diff --git a/go.mod b/go.mod index bde63cc3..28760b44 100644 --- a/go.mod +++ b/go.mod @@ -51,6 +51,7 @@ require ( ) require ( + github.com/3andne/restls-client-go v0.1.4 github.com/ajg/form v1.5.1 // indirect github.com/andybalholm/brotli v1.0.5 // indirect github.com/davecgh/go-spew v1.1.1 // indirect diff --git a/go.sum b/go.sum index 7b24bc35..f9e584a2 100644 --- a/go.sum +++ b/go.sum @@ -1,3 +1,5 @@ +github.com/3andne/restls-client-go v0.1.4 h1:kLNC2aSRHPlEVYmTj6EOqJoorCpobEe2toMRSfBF7FU= +github.com/3andne/restls-client-go v0.1.4/go.mod h1:04CGbRk1BwBiEDles8b5mlKgTqIwE5MqF7JDloJV47I= github.com/aead/chacha20 v0.0.0-20180709150244-8b13a72661da h1:KjTM2ks9d14ZYCvmHS9iAKVt9AyzRSqNU1qabPih5BY= github.com/aead/chacha20 v0.0.0-20180709150244-8b13a72661da/go.mod h1:eHEWzANqSiWQsof+nXEI9bUVUyV6F53Fp89EuCh2EAA= github.com/ajg/form v1.5.1 h1:t9c7v8JUKu/XxOGBU0yjNpaMloxGEJhUkqFRq0ibGeU= diff --git a/transport/restls/restls.go b/transport/restls/restls.go new file mode 100644 index 00000000..4b03b825 --- /dev/null +++ b/transport/restls/restls.go @@ -0,0 +1,57 @@ +package restls + +import ( + "net" + + tls "github.com/3andne/restls-client-go" +) + +const ( + Mode string = "restls" +) + +// Restls +type Restls struct { + net.Conn + firstPacketCache []byte + firstPacket bool +} + +func (r *Restls) Read(b []byte) (int, error) { + if err := r.Conn.(*tls.UConn).Handshake(); err != nil { + return 0, err + } + n, err := r.Conn.(*tls.UConn).Read(b) + return n, err +} + +func (r *Restls) Write(b []byte) (int, error) { + if r.firstPacket { + r.firstPacketCache = append([]byte(nil), b...) + r.firstPacket = false + return len(b), nil + } + if len(r.firstPacketCache) != 0 { + b = append(r.firstPacketCache, b...) + r.firstPacketCache = nil + } + n, err := r.Conn.(*tls.UConn).Write(b) + return n, err +} + +// NewRestls return a Restls Connection +func NewRestls(conn net.Conn, config *tls.Config) (net.Conn, error) { + if config != nil { + clientIDPtr := config.ClientID.Load() + if clientIDPtr != nil { + return &Restls{ + Conn: tls.UClient(conn, config, *clientIDPtr), + firstPacket: true, + }, nil + } + } + return &Restls{ + Conn: tls.UClient(conn, config, tls.HelloChrome_Auto), + firstPacket: true, + }, nil +}