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 <wwqgtxx@gmail.com> Co-authored-by: Larvan2 <78135608+Larvan2@users.noreply.github.com>
This commit is contained in:
parent
f9be12aa93
commit
492a334eed
6 changed files with 152 additions and 17 deletions
|
@ -12,11 +12,13 @@ import (
|
||||||
"github.com/Dreamacro/clash/common/structure"
|
"github.com/Dreamacro/clash/common/structure"
|
||||||
"github.com/Dreamacro/clash/component/dialer"
|
"github.com/Dreamacro/clash/component/dialer"
|
||||||
C "github.com/Dreamacro/clash/constant"
|
C "github.com/Dreamacro/clash/constant"
|
||||||
|
"github.com/Dreamacro/clash/transport/restls"
|
||||||
obfs "github.com/Dreamacro/clash/transport/simple-obfs"
|
obfs "github.com/Dreamacro/clash/transport/simple-obfs"
|
||||||
shadowtls "github.com/Dreamacro/clash/transport/sing-shadowtls"
|
shadowtls "github.com/Dreamacro/clash/transport/sing-shadowtls"
|
||||||
"github.com/Dreamacro/clash/transport/socks5"
|
"github.com/Dreamacro/clash/transport/socks5"
|
||||||
v2rayObfs "github.com/Dreamacro/clash/transport/v2ray-plugin"
|
v2rayObfs "github.com/Dreamacro/clash/transport/v2ray-plugin"
|
||||||
|
|
||||||
|
restlsC "github.com/3andne/restls-client-go"
|
||||||
shadowsocks "github.com/metacubex/sing-shadowsocks"
|
shadowsocks "github.com/metacubex/sing-shadowsocks"
|
||||||
"github.com/metacubex/sing-shadowsocks/shadowimpl"
|
"github.com/metacubex/sing-shadowsocks/shadowimpl"
|
||||||
"github.com/sagernet/sing/common/bufio"
|
"github.com/sagernet/sing/common/bufio"
|
||||||
|
@ -34,19 +36,21 @@ type ShadowSocks struct {
|
||||||
obfsOption *simpleObfsOption
|
obfsOption *simpleObfsOption
|
||||||
v2rayOption *v2rayObfs.Option
|
v2rayOption *v2rayObfs.Option
|
||||||
shadowTLSOption *shadowtls.ShadowTLSOption
|
shadowTLSOption *shadowtls.ShadowTLSOption
|
||||||
|
restlsConfig *restlsC.Config
|
||||||
}
|
}
|
||||||
|
|
||||||
type ShadowSocksOption struct {
|
type ShadowSocksOption struct {
|
||||||
BasicOption
|
BasicOption
|
||||||
Name string `proxy:"name"`
|
Name string `proxy:"name"`
|
||||||
Server string `proxy:"server"`
|
Server string `proxy:"server"`
|
||||||
Port int `proxy:"port"`
|
Port int `proxy:"port"`
|
||||||
Password string `proxy:"password"`
|
Password string `proxy:"password"`
|
||||||
Cipher string `proxy:"cipher"`
|
Cipher string `proxy:"cipher"`
|
||||||
UDP bool `proxy:"udp,omitempty"`
|
UDP bool `proxy:"udp,omitempty"`
|
||||||
Plugin string `proxy:"plugin,omitempty"`
|
Plugin string `proxy:"plugin,omitempty"`
|
||||||
PluginOpts map[string]any `proxy:"plugin-opts,omitempty"`
|
PluginOpts map[string]any `proxy:"plugin-opts,omitempty"`
|
||||||
UDPOverTCP bool `proxy:"udp-over-tcp,omitempty"`
|
UDPOverTCP bool `proxy:"udp-over-tcp,omitempty"`
|
||||||
|
ClientFingerprint string `proxy:"client-fingerprint,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type simpleObfsOption struct {
|
type simpleObfsOption struct {
|
||||||
|
@ -66,12 +70,18 @@ type v2rayObfsOption struct {
|
||||||
}
|
}
|
||||||
|
|
||||||
type shadowTLSOption struct {
|
type shadowTLSOption struct {
|
||||||
Password string `obfs:"password"`
|
Password string `obfs:"password"`
|
||||||
Host string `obfs:"host"`
|
Host string `obfs:"host"`
|
||||||
Fingerprint string `obfs:"fingerprint,omitempty"`
|
Fingerprint string `obfs:"fingerprint,omitempty"`
|
||||||
ClientFingerprint string `obfs:"client-fingerprint,omitempty"`
|
SkipCertVerify bool `obfs:"skip-cert-verify,omitempty"`
|
||||||
SkipCertVerify bool `obfs:"skip-cert-verify,omitempty"`
|
Version int `obfs:"version,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
|
// StreamConn implements C.ProxyAdapter
|
||||||
|
@ -86,6 +96,7 @@ func (ss *ShadowSocks) StreamConn(c net.Conn, metadata *C.Metadata) (net.Conn, e
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
return ss.streamConn(c, metadata)
|
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 {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("%s connect error: %w", ss.addr, err)
|
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 metadata.NetWork == C.UDP && ss.option.UDPOverTCP {
|
||||||
if N.NeedHandshake(c) {
|
if N.NeedHandshake(c) {
|
||||||
|
@ -202,6 +219,7 @@ func NewShadowSocks(option ShadowSocksOption) (*ShadowSocks, error) {
|
||||||
var v2rayOption *v2rayObfs.Option
|
var v2rayOption *v2rayObfs.Option
|
||||||
var obfsOption *simpleObfsOption
|
var obfsOption *simpleObfsOption
|
||||||
var shadowTLSOpt *shadowtls.ShadowTLSOption
|
var shadowTLSOpt *shadowtls.ShadowTLSOption
|
||||||
|
var restlsConfig *restlsC.Config
|
||||||
obfsMode := ""
|
obfsMode := ""
|
||||||
|
|
||||||
decoder := structure.NewDecoder(structure.Option{TagName: "obfs", WeaklyTypedInput: true})
|
decoder := structure.NewDecoder(structure.Option{TagName: "obfs", WeaklyTypedInput: true})
|
||||||
|
@ -250,10 +268,23 @@ func NewShadowSocks(option ShadowSocksOption) (*ShadowSocks, error) {
|
||||||
Password: opt.Password,
|
Password: opt.Password,
|
||||||
Host: opt.Host,
|
Host: opt.Host,
|
||||||
Fingerprint: opt.Fingerprint,
|
Fingerprint: opt.Fingerprint,
|
||||||
ClientFingerprint: opt.ClientFingerprint,
|
ClientFingerprint: option.ClientFingerprint,
|
||||||
SkipCertVerify: opt.SkipCertVerify,
|
SkipCertVerify: opt.SkipCertVerify,
|
||||||
Version: opt.Version,
|
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{
|
return &ShadowSocks{
|
||||||
|
@ -274,6 +305,7 @@ func NewShadowSocks(option ShadowSocksOption) (*ShadowSocks, error) {
|
||||||
v2rayOption: v2rayOption,
|
v2rayOption: v2rayOption,
|
||||||
obfsOption: obfsOption,
|
obfsOption: obfsOption,
|
||||||
shadowTLSOption: shadowTLSOpt,
|
shadowTLSOption: shadowTLSOpt,
|
||||||
|
restlsConfig: restlsConfig,
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -24,6 +24,11 @@ func ParseProxy(mapping map[string]any) (C.Proxy, error) {
|
||||||
switch proxyType {
|
switch proxyType {
|
||||||
case "ss":
|
case "ss":
|
||||||
ssOption := &outbound.ShadowSocksOption{}
|
ssOption := &outbound.ShadowSocksOption{}
|
||||||
|
|
||||||
|
if GlobalUtlsClient := tlsC.GetGlobalFingerprint(); len(GlobalUtlsClient) != 0 {
|
||||||
|
ssOption.ClientFingerprint = GlobalUtlsClient
|
||||||
|
}
|
||||||
|
|
||||||
err = decoder.Decode(mapping, ssOption)
|
err = decoder.Decode(mapping, ssOption)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
break
|
break
|
||||||
|
|
|
@ -331,18 +331,56 @@ proxies: # socks5
|
||||||
# headers:
|
# headers:
|
||||||
# custom: value
|
# custom: value
|
||||||
|
|
||||||
- name: "ss4"
|
- name: "ss4-shadow-tls"
|
||||||
type: ss
|
type: ss
|
||||||
server: server
|
server: server
|
||||||
port: 443
|
port: 443
|
||||||
cipher: chacha20-ietf-poly1305
|
cipher: chacha20-ietf-poly1305
|
||||||
password: "password"
|
password: "password"
|
||||||
plugin: shadow-tls
|
plugin: shadow-tls
|
||||||
|
client-fingerprint: chrome
|
||||||
plugin-opts:
|
plugin-opts:
|
||||||
host: "cloud.tencent.com"
|
host: "cloud.tencent.com"
|
||||||
password: "shadow_tls_password"
|
password: "shadow_tls_password"
|
||||||
version: 2 # support 1/2/3
|
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
|
# vmess
|
||||||
# cipher支持 auto/aes-128-gcm/chacha20-poly1305/none
|
# cipher支持 auto/aes-128-gcm/chacha20-poly1305/none
|
||||||
- name: "vmess"
|
- name: "vmess"
|
||||||
|
|
1
go.mod
1
go.mod
|
@ -51,6 +51,7 @@ require (
|
||||||
)
|
)
|
||||||
|
|
||||||
require (
|
require (
|
||||||
|
github.com/3andne/restls-client-go v0.1.4
|
||||||
github.com/ajg/form v1.5.1 // indirect
|
github.com/ajg/form v1.5.1 // indirect
|
||||||
github.com/andybalholm/brotli v1.0.5 // indirect
|
github.com/andybalholm/brotli v1.0.5 // indirect
|
||||||
github.com/davecgh/go-spew v1.1.1 // indirect
|
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||||
|
|
2
go.sum
2
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 h1:KjTM2ks9d14ZYCvmHS9iAKVt9AyzRSqNU1qabPih5BY=
|
||||||
github.com/aead/chacha20 v0.0.0-20180709150244-8b13a72661da/go.mod h1:eHEWzANqSiWQsof+nXEI9bUVUyV6F53Fp89EuCh2EAA=
|
github.com/aead/chacha20 v0.0.0-20180709150244-8b13a72661da/go.mod h1:eHEWzANqSiWQsof+nXEI9bUVUyV6F53Fp89EuCh2EAA=
|
||||||
github.com/ajg/form v1.5.1 h1:t9c7v8JUKu/XxOGBU0yjNpaMloxGEJhUkqFRq0ibGeU=
|
github.com/ajg/form v1.5.1 h1:t9c7v8JUKu/XxOGBU0yjNpaMloxGEJhUkqFRq0ibGeU=
|
||||||
|
|
57
transport/restls/restls.go
Normal file
57
transport/restls/restls.go
Normal file
|
@ -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
|
||||||
|
}
|
Loading…
Reference in a new issue