diff --git a/.github/workflows/prerelease.yml b/.github/workflows/prerelease.yml index 4a792eaa..c20efb0b 100644 --- a/.github/workflows/prerelease.yml +++ b/.github/workflows/prerelease.yml @@ -36,14 +36,16 @@ jobs: run: make -j$(($(nproc) + 1)) releases - name: Delete current release assets - uses: andreaswilli/delete-release-assets-action@v2.0.0 + uses: mknejp/delete-release-assets@v1 with: github_token: ${{ secrets.GITHUB_TOKEN }} - tag: Prerelease-${{ github.ref_name }} - deleteOnlyFromDrafts: false + tag_name: Prerelease-${{ github.ref_name }} + assets: | + *.zip + *.gz - name: Tag Repo - uses: richardsimko/update-tag@v1 + uses: richardsimko/update-tag@v1.0.6 with: tag_name: Prerelease-${{ github.ref_name }} env: @@ -53,7 +55,6 @@ jobs: uses: softprops/action-gh-release@v1 if: ${{ success() }} with: - tag: ${{ github.ref_name }} tag_name: Prerelease-${{ github.ref_name }} files: bin/* prerelease: true diff --git a/README.md b/README.md index b62e212d..321ce97e 100644 --- a/README.md +++ b/README.md @@ -34,6 +34,26 @@ Documentations are now moved to [GitHub Wiki](https://github.com/Dreamacro/clash ## 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 Support `geosite` with `fallback-filter`. diff --git a/adapter/outbound/shadowsocks.go b/adapter/outbound/shadowsocks.go index c318d263..736bb5bd 100644 --- a/adapter/outbound/shadowsocks.go +++ b/adapter/outbound/shadowsocks.go @@ -10,11 +10,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/shadowtls" obfs "github.com/Dreamacro/clash/transport/simple-obfs" "github.com/Dreamacro/clash/transport/socks5" + "github.com/Dreamacro/clash/transport/trojan" 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/sagernet/sing/common/bufio" M "github.com/sagernet/sing/common/metadata" @@ -27,9 +29,11 @@ type ShadowSocks struct { option *ShadowSocksOption // obfs - obfsMode string - obfsOption *simpleObfsOption - v2rayOption *v2rayObfs.Option + obfsMode string + obfsOption *simpleObfsOption + v2rayOption *v2rayObfs.Option + shadowTLSOption *shadowTLSOption + tlsConnector *trojan.Trojan } type ShadowSocksOption struct { @@ -61,6 +65,11 @@ type v2rayObfsOption struct { Mux bool `obfs:"mux,omitempty"` } +type shadowTLSOption struct { + Password string `obfs:"password"` + Host string `obfs:"host"` +} + // StreamConn implements C.ProxyAdapter func (ss *ShadowSocks) StreamConn(c net.Conn, metadata *C.Metadata) (net.Conn, error) { switch ss.obfsMode { @@ -75,6 +84,11 @@ 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 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 { 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 obfsOption *simpleObfsOption + var shadowTLSOpt *shadowTLSOption obfsMode := "" decoder := structure.NewDecoder(structure.Option{TagName: "obfs", WeaklyTypedInput: true}) @@ -192,6 +207,12 @@ func NewShadowSocks(option ShadowSocksOption) (*ShadowSocks, error) { v2rayOption.TLS = true 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{ @@ -206,10 +227,11 @@ func NewShadowSocks(option ShadowSocksOption) (*ShadowSocks, error) { }, method: method, - option: &option, - obfsMode: obfsMode, - v2rayOption: v2rayOption, - obfsOption: obfsOption, + option: &option, + obfsMode: obfsMode, + v2rayOption: v2rayOption, + obfsOption: obfsOption, + shadowTLSOption: shadowTLSOpt, }, nil } diff --git a/flake.nix b/flake.nix index c372ff4b..88a6eacb 100644 --- a/flake.nix +++ b/flake.nix @@ -28,7 +28,7 @@ inherit version; src = ./.; - vendorSha256 = "sha256-XVz2vts4on42lfxnov4jnUrHzSFF05+i1TVY3C7bgdw="; + vendorSha256 = "sha256-8cbcE9gKJjU14DNTLPc6nneEPZg7Akt+FlSDlPRvG5k="; # Do not build testing suit excludedPackages = [ "./test" ]; diff --git a/transport/shadowtls/shadowtls.go b/transport/shadowtls/shadowtls.go new file mode 100644 index 00000000..4a0db6a9 --- /dev/null +++ b/transport/shadowtls/shadowtls.go @@ -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, + } +} diff --git a/transport/simple-obfs/tls.go b/transport/simple-obfs/tls.go index 1c609c15..fed8a483 100644 --- a/transport/simple-obfs/tls.go +++ b/transport/simple-obfs/tls.go @@ -109,7 +109,12 @@ func (to *TLSObfs) write(b []byte) (int, error) { binary.Write(buf, binary.BigEndian, uint16(len(b))) buf.Write(b) _, 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 diff --git a/transport/trojan/trojan.go b/transport/trojan/trojan.go index 937b5f91..86de2f65 100644 --- a/transport/trojan/trojan.go +++ b/transport/trojan/trojan.go @@ -8,18 +8,17 @@ import ( "encoding/hex" "errors" "fmt" - tlsC "github.com/Dreamacro/clash/component/tls" "io" "net" "net/http" "sync" "github.com/Dreamacro/clash/common/pool" + tlsC "github.com/Dreamacro/clash/component/tls" C "github.com/Dreamacro/clash/constant" "github.com/Dreamacro/clash/transport/socks5" "github.com/Dreamacro/clash/transport/vless" "github.com/Dreamacro/clash/transport/vmess" - xtls "github.com/xtls/go" ) @@ -117,9 +116,6 @@ func (t *Trojan) StreamConn(conn net.Conn) (net.Conn, error) { } tlsConn := tls.Client(conn, tlsConfig) - if err := tlsConn.Handshake(); err != nil { - return nil, err - } // fix tls handshake not timeout ctx, cancel := context.WithTimeout(context.Background(), C.DefaultTLSTimeout) defer cancel()