feat: Support ShadowTLS v2 as Shadowsocks plugin (#330)
This commit is contained in:
parent
337be9124f
commit
51f9b34a7c
7 changed files with 206 additions and 20 deletions
11
.github/workflows/prerelease.yml
vendored
11
.github/workflows/prerelease.yml
vendored
|
@ -36,14 +36,16 @@ jobs:
|
||||||
run: make -j$(($(nproc) + 1)) releases
|
run: make -j$(($(nproc) + 1)) releases
|
||||||
|
|
||||||
- name: Delete current release assets
|
- name: Delete current release assets
|
||||||
uses: andreaswilli/delete-release-assets-action@v2.0.0
|
uses: mknejp/delete-release-assets@v1
|
||||||
with:
|
with:
|
||||||
github_token: ${{ secrets.GITHUB_TOKEN }}
|
github_token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
tag: Prerelease-${{ github.ref_name }}
|
tag_name: Prerelease-${{ github.ref_name }}
|
||||||
deleteOnlyFromDrafts: false
|
assets: |
|
||||||
|
*.zip
|
||||||
|
*.gz
|
||||||
|
|
||||||
- name: Tag Repo
|
- name: Tag Repo
|
||||||
uses: richardsimko/update-tag@v1
|
uses: richardsimko/update-tag@v1.0.6
|
||||||
with:
|
with:
|
||||||
tag_name: Prerelease-${{ github.ref_name }}
|
tag_name: Prerelease-${{ github.ref_name }}
|
||||||
env:
|
env:
|
||||||
|
@ -53,7 +55,6 @@ jobs:
|
||||||
uses: softprops/action-gh-release@v1
|
uses: softprops/action-gh-release@v1
|
||||||
if: ${{ success() }}
|
if: ${{ success() }}
|
||||||
with:
|
with:
|
||||||
tag: ${{ github.ref_name }}
|
|
||||||
tag_name: Prerelease-${{ github.ref_name }}
|
tag_name: Prerelease-${{ github.ref_name }}
|
||||||
files: bin/*
|
files: bin/*
|
||||||
prerelease: true
|
prerelease: true
|
||||||
|
|
20
README.md
20
README.md
|
@ -34,6 +34,26 @@ Documentations are now moved to [GitHub Wiki](https://github.com/Dreamacro/clash
|
||||||
|
|
||||||
## Advanced usage for this branch
|
## Advanced usage for this branch
|
||||||
|
|
||||||
|
## Build
|
||||||
|
|
||||||
|
You should install [golang](https://go.dev) first.
|
||||||
|
|
||||||
|
Then get the source code of Clash.Meta:
|
||||||
|
```shell
|
||||||
|
git clone https://github.com/MetaCubeX/Clash.Meta.git
|
||||||
|
cd Clash.Meta && go mod download
|
||||||
|
```
|
||||||
|
|
||||||
|
If you can't visit github,you should set proxy first:
|
||||||
|
```shell
|
||||||
|
go env -w GOPROXY=https://goproxy.io,direct
|
||||||
|
```
|
||||||
|
|
||||||
|
So now you can build it:
|
||||||
|
```shell
|
||||||
|
go build
|
||||||
|
```
|
||||||
|
|
||||||
### DNS configuration
|
### DNS configuration
|
||||||
|
|
||||||
Support `geosite` with `fallback-filter`.
|
Support `geosite` with `fallback-filter`.
|
||||||
|
|
|
@ -10,11 +10,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/shadowtls"
|
||||||
obfs "github.com/Dreamacro/clash/transport/simple-obfs"
|
obfs "github.com/Dreamacro/clash/transport/simple-obfs"
|
||||||
"github.com/Dreamacro/clash/transport/socks5"
|
"github.com/Dreamacro/clash/transport/socks5"
|
||||||
|
"github.com/Dreamacro/clash/transport/trojan"
|
||||||
v2rayObfs "github.com/Dreamacro/clash/transport/v2ray-plugin"
|
v2rayObfs "github.com/Dreamacro/clash/transport/v2ray-plugin"
|
||||||
|
|
||||||
"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"
|
||||||
M "github.com/sagernet/sing/common/metadata"
|
M "github.com/sagernet/sing/common/metadata"
|
||||||
|
@ -27,9 +29,11 @@ type ShadowSocks struct {
|
||||||
|
|
||||||
option *ShadowSocksOption
|
option *ShadowSocksOption
|
||||||
// obfs
|
// obfs
|
||||||
obfsMode string
|
obfsMode string
|
||||||
obfsOption *simpleObfsOption
|
obfsOption *simpleObfsOption
|
||||||
v2rayOption *v2rayObfs.Option
|
v2rayOption *v2rayObfs.Option
|
||||||
|
shadowTLSOption *shadowTLSOption
|
||||||
|
tlsConnector *trojan.Trojan
|
||||||
}
|
}
|
||||||
|
|
||||||
type ShadowSocksOption struct {
|
type ShadowSocksOption struct {
|
||||||
|
@ -61,6 +65,11 @@ type v2rayObfsOption struct {
|
||||||
Mux bool `obfs:"mux,omitempty"`
|
Mux bool `obfs:"mux,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type shadowTLSOption struct {
|
||||||
|
Password string `obfs:"password"`
|
||||||
|
Host string `obfs:"host"`
|
||||||
|
}
|
||||||
|
|
||||||
// StreamConn implements C.ProxyAdapter
|
// StreamConn implements C.ProxyAdapter
|
||||||
func (ss *ShadowSocks) StreamConn(c net.Conn, metadata *C.Metadata) (net.Conn, error) {
|
func (ss *ShadowSocks) StreamConn(c net.Conn, metadata *C.Metadata) (net.Conn, error) {
|
||||||
switch ss.obfsMode {
|
switch ss.obfsMode {
|
||||||
|
@ -75,6 +84,11 @@ 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 shadowtls.Mode:
|
||||||
|
if ss.tlsConnector == nil {
|
||||||
|
ss.tlsConnector = trojan.New(&trojan.Option{ServerName: ss.shadowTLSOption.Host, SkipCertVerify: false})
|
||||||
|
}
|
||||||
|
c = shadowtls.NewShadowTls(c, ss.shadowTLSOption.Password, ss.tlsConnector)
|
||||||
}
|
}
|
||||||
if metadata.NetWork == C.UDP && ss.option.UDPOverTCP {
|
if metadata.NetWork == C.UDP && ss.option.UDPOverTCP {
|
||||||
return ss.method.DialConn(c, M.ParseSocksaddr(uot.UOTMagicAddress+":443"))
|
return ss.method.DialConn(c, M.ParseSocksaddr(uot.UOTMagicAddress+":443"))
|
||||||
|
@ -157,6 +171,7 @@ func NewShadowSocks(option ShadowSocksOption) (*ShadowSocks, error) {
|
||||||
|
|
||||||
var v2rayOption *v2rayObfs.Option
|
var v2rayOption *v2rayObfs.Option
|
||||||
var obfsOption *simpleObfsOption
|
var obfsOption *simpleObfsOption
|
||||||
|
var shadowTLSOpt *shadowTLSOption
|
||||||
obfsMode := ""
|
obfsMode := ""
|
||||||
|
|
||||||
decoder := structure.NewDecoder(structure.Option{TagName: "obfs", WeaklyTypedInput: true})
|
decoder := structure.NewDecoder(structure.Option{TagName: "obfs", WeaklyTypedInput: true})
|
||||||
|
@ -192,6 +207,12 @@ func NewShadowSocks(option ShadowSocksOption) (*ShadowSocks, error) {
|
||||||
v2rayOption.TLS = true
|
v2rayOption.TLS = true
|
||||||
v2rayOption.SkipCertVerify = opts.SkipCertVerify
|
v2rayOption.SkipCertVerify = opts.SkipCertVerify
|
||||||
}
|
}
|
||||||
|
} else if option.Plugin == shadowtls.Mode {
|
||||||
|
obfsMode = shadowtls.Mode
|
||||||
|
shadowTLSOpt = &shadowTLSOption{}
|
||||||
|
if err := decoder.Decode(option.PluginOpts, shadowTLSOpt); err != nil {
|
||||||
|
return nil, fmt.Errorf("ss %s initialize shadow-tls-plugin error: %w", addr, err)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return &ShadowSocks{
|
return &ShadowSocks{
|
||||||
|
@ -206,10 +227,11 @@ func NewShadowSocks(option ShadowSocksOption) (*ShadowSocks, error) {
|
||||||
},
|
},
|
||||||
method: method,
|
method: method,
|
||||||
|
|
||||||
option: &option,
|
option: &option,
|
||||||
obfsMode: obfsMode,
|
obfsMode: obfsMode,
|
||||||
v2rayOption: v2rayOption,
|
v2rayOption: v2rayOption,
|
||||||
obfsOption: obfsOption,
|
obfsOption: obfsOption,
|
||||||
|
shadowTLSOption: shadowTLSOpt,
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -28,7 +28,7 @@
|
||||||
inherit version;
|
inherit version;
|
||||||
src = ./.;
|
src = ./.;
|
||||||
|
|
||||||
vendorSha256 = "sha256-XVz2vts4on42lfxnov4jnUrHzSFF05+i1TVY3C7bgdw=";
|
vendorSha256 = "sha256-8cbcE9gKJjU14DNTLPc6nneEPZg7Akt+FlSDlPRvG5k=";
|
||||||
|
|
||||||
# Do not build testing suit
|
# Do not build testing suit
|
||||||
excludedPackages = [ "./test" ];
|
excludedPackages = [ "./test" ];
|
||||||
|
|
142
transport/shadowtls/shadowtls.go
Normal file
142
transport/shadowtls/shadowtls.go
Normal file
|
@ -0,0 +1,142 @@
|
||||||
|
package shadowtls
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/hmac"
|
||||||
|
"crypto/sha1"
|
||||||
|
"encoding/binary"
|
||||||
|
"fmt"
|
||||||
|
"hash"
|
||||||
|
"io"
|
||||||
|
"net"
|
||||||
|
|
||||||
|
"github.com/Dreamacro/clash/common/pool"
|
||||||
|
"github.com/Dreamacro/clash/transport/trojan"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
chunkSize = 1 << 13
|
||||||
|
Mode string = "shadow-tls"
|
||||||
|
hashLen int = 8
|
||||||
|
tlsHeaderLen int = 5
|
||||||
|
)
|
||||||
|
|
||||||
|
// TLSObfs is shadowsocks tls simple-obfs implementation
|
||||||
|
type ShadowTls struct {
|
||||||
|
net.Conn
|
||||||
|
password []byte
|
||||||
|
remain int
|
||||||
|
firstRequest bool
|
||||||
|
tlsConnector *trojan.Trojan
|
||||||
|
}
|
||||||
|
|
||||||
|
type HashedConn struct {
|
||||||
|
net.Conn
|
||||||
|
hasher hash.Hash
|
||||||
|
}
|
||||||
|
|
||||||
|
func newHashedStream(conn net.Conn, password []byte) HashedConn {
|
||||||
|
return HashedConn{
|
||||||
|
Conn: conn,
|
||||||
|
hasher: hmac.New(sha1.New, password),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h HashedConn) Read(b []byte) (n int, err error) {
|
||||||
|
n, err = h.Conn.Read(b)
|
||||||
|
h.hasher.Write(b[:n])
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func (to *ShadowTls) read(b []byte) (int, error) {
|
||||||
|
buf := pool.Get(tlsHeaderLen)
|
||||||
|
_, err := io.ReadFull(to.Conn, buf)
|
||||||
|
if err != nil {
|
||||||
|
return 0, fmt.Errorf("shadowtls read failed %w", err)
|
||||||
|
}
|
||||||
|
if buf[0] != 0x17 || buf[1] != 0x3 || buf[2] != 0x3 {
|
||||||
|
return 0, fmt.Errorf("invalid shadowtls header %v", buf)
|
||||||
|
}
|
||||||
|
length := int(binary.BigEndian.Uint16(buf[3:]))
|
||||||
|
pool.Put(buf)
|
||||||
|
|
||||||
|
if length > len(b) {
|
||||||
|
n, err := to.Conn.Read(b)
|
||||||
|
if err != nil {
|
||||||
|
return n, err
|
||||||
|
}
|
||||||
|
to.remain = length - n
|
||||||
|
return n, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return io.ReadFull(to.Conn, b[:length])
|
||||||
|
}
|
||||||
|
|
||||||
|
func (to *ShadowTls) Read(b []byte) (int, error) {
|
||||||
|
if to.remain > 0 {
|
||||||
|
length := to.remain
|
||||||
|
if length > len(b) {
|
||||||
|
length = len(b)
|
||||||
|
}
|
||||||
|
|
||||||
|
n, err := io.ReadFull(to.Conn, b[:length])
|
||||||
|
if err != nil {
|
||||||
|
return n, fmt.Errorf("shadowtls Read failed with %w", err)
|
||||||
|
}
|
||||||
|
to.remain -= n
|
||||||
|
return n, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return to.read(b)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (to *ShadowTls) Write(b []byte) (int, error) {
|
||||||
|
length := len(b)
|
||||||
|
for i := 0; i < length; i += chunkSize {
|
||||||
|
end := i + chunkSize
|
||||||
|
if end > length {
|
||||||
|
end = length
|
||||||
|
}
|
||||||
|
|
||||||
|
n, err := to.write(b[i:end])
|
||||||
|
if err != nil {
|
||||||
|
return n, fmt.Errorf("shadowtls Write failed with %w, i=%d, end=%d, n=%d", err, i, end, n)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return length, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *ShadowTls) write(b []byte) (int, error) {
|
||||||
|
var hashVal []byte
|
||||||
|
if s.firstRequest {
|
||||||
|
hashedConn := newHashedStream(s.Conn, s.password)
|
||||||
|
if _, err := s.tlsConnector.StreamConn(hashedConn); err != nil {
|
||||||
|
return 0, fmt.Errorf("tls connect failed with %w", err)
|
||||||
|
}
|
||||||
|
hashVal = hashedConn.hasher.Sum(nil)[:hashLen]
|
||||||
|
s.firstRequest = false
|
||||||
|
}
|
||||||
|
|
||||||
|
buf := pool.GetBuffer()
|
||||||
|
defer pool.PutBuffer(buf)
|
||||||
|
buf.Write([]byte{0x17, 0x03, 0x03})
|
||||||
|
binary.Write(buf, binary.BigEndian, uint16(len(b)+len(hashVal)))
|
||||||
|
buf.Write(hashVal)
|
||||||
|
buf.Write(b)
|
||||||
|
_, err := s.Conn.Write(buf.Bytes())
|
||||||
|
if err != nil {
|
||||||
|
// return 0 because errors occur here make the
|
||||||
|
// whole situation irrecoverable
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
return len(b), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewShadowTls return a ShadowTls
|
||||||
|
func NewShadowTls(conn net.Conn, password string, tlsConnector *trojan.Trojan) net.Conn {
|
||||||
|
return &ShadowTls{
|
||||||
|
Conn: conn,
|
||||||
|
password: []byte(password),
|
||||||
|
firstRequest: true,
|
||||||
|
tlsConnector: tlsConnector,
|
||||||
|
}
|
||||||
|
}
|
|
@ -109,7 +109,12 @@ func (to *TLSObfs) write(b []byte) (int, error) {
|
||||||
binary.Write(buf, binary.BigEndian, uint16(len(b)))
|
binary.Write(buf, binary.BigEndian, uint16(len(b)))
|
||||||
buf.Write(b)
|
buf.Write(b)
|
||||||
_, err := to.Conn.Write(buf.Bytes())
|
_, err := to.Conn.Write(buf.Bytes())
|
||||||
return len(b), err
|
if err != nil {
|
||||||
|
// return 0 because errors occur here make the
|
||||||
|
// whole situation irrecoverable
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
return len(b), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewTLSObfs return a SimpleObfs
|
// NewTLSObfs return a SimpleObfs
|
||||||
|
|
|
@ -8,18 +8,17 @@ import (
|
||||||
"encoding/hex"
|
"encoding/hex"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
tlsC "github.com/Dreamacro/clash/component/tls"
|
|
||||||
"io"
|
"io"
|
||||||
"net"
|
"net"
|
||||||
"net/http"
|
"net/http"
|
||||||
"sync"
|
"sync"
|
||||||
|
|
||||||
"github.com/Dreamacro/clash/common/pool"
|
"github.com/Dreamacro/clash/common/pool"
|
||||||
|
tlsC "github.com/Dreamacro/clash/component/tls"
|
||||||
C "github.com/Dreamacro/clash/constant"
|
C "github.com/Dreamacro/clash/constant"
|
||||||
"github.com/Dreamacro/clash/transport/socks5"
|
"github.com/Dreamacro/clash/transport/socks5"
|
||||||
"github.com/Dreamacro/clash/transport/vless"
|
"github.com/Dreamacro/clash/transport/vless"
|
||||||
"github.com/Dreamacro/clash/transport/vmess"
|
"github.com/Dreamacro/clash/transport/vmess"
|
||||||
|
|
||||||
xtls "github.com/xtls/go"
|
xtls "github.com/xtls/go"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -117,9 +116,6 @@ func (t *Trojan) StreamConn(conn net.Conn) (net.Conn, error) {
|
||||||
}
|
}
|
||||||
|
|
||||||
tlsConn := tls.Client(conn, tlsConfig)
|
tlsConn := tls.Client(conn, tlsConfig)
|
||||||
if err := tlsConn.Handshake(); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
// fix tls handshake not timeout
|
// fix tls handshake not timeout
|
||||||
ctx, cancel := context.WithTimeout(context.Background(), C.DefaultTLSTimeout)
|
ctx, cancel := context.WithTimeout(context.Background(), C.DefaultTLSTimeout)
|
||||||
defer cancel()
|
defer cancel()
|
||||||
|
|
Loading…
Reference in a new issue