diff --git a/.github/rename-cgo.sh b/.github/rename-cgo.sh index 548417129a..a0d736deb8 100644 --- a/.github/rename-cgo.sh +++ b/.github/rename-cgo.sh @@ -15,6 +15,15 @@ do elif [[ $FILENAME =~ "windows-4.0-amd64" ]];then echo "rename windows amd64 $FILENAME" mv $FILENAME clash.meta-windows-amd64-cgo.exe + elif [[ $FILENAME =~ "clash.meta-linux-arm-5" ]];then + echo "rename clash.meta-linux-arm-5 $FILENAME" + mv $FILENAME clash.meta-linux-armv5-cgo + elif [[ $FILENAME =~ "clash.meta-linux-arm-6" ]];then + echo "rename clash.meta-linux-arm-6 $FILENAME" + mv $FILENAME clash.meta-linux-armv6-cgo + elif [[ $FILENAME =~ "clash.meta-linux-arm-7" ]];then + echo "rename clash.meta-linux-arm-7 $FILENAME" + mv $FILENAME clash.meta-linux-armv7-cgo elif [[ $FILENAME =~ "linux" ]];then echo "rename linux $FILENAME" mv $FILENAME $FILENAME-cgo diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 66b694def2..dc43a10e95 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -16,6 +16,11 @@ on: - Alpha - Beta - Meta + +concurrency: + group: ${{ github.ref }}-${{ github.workflow }} + cancel-in-progress: true + env: REGISTRY: docker.io jobs: @@ -46,25 +51,26 @@ jobs: target: "linux-mips-softfloat linux-mips-hardfloat linux-mipsle-softfloat linux-mipsle-hardfloat", id: "4", } + - { type: "WithoutCGO", target: "linux-386 linux-riscv64", id: "5" } - { type: "WithoutCGO", target: "freebsd-386 freebsd-amd64 freebsd-arm64", - id: "5", + id: "6", } - { type: "WithoutCGO", target: "windows-amd64-compatible windows-amd64 windows-386", - id: "6", + id: "7", } - { type: "WithoutCGO", target: "windows-arm64 windows-arm32v7", - id: "7", + id: "8", } - { type: "WithoutCGO", target: "darwin-amd64 darwin-arm64 android-arm64", - id: "8", + id: "9", } - { type: "WithCGO", target: "windows/*", id: "1" } - { type: "WithCGO", target: "linux/386", id: "2" } @@ -108,10 +114,11 @@ jobs: - name: Set ENV run: | + sudo timedatectl set-timezone "Asia/Shanghai" echo "NAME=clash.meta" >> $GITHUB_ENV echo "REPO=${{ github.repository }}" >> $GITHUB_ENV echo "ShortSHA=$(git rev-parse --short ${{ github.sha }})" >> $GITHUB_ENV - echo "BUILDTIME=$(date -u)" >> $GITHUB_ENV + echo "BUILDTIME=$(date)" >> $GITHUB_ENV echo "BRANCH=$(git rev-parse --abbrev-ref HEAD)" >> $GITHUB_ENV shell: bash @@ -193,6 +200,10 @@ jobs: ls -la cd .. + - name: Save version + run: echo ${VERSION} > bin/version.txt + shell: bash + - uses: actions/upload-artifact@v3 if: ${{ success() }} with: @@ -202,7 +213,7 @@ jobs: Upload-Prerelease: permissions: write-all if: ${{ github.ref_type=='branch' }} - needs: [ Build ] + needs: [Build] runs-on: ubuntu-latest steps: - uses: actions/download-artifact@v3 @@ -221,6 +232,11 @@ jobs: tag: Prerelease-${{ github.ref_name }} deleteOnlyFromDrafts: false + - name: Set Env + run: | + echo "BUILDTIME=$(TZ=Asia/Shanghai date)" >> $GITHUB_ENV + shell: bash + - name: Tag Repo uses: richardsimko/update-tag@v1.0.6 with: @@ -228,20 +244,34 @@ jobs: env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + - run: | + cat > release.txt << 'EOF' + Release created at ${{ env.BUILDTIME }} + Synchronize ${{ github.ref_name }} branch code updates, keeping only the latest version +
+ ### release version + `default(not specified in file name)`: compiled with GOAMD64=v3 + `cgo`: support lwip tun stack, compiled with GOAMD64=v1 + `compatible`: compiled with GOAMD64=v1 + Check details between different architectural levels [here](https://github.com/golang/go/wiki/MinimumRequirements#amd64). + EOF + - name: Upload Prerelease uses: softprops/action-gh-release@v1 if: ${{ success() }} with: tag: ${{ github.ref_name }} tag_name: Prerelease-${{ github.ref_name }} - files: bin/* + files: | + bin/* prerelease: true generate_release_notes: true + body_path: release.txt Upload-Release: permissions: write-all if: ${{ github.ref_type=='tag' }} - needs: [ Build ] + needs: [Build] runs-on: ubuntu-latest steps: - uses: actions/download-artifact@v3 @@ -264,7 +294,7 @@ jobs: Docker: permissions: write-all - needs: [ Build ] + needs: [Build] runs-on: ubuntu-latest steps: - name: Checkout repository @@ -321,5 +351,7 @@ jobs: linux/386 linux/amd64 linux/arm64/v8 + linux/arm/v7 + # linux/riscv64 tags: ${{ steps.meta.outputs.tags }} labels: ${{ steps.meta.outputs.labels }} diff --git a/Dockerfile b/Dockerfile index 9c2e44c791..f5dcc30752 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,4 +1,6 @@ FROM alpine:latest as builder +ARG TARGETPLATFORM +RUN echo "I'm building for $TARGETPLATFORM" RUN apk add --no-cache gzip && \ mkdir /clash-config && \ @@ -10,7 +12,7 @@ COPY docker/file-name.sh /clash/file-name.sh WORKDIR /clash COPY bin/ bin/ RUN FILE_NAME=`sh file-name.sh` && echo $FILE_NAME && \ - FILE_NAME=`ls bin/ | egrep "$FILE_NAME.*"|awk NR==1` && \ + FILE_NAME=`ls bin/ | egrep "$FILE_NAME.*"|awk NR==1` && echo $FILE_NAME && \ mv bin/$FILE_NAME clash.gz && gzip -d clash.gz && echo "$FILE_NAME" > /clash-config/test FROM alpine:latest LABEL org.opencontainers.image.source="https://github.com/MetaCubeX/Clash.Meta" diff --git a/Makefile b/Makefile index f7a637a9fb..ecd1f5e6ce 100644 --- a/Makefile +++ b/Makefile @@ -101,6 +101,9 @@ linux-mips64: linux-mips64le: GOARCH=mips64le GOOS=linux $(GOBUILD) -o $(BINDIR)/$(NAME)-$@ +linux-riscv64: + GOARCH=riscv64 GOOS=linux $(GOBUILD) -o $(BINDIR)/$(NAME)-$@ + android-arm64: GOARCH=arm64 GOOS=android $(GOBUILD) -o $(BINDIR)/$(NAME)-$@ diff --git a/README.md b/README.md index cdfa350501..9339a9354c 100644 --- a/README.md +++ b/README.md @@ -30,8 +30,7 @@ - Comprehensive HTTP RESTful API controller ## Wiki - -Documentation and configuring examples are available on [wiki](https://github.com/MetaCubeX/Clash.Meta/wiki) and [Clash.Meta Wiki](https://docs.metacubex.one/). +Configuration examples can be found at [/docs/config.yaml](https://github.com/MetaCubeX/Clash.Meta/blob/Alpha/docs/config.yaml), while documentation can be found [Clash.Meta Wiki](https://clash-meta.wiki). ## Build diff --git a/adapter/inbound/socket.go b/adapter/inbound/socket.go index 590f64d71e..a6b1288c69 100644 --- a/adapter/inbound/socket.go +++ b/adapter/inbound/socket.go @@ -34,7 +34,7 @@ func NewInner(conn net.Conn, dst string, host string) *context.ConnContext { metadata := &C.Metadata{} metadata.NetWork = C.TCP metadata.Type = C.INNER - metadata.DNSMode = C.DNSMapping + metadata.DNSMode = C.DNSNormal metadata.Host = host metadata.Process = C.ClashName if h, port, err := net.SplitHostPort(dst); err == nil { @@ -42,6 +42,8 @@ func NewInner(conn net.Conn, dst string, host string) *context.ConnContext { if host == "" { if ip, err := netip.ParseAddr(h); err == nil { metadata.DstIP = ip + } else { + metadata.Host = h } } } diff --git a/adapter/outbound/base.go b/adapter/outbound/base.go index 24de7d94d0..156bc1141a 100644 --- a/adapter/outbound/base.go +++ b/adapter/outbound/base.go @@ -8,10 +8,9 @@ import ( "strings" N "github.com/Dreamacro/clash/common/net" + "github.com/Dreamacro/clash/common/utils" "github.com/Dreamacro/clash/component/dialer" C "github.com/Dreamacro/clash/constant" - - "github.com/gofrs/uuid" ) type Base struct { @@ -35,12 +34,7 @@ func (b *Base) Name() string { // Id implements C.ProxyAdapter func (b *Base) Id() string { if b.id == "" { - id, err := uuid.NewV6() - if err != nil { - b.id = b.name - } else { - b.id = id.String() - } + b.id = utils.NewUUIDV6().String() } return b.id @@ -140,10 +134,15 @@ func (b *Base) DialOptions(opts ...dialer.Option) []dialer.Option { default: } + if b.tfo { + opts = append(opts, dialer.WithTFO(true)) + } + return opts } type BasicOption struct { + TFO bool `proxy:"tfo,omitempty" group:"tfo,omitempty"` Interface string `proxy:"interface-name,omitempty" group:"interface-name,omitempty"` RoutingMark int `proxy:"routing-mark,omitempty" group:"routing-mark,omitempty"` IPVersion string `proxy:"ip-version,omitempty" group:"ip-version,omitempty"` @@ -206,6 +205,8 @@ func NewConn(c net.Conn, a C.ProxyAdapter) C.Conn { type packetConn struct { net.PacketConn chain C.Chain + adapterName string + connID string actualRemoteDestination string } @@ -223,8 +224,13 @@ func (c *packetConn) AppendToChains(a C.ProxyAdapter) { c.chain = append(c.chain, a.Name()) } +func (c *packetConn) LocalAddr() net.Addr { + lAddr := c.PacketConn.LocalAddr() + return N.NewCustomAddr(c.adapterName, c.connID, lAddr) // make quic-go's connMultiplexer happy +} + func newPacketConn(pc net.PacketConn, a C.ProxyAdapter) C.PacketConn { - return &packetConn{pc, []string{a.Name()}, parseRemoteDestination(a.Addr())} + return &packetConn{pc, []string{a.Name()}, a.Name(), utils.NewUUIDV4().String(), parseRemoteDestination(a.Addr())} } func parseRemoteDestination(addr string) string { diff --git a/adapter/outbound/http.go b/adapter/outbound/http.go index 720dc3e117..6a668ebb70 100644 --- a/adapter/outbound/http.go +++ b/adapter/outbound/http.go @@ -170,6 +170,7 @@ func NewHttp(option HttpOption) (*Http, error) { name: option.Name, addr: net.JoinHostPort(option.Server, strconv.Itoa(option.Port)), tp: C.Http, + tfo: option.TFO, iface: option.Interface, rmark: option.RoutingMark, prefer: C.NewDNSPrefer(option.IPVersion), diff --git a/adapter/outbound/reality.go b/adapter/outbound/reality.go new file mode 100644 index 0000000000..23314e5f1b --- /dev/null +++ b/adapter/outbound/reality.go @@ -0,0 +1,35 @@ +package outbound + +import ( + "encoding/base64" + "encoding/hex" + "errors" + + tlsC "github.com/Dreamacro/clash/component/tls" + + "golang.org/x/crypto/curve25519" +) + +type RealityOptions struct { + PublicKey string `proxy:"public-key"` + ShortID string `proxy:"short-id"` +} + +func (o RealityOptions) Parse() (*tlsC.RealityConfig, error) { + if o.PublicKey != "" { + config := new(tlsC.RealityConfig) + + n, err := base64.RawURLEncoding.Decode(config.PublicKey[:], []byte(o.PublicKey)) + if err != nil || n != curve25519.ScalarSize { + return nil, errors.New("invalid REALITY public key") + } + + n, err = hex.Decode(config.ShortID[:], []byte(o.ShortID)) + if err != nil || n > tlsC.RealityMaxShortIDLen { + return nil, errors.New("invalid REALITY short ID") + } + + return config, nil + } + return nil, nil +} diff --git a/adapter/outbound/reject.go b/adapter/outbound/reject.go index 43833238c8..a469ed2a75 100644 --- a/adapter/outbound/reject.go +++ b/adapter/outbound/reject.go @@ -6,6 +6,7 @@ import ( "net" "time" + "github.com/Dreamacro/clash/common/buf" "github.com/Dreamacro/clash/component/dialer" C "github.com/Dreamacro/clash/constant" ) @@ -16,12 +17,12 @@ type Reject struct { // DialContext implements C.ProxyAdapter func (r *Reject) DialContext(ctx context.Context, metadata *C.Metadata, opts ...dialer.Option) (C.Conn, error) { - return NewConn(&nopConn{}, r), nil + return NewConn(nopConn{}, r), nil } // ListenPacketContext implements C.ProxyAdapter func (r *Reject) ListenPacketContext(ctx context.Context, metadata *C.Metadata, opts ...dialer.Option) (C.PacketConn, error) { - return newPacketConn(&nopPacketConn{}, r), nil + return newPacketConn(nopPacketConn{}, r), nil } func NewReject() *Reject { @@ -48,27 +49,37 @@ func NewPass() *Reject { type nopConn struct{} -func (rw *nopConn) Read(b []byte) (int, error) { +func (rw nopConn) Read(b []byte) (int, error) { return 0, io.EOF } -func (rw *nopConn) Write(b []byte) (int, error) { +func (rw nopConn) ReadBuffer(buffer *buf.Buffer) error { + return io.EOF +} + +func (rw nopConn) Write(b []byte) (int, error) { return 0, io.EOF } -func (rw *nopConn) Close() error { return nil } -func (rw *nopConn) LocalAddr() net.Addr { return nil } -func (rw *nopConn) RemoteAddr() net.Addr { return nil } -func (rw *nopConn) SetDeadline(time.Time) error { return nil } -func (rw *nopConn) SetReadDeadline(time.Time) error { return nil } -func (rw *nopConn) SetWriteDeadline(time.Time) error { return nil } +func (rw nopConn) WriteBuffer(buffer *buf.Buffer) error { + return io.EOF +} + +func (rw nopConn) Close() error { return nil } +func (rw nopConn) LocalAddr() net.Addr { return nil } +func (rw nopConn) RemoteAddr() net.Addr { return nil } +func (rw nopConn) SetDeadline(time.Time) error { return nil } +func (rw nopConn) SetReadDeadline(time.Time) error { return nil } +func (rw nopConn) SetWriteDeadline(time.Time) error { return nil } + +var udpAddrIPv4Unspecified = &net.UDPAddr{IP: net.IPv4zero, Port: 0} type nopPacketConn struct{} -func (npc *nopPacketConn) WriteTo(b []byte, addr net.Addr) (n int, err error) { return len(b), nil } -func (npc *nopPacketConn) ReadFrom(b []byte) (int, net.Addr, error) { return 0, nil, io.EOF } -func (npc *nopPacketConn) Close() error { return nil } -func (npc *nopPacketConn) LocalAddr() net.Addr { return &net.UDPAddr{IP: net.IPv4zero, Port: 0} } -func (npc *nopPacketConn) SetDeadline(time.Time) error { return nil } -func (npc *nopPacketConn) SetReadDeadline(time.Time) error { return nil } -func (npc *nopPacketConn) SetWriteDeadline(time.Time) error { return nil } +func (npc nopPacketConn) WriteTo(b []byte, addr net.Addr) (n int, err error) { return len(b), nil } +func (npc nopPacketConn) ReadFrom(b []byte) (int, net.Addr, error) { return 0, nil, io.EOF } +func (npc nopPacketConn) Close() error { return nil } +func (npc nopPacketConn) LocalAddr() net.Addr { return udpAddrIPv4Unspecified } +func (npc nopPacketConn) SetDeadline(time.Time) error { return nil } +func (npc nopPacketConn) SetReadDeadline(time.Time) error { return nil } +func (npc nopPacketConn) SetWriteDeadline(time.Time) error { return nil } diff --git a/adapter/outbound/shadowsocks.go b/adapter/outbound/shadowsocks.go index 54566666cf..1c64b3caf5 100644 --- a/adapter/outbound/shadowsocks.go +++ b/adapter/outbound/shadowsocks.go @@ -2,21 +2,23 @@ package outbound import ( "context" - "crypto/tls" "errors" "fmt" "net" "strconv" + "time" + N "github.com/Dreamacro/clash/common/net" "github.com/Dreamacro/clash/common/structure" "github.com/Dreamacro/clash/component/dialer" - tlsC "github.com/Dreamacro/clash/component/tls" C "github.com/Dreamacro/clash/constant" - "github.com/Dreamacro/clash/transport/shadowtls" + "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" @@ -33,21 +35,23 @@ type ShadowSocks struct { obfsMode string obfsOption *simpleObfsOption v2rayOption *v2rayObfs.Option - shadowTLSOption *shadowTLSOption - tlsConfig *tls.Config + 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"` + UDPOverTCPVersion int `proxy:"udp-over-tcp-version,omitempty"` + ClientFingerprint string `proxy:"client-fingerprint,omitempty"` } type simpleObfsOption struct { @@ -71,10 +75,26 @@ type shadowTLSOption struct { 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 func (ss *ShadowSocks) StreamConn(c net.Conn, metadata *C.Metadata) (net.Conn, error) { + // fix tls handshake not timeout + ctx, cancel := context.WithTimeout(context.Background(), C.DefaultTLSTimeout) + defer cancel() + return ss.StreamConnContext(ctx, c, metadata) +} + +func (ss *ShadowSocks) StreamConnContext(ctx context.Context, c net.Conn, metadata *C.Metadata) (net.Conn, error) { + useEarly := false switch ss.obfsMode { case "tls": c = obfs.NewTLSObfs(c, ss.obfsOption.Host) @@ -88,12 +108,34 @@ func (ss *ShadowSocks) StreamConn(c net.Conn, metadata *C.Metadata) (net.Conn, e return nil, fmt.Errorf("%s connect error: %w", ss.addr, err) } case shadowtls.Mode: - c = shadowtls.NewShadowTLS(c, ss.shadowTLSOption.Password, ss.tlsConfig) + var err error + c, err = shadowtls.NewShadowTLS(ctx, c, ss.shadowTLSOption) + if err != nil { + return nil, err + } + useEarly = true + case restls.Mode: + var err error + c, err = restls.NewRestls(ctx, c, ss.restlsConfig) + if err != nil { + return nil, fmt.Errorf("%s (restls) connect error: %w", ss.addr, err) + } + useEarly = true } + useEarly = useEarly || N.NeedHandshake(c) if metadata.NetWork == C.UDP && ss.option.UDPOverTCP { - return ss.method.DialConn(c, M.ParseSocksaddr(uot.UOTMagicAddress+":443")) + uotDestination := uot.RequestDestination(uint8(ss.option.UDPOverTCPVersion)) + if useEarly { + return ss.method.DialEarlyConn(c, uotDestination), nil + } else { + return ss.method.DialConn(c, uotDestination) + } + } + if useEarly { + return ss.method.DialEarlyConn(c, M.ParseSocksaddr(metadata.RemoteAddress())), nil + } else { + return ss.method.DialConn(c, M.ParseSocksaddr(metadata.RemoteAddress())) } - return ss.method.DialConn(c, M.ParseSocksaddr(metadata.RemoteAddress())) } // DialContext implements C.ProxyAdapter @@ -113,7 +155,7 @@ func (ss *ShadowSocks) DialContextWithDialer(ctx context.Context, dialer C.Diale safeConnClose(c, err) }(c) - c, err = ss.StreamConn(c, metadata) + c, err = ss.StreamConnContext(ctx, c, metadata) return NewConn(c, ss), err } @@ -129,7 +171,12 @@ func (ss *ShadowSocks) ListenPacketWithDialer(ctx context.Context, dialer C.Dial if err != nil { return nil, err } - return newPacketConn(uot.NewClientConn(tcpConn), ss), nil + destination := M.ParseSocksaddr(metadata.RemoteAddress()) + if ss.option.UDPOverTCPVersion == 1 { + return newPacketConn(uot.NewConn(tcpConn, uot.Request{Destination: destination}), ss), nil + } else { + return newPacketConn(uot.NewLazyConn(tcpConn, uot.Request{Destination: destination}), ss), nil + } } addr, err := resolveUDPAddrWithPrefer(ctx, "udp", ss.addr, ss.prefer) if err != nil { @@ -152,7 +199,12 @@ func (ss *ShadowSocks) SupportWithDialer() bool { // ListenPacketOnStreamConn implements C.ProxyAdapter func (ss *ShadowSocks) ListenPacketOnStreamConn(c net.Conn, metadata *C.Metadata) (_ C.PacketConn, err error) { if ss.option.UDPOverTCP { - return newPacketConn(uot.NewClientConn(c), ss), nil + destination := M.ParseSocksaddr(metadata.RemoteAddress()) + if ss.option.UDPOverTCPVersion == uot.LegacyVersion { + return newPacketConn(uot.NewConn(c, uot.Request{Destination: destination}), ss), nil + } else { + return newPacketConn(uot.NewLazyConn(c, uot.Request{Destination: destination}), ss), nil + } } return nil, errors.New("no support") } @@ -164,15 +216,15 @@ func (ss *ShadowSocks) SupportUOT() bool { func NewShadowSocks(option ShadowSocksOption) (*ShadowSocks, error) { addr := net.JoinHostPort(option.Server, strconv.Itoa(option.Port)) - method, err := shadowimpl.FetchMethod(option.Cipher, option.Password) + method, err := shadowimpl.FetchMethod(option.Cipher, option.Password, time.Now) if err != nil { return nil, fmt.Errorf("ss %s initialize error: %w", addr, err) } var v2rayOption *v2rayObfs.Option var obfsOption *simpleObfsOption - var shadowTLSOpt *shadowTLSOption - var tlsConfig *tls.Config + var shadowTLSOpt *shadowtls.ShadowTLSOption + var restlsConfig *restlsC.Config obfsMode := "" decoder := structure.NewDecoder(structure.Option{TagName: "obfs", WeaklyTypedInput: true}) @@ -210,25 +262,41 @@ func NewShadowSocks(option ShadowSocksOption) (*ShadowSocks, error) { } } else if option.Plugin == shadowtls.Mode { obfsMode = shadowtls.Mode - shadowTLSOpt = &shadowTLSOption{} - if err := decoder.Decode(option.PluginOpts, shadowTLSOpt); err != nil { + opt := &shadowTLSOption{ + Version: 2, + } + if err := decoder.Decode(option.PluginOpts, opt); err != nil { return nil, fmt.Errorf("ss %s initialize shadow-tls-plugin error: %w", addr, err) } - tlsConfig = &tls.Config{ - NextProtos: shadowtls.DefaultALPN, - MinVersion: tls.VersionTLS12, - InsecureSkipVerify: shadowTLSOpt.SkipCertVerify, - ServerName: shadowTLSOpt.Host, + shadowTLSOpt = &shadowtls.ShadowTLSOption{ + Password: opt.Password, + Host: opt.Host, + Fingerprint: opt.Fingerprint, + 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) } - if len(shadowTLSOpt.Fingerprint) == 0 { - tlsConfig = tlsC.GetGlobalTLSConfig(tlsConfig) - } else { - if tlsConfig, err = tlsC.GetSpecifiedFingerprintTLSConfig(tlsConfig, shadowTLSOpt.Fingerprint); err != nil { - return nil, 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) } + + } + switch option.UDPOverTCPVersion { + case uot.Version, uot.LegacyVersion: + case 0: + option.UDPOverTCPVersion = uot.Version + default: + return nil, fmt.Errorf("ss %s unknown udp over tcp protocol version: %d", addr, option.UDPOverTCPVersion) } return &ShadowSocks{ @@ -237,6 +305,7 @@ func NewShadowSocks(option ShadowSocksOption) (*ShadowSocks, error) { addr: addr, tp: C.Shadowsocks, udp: option.UDP, + tfo: option.TFO, iface: option.Interface, rmark: option.RoutingMark, prefer: C.NewDNSPrefer(option.IPVersion), @@ -248,7 +317,7 @@ func NewShadowSocks(option ShadowSocksOption) (*ShadowSocks, error) { v2rayOption: v2rayOption, obfsOption: obfsOption, shadowTLSOption: shadowTLSOpt, - tlsConfig: tlsConfig, + restlsConfig: restlsConfig, }, nil } diff --git a/adapter/outbound/shadowsocksr.go b/adapter/outbound/shadowsocksr.go index e84de879a6..135e713261 100644 --- a/adapter/outbound/shadowsocksr.go +++ b/adapter/outbound/shadowsocksr.go @@ -163,6 +163,7 @@ func NewShadowSocksR(option ShadowSocksROption) (*ShadowSocksR, error) { addr: addr, tp: C.ShadowsocksR, udp: option.UDP, + tfo: option.TFO, iface: option.Interface, rmark: option.RoutingMark, prefer: C.NewDNSPrefer(option.IPVersion), diff --git a/adapter/outbound/snell.go b/adapter/outbound/snell.go index 1331b52632..d6f1efeeb6 100644 --- a/adapter/outbound/snell.go +++ b/adapter/outbound/snell.go @@ -167,6 +167,7 @@ func NewSnell(option SnellOption) (*Snell, error) { addr: addr, tp: C.Snell, udp: option.UDP, + tfo: option.TFO, iface: option.Interface, rmark: option.RoutingMark, prefer: C.NewDNSPrefer(option.IPVersion), diff --git a/adapter/outbound/socks5.go b/adapter/outbound/socks5.go index d40a6bff4a..cdb89cc229 100644 --- a/adapter/outbound/socks5.go +++ b/adapter/outbound/socks5.go @@ -182,6 +182,7 @@ func NewSocks5(option Socks5Option) (*Socks5, error) { addr: net.JoinHostPort(option.Server, strconv.Itoa(option.Port)), tp: C.Socks5, udp: option.UDP, + tfo: option.TFO, iface: option.Interface, rmark: option.RoutingMark, prefer: C.NewDNSPrefer(option.IPVersion), diff --git a/adapter/outbound/trojan.go b/adapter/outbound/trojan.go index 2a8cfe4780..030e74a992 100644 --- a/adapter/outbound/trojan.go +++ b/adapter/outbound/trojan.go @@ -26,25 +26,28 @@ type Trojan struct { gunTLSConfig *tls.Config gunConfig *gun.Config transport *gun.TransportWrap + + realityConfig *tlsC.RealityConfig } type TrojanOption struct { BasicOption - Name string `proxy:"name"` - Server string `proxy:"server"` - Port int `proxy:"port"` - Password string `proxy:"password"` - ALPN []string `proxy:"alpn,omitempty"` - SNI string `proxy:"sni,omitempty"` - SkipCertVerify bool `proxy:"skip-cert-verify,omitempty"` - Fingerprint string `proxy:"fingerprint,omitempty"` - UDP bool `proxy:"udp,omitempty"` - Network string `proxy:"network,omitempty"` - GrpcOpts GrpcOptions `proxy:"grpc-opts,omitempty"` - WSOpts WSOptions `proxy:"ws-opts,omitempty"` - Flow string `proxy:"flow,omitempty"` - FlowShow bool `proxy:"flow-show,omitempty"` - ClientFingerprint string `proxy:"client-fingerprint,omitempty"` + Name string `proxy:"name"` + Server string `proxy:"server"` + Port int `proxy:"port"` + Password string `proxy:"password"` + ALPN []string `proxy:"alpn,omitempty"` + SNI string `proxy:"sni,omitempty"` + SkipCertVerify bool `proxy:"skip-cert-verify,omitempty"` + Fingerprint string `proxy:"fingerprint,omitempty"` + UDP bool `proxy:"udp,omitempty"` + Network string `proxy:"network,omitempty"` + RealityOpts RealityOptions `proxy:"reality-opts,omitempty"` + GrpcOpts GrpcOptions `proxy:"grpc-opts,omitempty"` + WSOpts WSOptions `proxy:"ws-opts,omitempty"` + Flow string `proxy:"flow,omitempty"` + FlowShow bool `proxy:"flow-show,omitempty"` + ClientFingerprint string `proxy:"client-fingerprint,omitempty"` } func (t *Trojan) plainStream(c net.Conn) (net.Conn, error) { @@ -83,7 +86,7 @@ func (t *Trojan) StreamConn(c net.Conn, metadata *C.Metadata) (net.Conn, error) } if t.transport != nil { - c, err = gun.StreamGunWithConn(c, t.gunTLSConfig, t.gunConfig) + c, err = gun.StreamGunWithConn(c, t.gunTLSConfig, t.gunConfig, t.realityConfig) } else { c, err = t.plainStream(c) } @@ -250,6 +253,7 @@ func NewTrojan(option TrojanOption) (*Trojan, error) { addr: addr, tp: C.Trojan, udp: option.UDP, + tfo: option.TFO, iface: option.Interface, rmark: option.RoutingMark, prefer: C.NewDNSPrefer(option.IPVersion), @@ -258,6 +262,13 @@ func NewTrojan(option TrojanOption) (*Trojan, error) { option: &option, } + var err error + t.realityConfig, err = option.RealityOpts.Parse() + if err != nil { + return nil, err + } + tOption.Reality = t.realityConfig + if option.Network == "grpc" { dialFn := func(network, addr string) (net.Conn, error) { c, err := dialer.DialContext(context.Background(), "tcp", t.addr, t.Base.DialOptions()...) @@ -284,7 +295,7 @@ func NewTrojan(option TrojanOption) (*Trojan, error) { } } - t.transport = gun.NewHTTP2Client(dialFn, tlsConfig, tOption.ClientFingerprint) + t.transport = gun.NewHTTP2Client(dialFn, tlsConfig, tOption.ClientFingerprint, t.realityConfig) t.gunTLSConfig = tlsConfig t.gunConfig = &gun.Config{ diff --git a/adapter/outbound/tuic.go b/adapter/outbound/tuic.go index 0ca136700c..d2f2b5e9f4 100644 --- a/adapter/outbound/tuic.go +++ b/adapter/outbound/tuic.go @@ -42,15 +42,17 @@ type TuicOption struct { DisableSni bool `proxy:"disable-sni,omitempty"` MaxUdpRelayPacketSize int `proxy:"max-udp-relay-packet-size,omitempty"` - FastOpen bool `proxy:"fast-open,omitempty"` - MaxOpenStreams int `proxy:"max-open-streams,omitempty"` - SkipCertVerify bool `proxy:"skip-cert-verify,omitempty"` - Fingerprint string `proxy:"fingerprint,omitempty"` - CustomCA string `proxy:"ca,omitempty"` - CustomCAString string `proxy:"ca-str,omitempty"` - ReceiveWindowConn int `proxy:"recv-window-conn,omitempty"` - ReceiveWindow int `proxy:"recv-window,omitempty"` - DisableMTUDiscovery bool `proxy:"disable-mtu-discovery,omitempty"` + FastOpen bool `proxy:"fast-open,omitempty"` + MaxOpenStreams int `proxy:"max-open-streams,omitempty"` + SkipCertVerify bool `proxy:"skip-cert-verify,omitempty"` + Fingerprint string `proxy:"fingerprint,omitempty"` + CustomCA string `proxy:"ca,omitempty"` + CustomCAString string `proxy:"ca-str,omitempty"` + ReceiveWindowConn int `proxy:"recv-window-conn,omitempty"` + ReceiveWindow int `proxy:"recv-window,omitempty"` + DisableMTUDiscovery bool `proxy:"disable-mtu-discovery,omitempty"` + MaxDatagramFrameSize int `proxy:"max-datagram-frame-size,omitempty"` + SNI string `proxy:"sni,omitempty"` } // DialContext implements C.ProxyAdapter @@ -106,12 +108,14 @@ func (t *Tuic) dialWithDialer(ctx context.Context, dialer C.Dialer) (pc net.Pack func NewTuic(option TuicOption) (*Tuic, error) { addr := net.JoinHostPort(option.Server, strconv.Itoa(option.Port)) serverName := option.Server - tlsConfig := &tls.Config{ ServerName: serverName, InsecureSkipVerify: option.SkipCertVerify, MinVersion: tls.VersionTLS13, } + if option.SNI != "" { + tlsConfig.ServerName = option.SNI + } var bs []byte var err error @@ -172,6 +176,15 @@ func NewTuic(option TuicOption) (*Tuic, error) { option.MaxOpenStreams = 100 } + if option.MaxDatagramFrameSize == 0 { + option.MaxDatagramFrameSize = option.MaxUdpRelayPacketSize + tuic.PacketOverHead + } + + if option.MaxDatagramFrameSize > 1400 { + option.MaxDatagramFrameSize = 1400 + } + option.MaxUdpRelayPacketSize = option.MaxDatagramFrameSize - tuic.PacketOverHead + // ensure server's incoming stream can handle correctly, increase to 1.1x quicMaxOpenStreams := int64(option.MaxOpenStreams) quicMaxOpenStreams = quicMaxOpenStreams + int64(math.Ceil(float64(quicMaxOpenStreams)/10.0)) @@ -184,6 +197,7 @@ func NewTuic(option TuicOption) (*Tuic, error) { MaxIncomingUniStreams: quicMaxOpenStreams, KeepAlivePeriod: time.Duration(option.HeartbeatInterval) * time.Millisecond, DisablePathMTUDiscovery: option.DisableMTUDiscovery, + MaxDatagramFrameSize: int64(option.MaxDatagramFrameSize), EnableDatagrams: true, } if option.ReceiveWindowConn == 0 { @@ -213,6 +227,7 @@ func NewTuic(option TuicOption) (*Tuic, error) { udp: true, tfo: option.FastOpen, iface: option.Interface, + rmark: option.RoutingMark, prefer: C.NewDNSPrefer(option.IPVersion), }, } diff --git a/adapter/outbound/vless.go b/adapter/outbound/vless.go index e46e245da5..757661cc2e 100644 --- a/adapter/outbound/vless.go +++ b/adapter/outbound/vless.go @@ -17,6 +17,7 @@ import ( "github.com/Dreamacro/clash/component/resolver" tlsC "github.com/Dreamacro/clash/component/tls" C "github.com/Dreamacro/clash/constant" + "github.com/Dreamacro/clash/log" "github.com/Dreamacro/clash/transport/gun" "github.com/Dreamacro/clash/transport/socks5" "github.com/Dreamacro/clash/transport/vless" @@ -41,6 +42,8 @@ type Vless struct { gunTLSConfig *tls.Config gunConfig *gun.Config transport *gun.TransportWrap + + realityConfig *tlsC.RealityConfig } type VlessOption struct { @@ -57,6 +60,7 @@ type VlessOption struct { XUDP bool `proxy:"xudp,omitempty"` PacketEncoding string `proxy:"packet-encoding,omitempty"` Network string `proxy:"network,omitempty"` + RealityOpts RealityOptions `proxy:"reality-opts,omitempty"` HTTPOpts HTTPOptions `proxy:"http-opts,omitempty"` HTTP2Opts HTTP2Options `proxy:"h2-opts,omitempty"` GrpcOpts GrpcOptions `proxy:"grpc-opts,omitempty"` @@ -78,7 +82,6 @@ func (v *Vless) StreamConn(c net.Conn, metadata *C.Metadata) (net.Conn, error) { switch v.option.Network { case "ws": - host, port, _ := net.SplitHostPort(v.addr) wsOpts := &vmess.WebsocketConfig{ Host: host, @@ -154,7 +157,7 @@ func (v *Vless) StreamConn(c net.Conn, metadata *C.Metadata) (net.Conn, error) { c, err = vmess.StreamH2Conn(c, h2Opts) case "grpc": - c, err = gun.StreamGunWithConn(c, v.gunTLSConfig, v.gunConfig) + c, err = gun.StreamGunWithConn(c, v.gunTLSConfig, v.gunConfig, v.realityConfig) default: // default tcp network // handle TLS And XTLS @@ -171,7 +174,7 @@ func (v *Vless) StreamConn(c net.Conn, metadata *C.Metadata) (net.Conn, error) { func (v *Vless) streamTLSOrXTLSConn(conn net.Conn, isH2 bool) (net.Conn, error) { host, _, _ := net.SplitHostPort(v.addr) - if v.isXTLSEnabled() && !isH2 { + if v.isLegacyXTLSEnabled() && !isH2 { xtlsOpts := vless.XTLSConfig{ Host: host, SkipCertVerify: v.option.SkipCertVerify, @@ -190,6 +193,7 @@ func (v *Vless) streamTLSOrXTLSConn(conn net.Conn, isH2 bool) (net.Conn, error) SkipCertVerify: v.option.SkipCertVerify, FingerPrint: v.option.Fingerprint, ClientFingerprint: v.option.ClientFingerprint, + Reality: v.realityConfig, } if isH2 { @@ -206,8 +210,8 @@ func (v *Vless) streamTLSOrXTLSConn(conn net.Conn, isH2 bool) (net.Conn, error) return conn, nil } -func (v *Vless) isXTLSEnabled() bool { - return v.client.Addons != nil +func (v *Vless) isLegacyXTLSEnabled() bool { + return v.client.Addons != nil && v.client.Addons.Flow != vless.XRV } // DialContext implements C.ProxyAdapter @@ -479,6 +483,9 @@ func NewVless(option VlessOption) (*Vless, error) { if option.Network != "ws" && len(option.Flow) >= 16 { option.Flow = option.Flow[:16] switch option.Flow { + case vless.XRV: + log.Warnln("To use %s, ensure your server is upgrade to Xray-core v1.8.0+", vless.XRV) + fallthrough case vless.XRO, vless.XRD, vless.XRS: addons = &vless.Addons{ Flow: option.Flow, @@ -510,6 +517,7 @@ func NewVless(option VlessOption) (*Vless, error) { tp: C.Vless, udp: option.UDP, xudp: option.XUDP, + tfo: option.TFO, iface: option.Interface, rmark: option.RoutingMark, prefer: C.NewDNSPrefer(option.IPVersion), @@ -518,6 +526,11 @@ func NewVless(option VlessOption) (*Vless, error) { option: &option, } + v.realityConfig, err = v.option.RealityOpts.Parse() + if err != nil { + return nil, err + } + switch option.Network { case "h2": if len(option.HTTP2Opts.Host) == 0 { @@ -552,8 +565,7 @@ func NewVless(option VlessOption) (*Vless, error) { v.gunTLSConfig = tlsConfig v.gunConfig = gunConfig - v.transport = gun.NewHTTP2Client(dialFn, tlsConfig, v.option.ClientFingerprint) - + v.transport = gun.NewHTTP2Client(dialFn, tlsConfig, v.option.ClientFingerprint, v.realityConfig) } return v, nil diff --git a/adapter/outbound/vmess.go b/adapter/outbound/vmess.go index 5da8c8b1c7..5bb46dadca 100644 --- a/adapter/outbound/vmess.go +++ b/adapter/outbound/vmess.go @@ -11,6 +11,7 @@ import ( "strings" "sync" + N "github.com/Dreamacro/clash/common/net" "github.com/Dreamacro/clash/component/dialer" "github.com/Dreamacro/clash/component/resolver" tlsC "github.com/Dreamacro/clash/component/tls" @@ -34,32 +35,35 @@ type Vmess struct { gunTLSConfig *tls.Config gunConfig *gun.Config transport *gun.TransportWrap + + realityConfig *tlsC.RealityConfig } type VmessOption struct { BasicOption - Name string `proxy:"name"` - Server string `proxy:"server"` - Port int `proxy:"port"` - UUID string `proxy:"uuid"` - AlterID int `proxy:"alterId"` - Cipher string `proxy:"cipher"` - UDP bool `proxy:"udp,omitempty"` - Network string `proxy:"network,omitempty"` - TLS bool `proxy:"tls,omitempty"` - SkipCertVerify bool `proxy:"skip-cert-verify,omitempty"` - Fingerprint string `proxy:"fingerprint,omitempty"` - ServerName string `proxy:"servername,omitempty"` - HTTPOpts HTTPOptions `proxy:"http-opts,omitempty"` - HTTP2Opts HTTP2Options `proxy:"h2-opts,omitempty"` - GrpcOpts GrpcOptions `proxy:"grpc-opts,omitempty"` - WSOpts WSOptions `proxy:"ws-opts,omitempty"` - PacketAddr bool `proxy:"packet-addr,omitempty"` - XUDP bool `proxy:"xudp,omitempty"` - PacketEncoding string `proxy:"packet-encoding,omitempty"` - GlobalPadding bool `proxy:"global-padding,omitempty"` - AuthenticatedLength bool `proxy:"authenticated-length,omitempty"` - ClientFingerprint string `proxy:"client-fingerprint,omitempty"` + Name string `proxy:"name"` + Server string `proxy:"server"` + Port int `proxy:"port"` + UUID string `proxy:"uuid"` + AlterID int `proxy:"alterId"` + Cipher string `proxy:"cipher"` + UDP bool `proxy:"udp,omitempty"` + Network string `proxy:"network,omitempty"` + TLS bool `proxy:"tls,omitempty"` + SkipCertVerify bool `proxy:"skip-cert-verify,omitempty"` + Fingerprint string `proxy:"fingerprint,omitempty"` + ServerName string `proxy:"servername,omitempty"` + RealityOpts RealityOptions `proxy:"reality-opts,omitempty"` + HTTPOpts HTTPOptions `proxy:"http-opts,omitempty"` + HTTP2Opts HTTP2Options `proxy:"h2-opts,omitempty"` + GrpcOpts GrpcOptions `proxy:"grpc-opts,omitempty"` + WSOpts WSOptions `proxy:"ws-opts,omitempty"` + PacketAddr bool `proxy:"packet-addr,omitempty"` + XUDP bool `proxy:"xudp,omitempty"` + PacketEncoding string `proxy:"packet-encoding,omitempty"` + GlobalPadding bool `proxy:"global-padding,omitempty"` + AuthenticatedLength bool `proxy:"authenticated-length,omitempty"` + ClientFingerprint string `proxy:"client-fingerprint,omitempty"` } type HTTPOptions struct { @@ -94,7 +98,6 @@ func (v *Vmess) StreamConn(c net.Conn, metadata *C.Metadata) (net.Conn, error) { switch v.option.Network { case "ws": - host, port, _ := net.SplitHostPort(v.addr) wsOpts := &clashVMess.WebsocketConfig{ Host: host, @@ -143,12 +146,12 @@ func (v *Vmess) StreamConn(c net.Conn, metadata *C.Metadata) (net.Conn, error) { Host: host, SkipCertVerify: v.option.SkipCertVerify, ClientFingerprint: v.option.ClientFingerprint, + Reality: v.realityConfig, } if v.option.ServerName != "" { tlsOpts.Host = v.option.ServerName } - c, err = clashVMess.StreamTLSConn(c, tlsOpts) if err != nil { return nil, err @@ -171,6 +174,7 @@ func (v *Vmess) StreamConn(c net.Conn, metadata *C.Metadata) (net.Conn, error) { SkipCertVerify: v.option.SkipCertVerify, NextProtos: []string{"h2"}, ClientFingerprint: v.option.ClientFingerprint, + Reality: v.realityConfig, } if v.option.ServerName != "" { @@ -189,7 +193,7 @@ func (v *Vmess) StreamConn(c net.Conn, metadata *C.Metadata) (net.Conn, error) { c, err = clashVMess.StreamH2Conn(c, h2Opts) case "grpc": - c, err = gun.StreamGunWithConn(c, v.gunTLSConfig, v.gunConfig) + c, err = gun.StreamGunWithConn(c, v.gunTLSConfig, v.gunConfig, v.realityConfig) default: // handle TLS if v.option.TLS { @@ -198,6 +202,7 @@ func (v *Vmess) StreamConn(c net.Conn, metadata *C.Metadata) (net.Conn, error) { Host: host, SkipCertVerify: v.option.SkipCertVerify, ClientFingerprint: v.option.ClientFingerprint, + Reality: v.realityConfig, } if v.option.ServerName != "" { @@ -213,12 +218,24 @@ func (v *Vmess) StreamConn(c net.Conn, metadata *C.Metadata) (net.Conn, error) { } if metadata.NetWork == C.UDP { if v.option.XUDP { - return v.client.DialXUDPPacketConn(c, M.ParseSocksaddr(metadata.RemoteAddress())) + if N.NeedHandshake(c) { + return v.client.DialEarlyXUDPPacketConn(c, M.ParseSocksaddr(metadata.RemoteAddress())), nil + } else { + return v.client.DialXUDPPacketConn(c, M.ParseSocksaddr(metadata.RemoteAddress())) + } } else { - return v.client.DialPacketConn(c, M.ParseSocksaddr(metadata.RemoteAddress())) + if N.NeedHandshake(c) { + return v.client.DialEarlyPacketConn(c, M.ParseSocksaddr(metadata.RemoteAddress())), nil + } else { + return v.client.DialPacketConn(c, M.ParseSocksaddr(metadata.RemoteAddress())) + } } } else { - return v.client.DialConn(c, M.ParseSocksaddr(metadata.RemoteAddress())) + if N.NeedHandshake(c) { + return v.client.DialEarlyConn(c, M.ParseSocksaddr(metadata.RemoteAddress())), nil + } else { + return v.client.DialConn(c, M.ParseSocksaddr(metadata.RemoteAddress())) + } } } @@ -289,9 +306,17 @@ func (v *Vmess) ListenPacketContext(ctx context.Context, metadata *C.Metadata, o }(c) if v.option.XUDP { - c, err = v.client.DialXUDPPacketConn(c, M.ParseSocksaddr(metadata.RemoteAddress())) + if N.NeedHandshake(c) { + c = v.client.DialEarlyXUDPPacketConn(c, M.ParseSocksaddr(metadata.RemoteAddress())) + } else { + c, err = v.client.DialXUDPPacketConn(c, M.ParseSocksaddr(metadata.RemoteAddress())) + } } else { - c, err = v.client.DialPacketConn(c, M.ParseSocksaddr(metadata.RemoteAddress())) + if N.NeedHandshake(c) { + c = v.client.DialEarlyPacketConn(c, M.ParseSocksaddr(metadata.RemoteAddress())) + } else { + c, err = v.client.DialPacketConn(c, M.ParseSocksaddr(metadata.RemoteAddress())) + } } if err != nil { @@ -387,6 +412,7 @@ func NewVmess(option VmessOption) (*Vmess, error) { tp: C.Vmess, udp: option.UDP, xudp: option.XUDP, + tfo: option.TFO, iface: option.Interface, rmark: option.RoutingMark, prefer: C.NewDNSPrefer(option.IPVersion), @@ -429,9 +455,14 @@ func NewVmess(option VmessOption) (*Vmess, error) { v.gunTLSConfig = tlsConfig v.gunConfig = gunConfig - v.transport = gun.NewHTTP2Client(dialFn, tlsConfig, v.option.ClientFingerprint) + v.transport = gun.NewHTTP2Client(dialFn, tlsConfig, v.option.ClientFingerprint, v.realityConfig) + } + v.realityConfig, err = v.option.RealityOpts.Parse() + if err != nil { + return nil, err } + return v, nil } diff --git a/adapter/outbound/wireguard.go b/adapter/outbound/wireguard.go index d1a5ea6edf..7eae30fc02 100644 --- a/adapter/outbound/wireguard.go +++ b/adapter/outbound/wireguard.go @@ -17,7 +17,7 @@ import ( "github.com/Dreamacro/clash/component/dialer" "github.com/Dreamacro/clash/component/resolver" C "github.com/Dreamacro/clash/constant" - "github.com/Dreamacro/clash/listener/sing" + "github.com/Dreamacro/clash/log" wireguard "github.com/metacubex/sing-wireguard" @@ -34,7 +34,7 @@ type WireGuard struct { bind *wireguard.ClientBind device *device.Device tunDevice wireguard.Device - dialer *wgDialer + dialer *wgSingDialer startOnce sync.Once startErr error } @@ -56,16 +56,28 @@ type WireGuardOption struct { PersistentKeepalive int `proxy:"persistent-keepalive,omitempty"` } -type wgDialer struct { - options []dialer.Option +type wgSingDialer struct { + dialer dialer.Dialer } -func (d *wgDialer) DialContext(ctx context.Context, network string, destination M.Socksaddr) (net.Conn, error) { - return dialer.DialContext(ctx, network, destination.String(), d.options...) +var _ N.Dialer = &wgSingDialer{} + +func (d *wgSingDialer) DialContext(ctx context.Context, network string, destination M.Socksaddr) (net.Conn, error) { + return d.dialer.DialContext(ctx, network, destination.String()) } -func (d *wgDialer) ListenPacket(ctx context.Context, destination M.Socksaddr) (net.PacketConn, error) { - return dialer.ListenPacket(ctx, dialer.ParseNetwork("udp", destination.Addr), "", d.options...) +func (d *wgSingDialer) ListenPacket(ctx context.Context, destination M.Socksaddr) (net.PacketConn, error) { + return d.dialer.ListenPacket(ctx, "udp", "", destination.AddrPort()) +} + +type wgNetDialer struct { + tunDevice wireguard.Device +} + +var _ dialer.NetDialer = &wgNetDialer{} + +func (d wgNetDialer) DialContext(ctx context.Context, network, address string) (net.Conn, error) { + return d.tunDevice.DialContext(ctx, network, M.ParseSocksaddr(address).Unwrap()) } func NewWireGuard(option WireGuardOption) (*WireGuard, error) { @@ -79,7 +91,7 @@ func NewWireGuard(option WireGuardOption) (*WireGuard, error) { rmark: option.RoutingMark, prefer: C.NewDNSPrefer(option.IPVersion), }, - dialer: &wgDialer{}, + dialer: &wgSingDialer{dialer: dialer.NewDialer()}, } runtime.SetFinalizer(outbound, closeWireGuard) @@ -174,14 +186,14 @@ func NewWireGuard(option WireGuardOption) (*WireGuard, error) { } outbound.device = device.NewDevice(outbound.tunDevice, outbound.bind, &device.Logger{ Verbosef: func(format string, args ...interface{}) { - sing.Logger.Debug(fmt.Sprintf(strings.ToLower(format), args...)) + log.SingLogger.Debug(fmt.Sprintf(strings.ToLower(format), args...)) }, Errorf: func(format string, args ...interface{}) { - sing.Logger.Error(fmt.Sprintf(strings.ToLower(format), args...)) + log.SingLogger.Error(fmt.Sprintf(strings.ToLower(format), args...)) }, }, option.Workers) if debug.Enabled { - sing.Logger.Trace("created wireguard ipc conf: \n", ipcConf) + log.SingLogger.Trace("created wireguard ipc conf: \n", ipcConf) } err = outbound.device.IpcSet(ipcConf) if err != nil { @@ -199,7 +211,8 @@ func closeWireGuard(w *WireGuard) { } func (w *WireGuard) DialContext(ctx context.Context, metadata *C.Metadata, opts ...dialer.Option) (_ C.Conn, err error) { - w.dialer.options = opts + options := w.Base.DialOptions(opts...) + w.dialer.dialer = dialer.NewDialer(options...) var conn net.Conn w.startOnce.Do(func() { w.startErr = w.tunDevice.Start() @@ -208,15 +221,12 @@ func (w *WireGuard) DialContext(ctx context.Context, metadata *C.Metadata, opts return nil, w.startErr } if !metadata.Resolved() { - var addrs []netip.Addr - addrs, err = resolver.LookupIP(ctx, metadata.Host) - if err != nil { - return nil, err - } - conn, err = N.DialSerial(ctx, w.tunDevice, "tcp", M.ParseSocksaddr(metadata.RemoteAddress()), addrs) + options = append(options, dialer.WithResolver(resolver.DefaultResolver)) + options = append(options, dialer.WithNetDialer(wgNetDialer{tunDevice: w.tunDevice})) + conn, err = dialer.NewDialer(options...).DialContext(ctx, "tcp", metadata.RemoteAddress()) } else { port, _ := strconv.Atoi(metadata.DstPort) - conn, err = w.tunDevice.DialContext(ctx, "tcp", M.SocksaddrFrom(metadata.DstIP, uint16(port))) + conn, err = w.tunDevice.DialContext(ctx, "tcp", M.SocksaddrFrom(metadata.DstIP, uint16(port)).Unwrap()) } if err != nil { return nil, err @@ -228,7 +238,8 @@ func (w *WireGuard) DialContext(ctx context.Context, metadata *C.Metadata, opts } func (w *WireGuard) ListenPacketContext(ctx context.Context, metadata *C.Metadata, opts ...dialer.Option) (_ C.PacketConn, err error) { - w.dialer.options = opts + options := w.Base.DialOptions(opts...) + w.dialer.dialer = dialer.NewDialer(options...) var pc net.PacketConn w.startOnce.Do(func() { w.startErr = w.tunDevice.Start() @@ -247,7 +258,7 @@ func (w *WireGuard) ListenPacketContext(ctx context.Context, metadata *C.Metadat metadata.DstIP = ip } port, _ := strconv.Atoi(metadata.DstPort) - pc, err = w.tunDevice.ListenPacket(ctx, M.SocksaddrFrom(metadata.DstIP, uint16(port))) + pc, err = w.tunDevice.ListenPacket(ctx, M.SocksaddrFrom(metadata.DstIP, uint16(port)).Unwrap()) if err != nil { return nil, err } diff --git a/adapter/outboundgroup/fallback.go b/adapter/outboundgroup/fallback.go index 34365d0e4a..d1d5e6b31f 100644 --- a/adapter/outboundgroup/fallback.go +++ b/adapter/outboundgroup/fallback.go @@ -7,6 +7,8 @@ import ( "time" "github.com/Dreamacro/clash/adapter/outbound" + "github.com/Dreamacro/clash/common/callback" + N "github.com/Dreamacro/clash/common/net" "github.com/Dreamacro/clash/component/dialer" C "github.com/Dreamacro/clash/constant" "github.com/Dreamacro/clash/constant/provider" @@ -30,11 +32,23 @@ func (f *Fallback) DialContext(ctx context.Context, metadata *C.Metadata, opts . c, err := proxy.DialContext(ctx, metadata, f.Base.DialOptions(opts...)...) if err == nil { c.AppendToChains(f) - f.onDialSuccess() } else { f.onDialFailed(proxy.Type(), err) } + if N.NeedHandshake(c) { + c = &callback.FirstWriteCallBackConn{ + Conn: c, + Callback: func(err error) { + if err == nil { + f.onDialSuccess() + } else { + f.onDialFailed(proxy.Type(), err) + } + }, + } + } + return c, err } diff --git a/adapter/outboundgroup/loadbalance.go b/adapter/outboundgroup/loadbalance.go index 48bd499428..1ed8049610 100644 --- a/adapter/outboundgroup/loadbalance.go +++ b/adapter/outboundgroup/loadbalance.go @@ -6,11 +6,14 @@ import ( "errors" "fmt" "net" + "sync" "time" "github.com/Dreamacro/clash/adapter/outbound" "github.com/Dreamacro/clash/common/cache" + "github.com/Dreamacro/clash/common/callback" "github.com/Dreamacro/clash/common/murmur3" + N "github.com/Dreamacro/clash/common/net" "github.com/Dreamacro/clash/component/dialer" C "github.com/Dreamacro/clash/constant" "github.com/Dreamacro/clash/constant/provider" @@ -18,7 +21,7 @@ import ( "golang.org/x/net/publicsuffix" ) -type strategyFn = func(proxies []C.Proxy, metadata *C.Metadata) C.Proxy +type strategyFn = func(proxies []C.Proxy, metadata *C.Metadata, touch bool) C.Proxy type LoadBalance struct { *GroupBase @@ -83,17 +86,27 @@ func jumpHash(key uint64, buckets int32) int32 { // DialContext implements C.ProxyAdapter func (lb *LoadBalance) DialContext(ctx context.Context, metadata *C.Metadata, opts ...dialer.Option) (c C.Conn, err error) { proxy := lb.Unwrap(metadata, true) + c, err = proxy.DialContext(ctx, metadata, lb.Base.DialOptions(opts...)...) - defer func() { - if err == nil { - c.AppendToChains(lb) - lb.onDialSuccess() - } else { - lb.onDialFailed(proxy.Type(), err) + if err == nil { + c.AppendToChains(lb) + } else { + lb.onDialFailed(proxy.Type(), err) + } + + if N.NeedHandshake(c) { + c = &callback.FirstWriteCallBackConn{ + Conn: c, + Callback: func(err error) { + if err == nil { + lb.onDialSuccess() + } else { + lb.onDialFailed(proxy.Type(), err) + } + }, } - }() + } - c, err = proxy.DialContext(ctx, metadata, lb.Base.DialOptions(opts...)...) return } @@ -115,22 +128,26 @@ func (lb *LoadBalance) SupportUDP() bool { } func strategyRoundRobin() strategyFn { - flag := true idx := 0 - return func(proxies []C.Proxy, metadata *C.Metadata) C.Proxy { + idxMutex := sync.Mutex{} + return func(proxies []C.Proxy, metadata *C.Metadata, touch bool) C.Proxy { + idxMutex.Lock() + defer idxMutex.Unlock() + + i := 0 length := len(proxies) - for i := 0; i < length; i++ { - flag = !flag - if flag { - idx = (idx - 1) % length - } else { - idx = (idx + 2) % length - } - if idx < 0 { - idx = idx + length - } - proxy := proxies[idx] + + if touch { + defer func() { + idx = (idx + i) % length + }() + } + + for ; i < length; i++ { + id := (idx + i) % length + proxy := proxies[id] if proxy.Alive() { + i++ return proxy } } @@ -141,7 +158,7 @@ func strategyRoundRobin() strategyFn { func strategyConsistentHashing() strategyFn { maxRetry := 5 - return func(proxies []C.Proxy, metadata *C.Metadata) C.Proxy { + return func(proxies []C.Proxy, metadata *C.Metadata, touch bool) C.Proxy { key := uint64(murmur3.Sum32([]byte(getKey(metadata)))) buckets := int32(len(proxies)) for i := 0; i < maxRetry; i, key = i+1, key+1 { @@ -169,7 +186,7 @@ func strategyStickySessions() strategyFn { lruCache := cache.New[uint64, int]( cache.WithAge[uint64, int](int64(ttl.Seconds())), cache.WithSize[uint64, int](1000)) - return func(proxies []C.Proxy, metadata *C.Metadata) C.Proxy { + return func(proxies []C.Proxy, metadata *C.Metadata, touch bool) C.Proxy { key := uint64(murmur3.Sum32([]byte(getKeyWithSrcAndDst(metadata)))) length := len(proxies) idx, has := lruCache.Get(key) @@ -201,7 +218,7 @@ func strategyStickySessions() strategyFn { // Unwrap implements C.ProxyAdapter func (lb *LoadBalance) Unwrap(metadata *C.Metadata, touch bool) C.Proxy { proxies := lb.GetProxies(touch) - return lb.strategyFn(proxies, metadata) + return lb.strategyFn(proxies, metadata, touch) } // MarshalJSON implements C.ProxyAdapter diff --git a/adapter/outboundgroup/relay.go b/adapter/outboundgroup/relay.go index b466b1ff2b..e17ae13201 100644 --- a/adapter/outboundgroup/relay.go +++ b/adapter/outboundgroup/relay.go @@ -176,7 +176,7 @@ func (r *Relay) proxies(metadata *C.Metadata, touch bool) ([]C.Proxy, []C.Proxy) } func (r *Relay) Addr() string { - proxies, _ := r.proxies(nil, true) + proxies, _ := r.proxies(nil, false) return proxies[len(proxies)-1].Addr() } diff --git a/adapter/outboundgroup/urltest.go b/adapter/outboundgroup/urltest.go index 27cef9c64f..d340539cd2 100644 --- a/adapter/outboundgroup/urltest.go +++ b/adapter/outboundgroup/urltest.go @@ -6,6 +6,8 @@ import ( "time" "github.com/Dreamacro/clash/adapter/outbound" + "github.com/Dreamacro/clash/common/callback" + N "github.com/Dreamacro/clash/common/net" "github.com/Dreamacro/clash/common/singledo" "github.com/Dreamacro/clash/component/dialer" C "github.com/Dreamacro/clash/constant" @@ -38,10 +40,23 @@ func (u *URLTest) DialContext(ctx context.Context, metadata *C.Metadata, opts .. c, err = proxy.DialContext(ctx, metadata, u.Base.DialOptions(opts...)...) if err == nil { c.AppendToChains(u) - u.onDialSuccess() } else { u.onDialFailed(proxy.Type(), err) } + + if N.NeedHandshake(c) { + c = &callback.FirstWriteCallBackConn{ + Conn: c, + Callback: func(err error) { + if err == nil { + u.onDialSuccess() + } else { + u.onDialFailed(proxy.Type(), err) + } + }, + } + } + return c, err } diff --git a/adapter/parser.go b/adapter/parser.go index d9c1869479..1f626f33b2 100644 --- a/adapter/parser.go +++ b/adapter/parser.go @@ -23,7 +23,7 @@ func ParseProxy(mapping map[string]any) (C.Proxy, error) { ) switch proxyType { case "ss": - ssOption := &outbound.ShadowSocksOption{} + ssOption := &outbound.ShadowSocksOption{ClientFingerprint: tlsC.GetGlobalFingerprint()} err = decoder.Decode(mapping, ssOption) if err != nil { break @@ -56,10 +56,7 @@ func ParseProxy(mapping map[string]any) (C.Proxy, error) { Method: "GET", Path: []string{"/"}, }, - } - - if GlobalUtlsClient := tlsC.GetGlobalFingerprint(); len(GlobalUtlsClient) != 0 { - vmessOption.ClientFingerprint = GlobalUtlsClient + ClientFingerprint: tlsC.GetGlobalFingerprint(), } err = decoder.Decode(mapping, vmessOption) @@ -68,12 +65,7 @@ func ParseProxy(mapping map[string]any) (C.Proxy, error) { } proxy, err = outbound.NewVmess(*vmessOption) case "vless": - vlessOption := &outbound.VlessOption{} - - if GlobalUtlsClient := tlsC.GetGlobalFingerprint(); len(GlobalUtlsClient) != 0 { - vlessOption.ClientFingerprint = GlobalUtlsClient - } - + vlessOption := &outbound.VlessOption{ClientFingerprint: tlsC.GetGlobalFingerprint()} err = decoder.Decode(mapping, vlessOption) if err != nil { break @@ -87,12 +79,7 @@ func ParseProxy(mapping map[string]any) (C.Proxy, error) { } proxy, err = outbound.NewSnell(*snellOption) case "trojan": - trojanOption := &outbound.TrojanOption{} - - if GlobalUtlsClient := tlsC.GetGlobalFingerprint(); len(GlobalUtlsClient) != 0 { - trojanOption.ClientFingerprint = GlobalUtlsClient - } - + trojanOption := &outbound.TrojanOption{ClientFingerprint: tlsC.GetGlobalFingerprint()} err = decoder.Decode(mapping, trojanOption) if err != nil { break diff --git a/adapter/provider/healthcheck.go b/adapter/provider/healthcheck.go index 16b9ad6114..9deb7b8202 100644 --- a/adapter/provider/healthcheck.go +++ b/adapter/provider/healthcheck.go @@ -6,10 +6,10 @@ import ( "github.com/Dreamacro/clash/common/batch" "github.com/Dreamacro/clash/common/singledo" + "github.com/Dreamacro/clash/common/utils" C "github.com/Dreamacro/clash/constant" "github.com/Dreamacro/clash/log" - "github.com/gofrs/uuid" "go.uber.org/atomic" ) @@ -34,16 +34,15 @@ type HealthCheck struct { func (hc *HealthCheck) process() { ticker := time.NewTicker(time.Duration(hc.interval) * time.Second) - - go func() { - time.Sleep(30 * time.Second) - hc.lazyCheck() - }() - for { select { case <-ticker.C: - hc.lazyCheck() + now := time.Now().Unix() + if !hc.lazy || now-hc.lastTouch.Load() < int64(hc.interval) { + hc.check() + } else { + log.Debugln("Skip once health check because we are lazy") + } case <-hc.done: ticker.Stop() return @@ -51,17 +50,6 @@ func (hc *HealthCheck) process() { } } -func (hc *HealthCheck) lazyCheck() bool { - now := time.Now().Unix() - if !hc.lazy || now-hc.lastTouch.Load() < int64(hc.interval) { - hc.check() - return true - } else { - log.Debugln("Skip once health check because we are lazy") - return false - } -} - func (hc *HealthCheck) setProxy(proxies []C.Proxy) { hc.proxies = proxies } @@ -76,10 +64,7 @@ func (hc *HealthCheck) touch() { func (hc *HealthCheck) check() { _, _, _ = hc.singleDo.Do(func() (struct{}, error) { - id := "" - if uid, err := uuid.NewV4(); err == nil { - id = uid.String() - } + id := utils.NewUUIDV4().String() log.Debugln("Start New Health Checking {%s}", id) b, _ := batch.New[bool](context.Background(), batch.WithConcurrencyNum[bool](10)) for _, proxy := range hc.proxies { diff --git a/adapter/provider/provider.go b/adapter/provider/provider.go index e8bd7ed1f9..4a2cf7b87b 100644 --- a/adapter/provider/provider.go +++ b/adapter/provider/provider.go @@ -80,6 +80,7 @@ func (pp *proxySetProvider) Initial() error { return err } pp.OnUpdate(elm) + pp.getSubscriptionInfo() return nil } @@ -99,7 +100,7 @@ func (pp *proxySetProvider) setProxies(proxies []C.Proxy) { pp.proxies = proxies pp.healthCheck.setProxy(proxies) if pp.healthCheck.auto() { - defer func() { go pp.healthCheck.lazyCheck() }() + go pp.healthCheck.check() } } @@ -172,8 +173,6 @@ func NewProxySetProvider(name string, interval time.Duration, filter string, exc fetcher := resource.NewFetcher[[]C.Proxy](name, interval, vehicle, proxiesParseAndFilter(filter, excludeFilter, excludeTypeArray, filterRegs, excludeFilterReg), proxiesOnUpdate(pd)) pd.Fetcher = fetcher - - pd.getSubscriptionInfo() wrapper := &ProxySetProvider{pd} runtime.SetFinalizer(wrapper, stopProxyProvider) return wrapper, nil diff --git a/common/buf/sing.go b/common/buf/sing.go index b5e015f59f..f86b57554b 100644 --- a/common/buf/sing.go +++ b/common/buf/sing.go @@ -5,9 +5,15 @@ import ( "github.com/sagernet/sing/common/buf" ) +const BufferSize = buf.BufferSize + type Buffer = buf.Buffer +var New = buf.New +var StackNew = buf.StackNew var StackNewSize = buf.StackNewSize +var With = buf.With + var KeepAlive = common.KeepAlive //go:norace diff --git a/common/callback/callback.go b/common/callback/callback.go new file mode 100644 index 0000000000..a0f1e71790 --- /dev/null +++ b/common/callback/callback.go @@ -0,0 +1,25 @@ +package callback + +import ( + C "github.com/Dreamacro/clash/constant" +) + +type FirstWriteCallBackConn struct { + C.Conn + Callback func(error) + written bool +} + +func (c *FirstWriteCallBackConn) Write(b []byte) (n int, err error) { + defer func() { + if !c.written { + c.written = true + c.Callback(err) + } + }() + return c.Conn.Write(b) +} + +func (c *FirstWriteCallBackConn) Upstream() any { + return c.Conn +} diff --git a/common/convert/converter.go b/common/convert/converter.go index 7d896d5364..abd07a943b 100644 --- a/common/convert/converter.go +++ b/common/convert/converter.go @@ -83,7 +83,7 @@ func ConvertsV2Ray(buf []byte) ([]map[string]any, error) { trojan["port"] = urlTrojan.Port() trojan["password"] = urlTrojan.User.Username() trojan["udp"] = true - trojan["skip-cert-verify"] = false + trojan["skip-cert-verify"], _ = strconv.ParseBool(query.Get("allowInsecure")) sni := query.Get("sni") if sni != "" { diff --git a/common/convert/util.go b/common/convert/util.go index 03a48ecdb1..0ec35acdd7 100644 --- a/common/convert/util.go +++ b/common/convert/util.go @@ -2,12 +2,14 @@ package convert import ( "encoding/base64" - "github.com/metacubex/sing-shadowsocks/shadowimpl" - "math/rand" "net/http" "strings" + "time" + + "github.com/Dreamacro/clash/common/utils" - "github.com/gofrs/uuid" + "github.com/metacubex/sing-shadowsocks/shadowimpl" + "github.com/zhangyunhao116/fastrand" ) var hostsSuffix = []string{ @@ -292,8 +294,7 @@ var ( ) func RandHost() string { - id, _ := uuid.NewV4() - base := strings.ToLower(base64.RawURLEncoding.EncodeToString(id.Bytes())) + base := strings.ToLower(base64.RawURLEncoding.EncodeToString(utils.NewUUIDV4().Bytes())) base = strings.ReplaceAll(base, "-", "") base = strings.ReplaceAll(base, "_", "") buf := []byte(base) @@ -301,11 +302,11 @@ func RandHost() string { prefix += string(buf[6:8]) + "-" prefix += string(buf[len(buf)-8:]) - return prefix + hostsSuffix[rand.Intn(hostsLen)] + return prefix + hostsSuffix[fastrand.Intn(hostsLen)] } func RandUserAgent() string { - return userAgents[rand.Intn(uaLen)] + return userAgents[fastrand.Intn(uaLen)] } func SetUserAgent(header http.Header) { @@ -317,6 +318,6 @@ func SetUserAgent(header http.Header) { } func VerifyMethod(cipher, password string) (err error) { - _, err = shadowimpl.FetchMethod(cipher, password) + _, err = shadowimpl.FetchMethod(cipher, password, time.Now) return } diff --git a/common/convert/v.go b/common/convert/v.go index 606d8affc8..23949aab54 100644 --- a/common/convert/v.go +++ b/common/convert/v.go @@ -27,7 +27,7 @@ func handleVShareLink(names map[string]int, url *url.URL, scheme string, proxy m proxy["skip-cert-verify"] = false proxy["tls"] = false tls := strings.ToLower(query.Get("security")) - if strings.HasSuffix(tls, "tls") { + if strings.HasSuffix(tls, "tls") || tls == "reality" { proxy["tls"] = true if fingerprint := query.Get("fp"); fingerprint == "" { proxy["client-fingerprint"] = "chrome" @@ -38,6 +38,12 @@ func handleVShareLink(names map[string]int, url *url.URL, scheme string, proxy m if sni := query.Get("sni"); sni != "" { proxy["servername"] = sni } + if realityPublicKey := query.Get("pbk"); realityPublicKey != "" { + proxy["reality-opts"] = map[string]any{ + "public-key": realityPublicKey, + "short-id": query.Get("sid"), + } + } switch query.Get("packetEncoding") { case "none": diff --git a/common/net/addr.go b/common/net/addr.go new file mode 100644 index 0000000000..4efaefcd38 --- /dev/null +++ b/common/net/addr.go @@ -0,0 +1,36 @@ +package net + +import ( + "net" +) + +type CustomAddr interface { + net.Addr + RawAddr() net.Addr +} + +type customAddr struct { + networkStr string + addrStr string + rawAddr net.Addr +} + +func (a customAddr) Network() string { + return a.networkStr +} + +func (a customAddr) String() string { + return a.addrStr +} + +func (a customAddr) RawAddr() net.Addr { + return a.rawAddr +} + +func NewCustomAddr(networkStr string, addrStr string, rawAddr net.Addr) CustomAddr { + return customAddr{ + networkStr: networkStr, + addrStr: addrStr, + rawAddr: rawAddr, + } +} diff --git a/common/net/bufconn.go b/common/net/bufconn.go index ba0ca02638..8087608ca1 100644 --- a/common/net/bufconn.go +++ b/common/net/bufconn.go @@ -12,13 +12,14 @@ var _ ExtendedConn = (*BufferedConn)(nil) type BufferedConn struct { r *bufio.Reader ExtendedConn + peeked bool } func NewBufferedConn(c net.Conn) *BufferedConn { if bc, ok := c.(*BufferedConn); ok { return bc } - return &BufferedConn{bufio.NewReader(c), NewExtendedConn(c)} + return &BufferedConn{bufio.NewReader(c), NewExtendedConn(c), false} } // Reader returns the internal bufio.Reader. @@ -26,11 +27,24 @@ func (c *BufferedConn) Reader() *bufio.Reader { return c.r } +func (c *BufferedConn) ResetPeeked() { + c.peeked = false +} + +func (c *BufferedConn) Peeked() bool { + return c.peeked +} + // Peek returns the next n bytes without advancing the reader. func (c *BufferedConn) Peek(n int) ([]byte, error) { + c.peeked = true return c.r.Peek(n) } +func (c *BufferedConn) Discard(n int) (discarded int, err error) { + return c.r.Discard(n) +} + func (c *BufferedConn) Read(p []byte) (int, error) { return c.r.Read(p) } diff --git a/common/net/sing.go b/common/net/sing.go index 342f2e9581..5c98073857 100644 --- a/common/net/sing.go +++ b/common/net/sing.go @@ -4,6 +4,7 @@ import ( "context" "net" + "github.com/sagernet/sing/common" "github.com/sagernet/sing/common/bufio" "github.com/sagernet/sing/common/network" ) @@ -16,6 +17,13 @@ type ExtendedConn = network.ExtendedConn type ExtendedWriter = network.ExtendedWriter type ExtendedReader = network.ExtendedReader +func NeedHandshake(conn any) bool { + if earlyConn, isEarlyConn := common.Cast[network.EarlyConn](conn); isEarlyConn && earlyConn.NeedHandshake() { + return true + } + return false +} + // Relay copies between left and right bidirectionally. func Relay(leftConn, rightConn net.Conn) { _ = bufio.CopyConn(context.TODO(), leftConn, rightConn) diff --git a/common/pool/alloc_test.go b/common/pool/alloc_test.go index 3d0633156a..30aa5c538e 100644 --- a/common/pool/alloc_test.go +++ b/common/pool/alloc_test.go @@ -1,10 +1,10 @@ package pool import ( - "math/rand" "testing" "github.com/stretchr/testify/assert" + "github.com/zhangyunhao116/fastrand" ) func TestAllocGet(t *testing.T) { @@ -43,6 +43,6 @@ func TestAllocPutThenGet(t *testing.T) { func BenchmarkMSB(b *testing.B) { for i := 0; i < b.N; i++ { - msb(rand.Int()) + msb(fastrand.Int()) } } diff --git a/common/utils/must.go b/common/utils/must.go new file mode 100644 index 0000000000..2dd5ff5ee3 --- /dev/null +++ b/common/utils/must.go @@ -0,0 +1,8 @@ +package utils + +func MustOK[T any](result T, ok bool) T { + if ok { + return result + } + panic("operation failed") +} diff --git a/common/utils/slice.go b/common/utils/slice.go new file mode 100644 index 0000000000..1b0fa4940d --- /dev/null +++ b/common/utils/slice.go @@ -0,0 +1,34 @@ +package utils + +import ( + "errors" + "fmt" + "reflect" +) + +func Filter[T comparable](tSlice []T, filter func(t T) bool) []T { + result := make([]T, 0) + for _, t := range tSlice { + if filter(t) { + result = append(result, t) + } + } + return result +} + +func ToStringSlice(value any) ([]string, error) { + strArr := make([]string, 0) + switch reflect.TypeOf(value).Kind() { + case reflect.Slice, reflect.Array: + origin := reflect.ValueOf(value) + for i := 0; i < origin.Len(); i++ { + item := fmt.Sprintf("%v", origin.Index(i)) + strArr = append(strArr, item) + } + case reflect.String: + strArr = append(strArr, fmt.Sprintf("%v", value)) + default: + return nil, errors.New("value format error, must be string or array") + } + return strArr, nil +} diff --git a/common/utils/uuid.go b/common/utils/uuid.go index 66e176edd8..930fda7d8b 100644 --- a/common/utils/uuid.go +++ b/common/utils/uuid.go @@ -2,15 +2,50 @@ package utils import ( "github.com/gofrs/uuid" + "github.com/zhangyunhao116/fastrand" ) -var uuidNamespace, _ = uuid.FromString("00000000-0000-0000-0000-000000000000") +type fastRandReader struct{} + +func (r fastRandReader) Read(p []byte) (int, error) { + return fastrand.Read(p) +} + +var UnsafeUUIDGenerator = uuid.NewGenWithOptions(uuid.WithRandomReader(fastRandReader{})) + +func NewUUIDV1() uuid.UUID { + u, _ := UnsafeUUIDGenerator.NewV1() // fastrand.Read wouldn't cause error, so ignore err is safe + return u +} + +func NewUUIDV3(ns uuid.UUID, name string) uuid.UUID { + return UnsafeUUIDGenerator.NewV3(ns, name) +} + +func NewUUIDV4() uuid.UUID { + u, _ := UnsafeUUIDGenerator.NewV4() // fastrand.Read wouldn't cause error, so ignore err is safe + return u +} + +func NewUUIDV5(ns uuid.UUID, name string) uuid.UUID { + return UnsafeUUIDGenerator.NewV5(ns, name) +} + +func NewUUIDV6() uuid.UUID { + u, _ := UnsafeUUIDGenerator.NewV6() // fastrand.Read wouldn't cause error, so ignore err is safe + return u +} + +func NewUUIDV7() uuid.UUID { + u, _ := UnsafeUUIDGenerator.NewV7() // fastrand.Read wouldn't cause error, so ignore err is safe + return u +} // UUIDMap https://github.com/XTLS/Xray-core/issues/158#issue-783294090 func UUIDMap(str string) (uuid.UUID, error) { u, err := uuid.FromString(str) if err != nil { - return uuid.NewV5(uuidNamespace, str), nil + return NewUUIDV5(uuid.Nil, str), nil } return u, nil } diff --git a/component/dialer/bind_windows.go b/component/dialer/bind_windows.go index b680e90fe5..4a0991699a 100644 --- a/component/dialer/bind_windows.go +++ b/component/dialer/bind_windows.go @@ -37,14 +37,25 @@ func bindControl(ifaceIdx int) controlFn { var innerErr error err = c.Control(func(fd uintptr) { handle := syscall.Handle(fd) + bind6err := bind6(handle, ifaceIdx) + bind4err := bind4(handle, ifaceIdx) switch network { - case "tcp6", "udp6": - innerErr = bind6(handle, ifaceIdx) - _ = bind4(handle, ifaceIdx) - default: - innerErr = bind4(handle, ifaceIdx) - // try bind ipv6, if failed, ignore. it's a workaround for windows disable interface ipv6 - _ = bind6(handle, ifaceIdx) + case "ip6", "tcp6": + innerErr = bind6err + case "ip4", "tcp4", "udp4": + innerErr = bind4err + case "udp6": + // golang will set network to udp6 when listenUDP on wildcard ip (eg: ":0", "") + if (!addrPort.Addr().IsValid() || addrPort.Addr().IsUnspecified()) && bind6err != nil { + // try bind ipv6, if failed, ignore. it's a workaround for windows disable interface ipv6 + if bind4err != nil { + innerErr = bind6err + } else { + innerErr = bind4err + } + } else { + innerErr = bind6err + } } }) diff --git a/component/dialer/dialer.go b/component/dialer/dialer.go index e31936e993..d70e917321 100644 --- a/component/dialer/dialer.go +++ b/component/dialer/dialer.go @@ -2,26 +2,25 @@ package dialer import ( "context" - "errors" "fmt" "net" "net/netip" + "os" "strings" "sync" + "time" "github.com/Dreamacro/clash/component/resolver" - - "go.uber.org/atomic" ) +type dialFunc func(ctx context.Context, network string, ips []netip.Addr, port string, opt *option) (net.Conn, error) + var ( - dialMux sync.Mutex - actualSingleDialContext = singleDialContext - actualDualStackDialContext = dualStackDialContext - tcpConcurrent = false - DisableIPv6 = false - ErrorInvalidedNetworkStack = errors.New("invalided network stack") - ErrorDisableIPv6 = errors.New("IPv6 is disabled, dialer cancel") + dialMux sync.Mutex + actualSingleStackDialContext = serialSingleStackDialContext + actualDualStackDialContext = serialDualStackDialContext + tcpConcurrent = false + fallbackTimeout = 300 * time.Millisecond ) func applyOptions(options ...Option) *option { @@ -54,29 +53,23 @@ func DialContext(ctx context.Context, network, address string, options ...Option network = fmt.Sprintf("%s%d", network, opt.network) } + ips, port, err := parseAddr(ctx, network, address, opt.resolver) + if err != nil { + return nil, err + } + switch network { case "tcp4", "tcp6", "udp4", "udp6": - return actualSingleDialContext(ctx, network, address, opt) + return actualSingleStackDialContext(ctx, network, ips, port, opt) case "tcp", "udp": - return actualDualStackDialContext(ctx, network, address, opt) + return actualDualStackDialContext(ctx, network, ips, port, opt) default: return nil, ErrorInvalidedNetworkStack } } func ListenPacket(ctx context.Context, network, address string, options ...Option) (net.PacketConn, error) { - cfg := &option{ - interfaceName: DefaultInterface.Load(), - routingMark: int(DefaultRoutingMark.Load()), - } - - for _, o := range DefaultOptions { - o(cfg) - } - - for _, o := range options { - o(cfg) - } + cfg := applyOptions(options...) lc := &net.ListenConfig{} if cfg.interfaceName != "" { @@ -96,26 +89,40 @@ func ListenPacket(ctx context.Context, network, address string, options ...Optio return lc.ListenPacket(ctx, network, address) } -func SetDial(concurrent bool) { +func SetTcpConcurrent(concurrent bool) { dialMux.Lock() + defer dialMux.Unlock() tcpConcurrent = concurrent if concurrent { - actualSingleDialContext = concurrentSingleDialContext + actualSingleStackDialContext = concurrentSingleStackDialContext actualDualStackDialContext = concurrentDualStackDialContext } else { - actualSingleDialContext = singleDialContext - actualDualStackDialContext = dualStackDialContext + actualSingleStackDialContext = serialSingleStackDialContext + actualDualStackDialContext = serialDualStackDialContext } - - dialMux.Unlock() } -func GetDial() bool { +func GetTcpConcurrent() bool { + dialMux.Lock() + defer dialMux.Unlock() return tcpConcurrent } func dialContext(ctx context.Context, network string, destination netip.Addr, port string, opt *option) (net.Conn, error) { - dialer := &net.Dialer{} + address := net.JoinHostPort(destination.String(), port) + + netDialer := opt.netDialer + switch netDialer.(type) { + case nil: + netDialer = &net.Dialer{} + case *net.Dialer: + _netDialer := *netDialer.(*net.Dialer) + netDialer = &_netDialer // make a copy + default: + return netDialer.DialContext(ctx, network, address) + } + + dialer := netDialer.(*net.Dialer) if opt.interfaceName != "" { if err := bindIfaceToDialer(opt.interfaceName, dialer, network, destination); err != nil { return nil, err @@ -124,328 +131,213 @@ func dialContext(ctx context.Context, network string, destination netip.Addr, po if opt.routingMark != 0 { bindMarkToDialer(opt.routingMark, dialer, network, destination) } - - if DisableIPv6 && destination.Is6() { - return nil, ErrorDisableIPv6 + if opt.tfo { + return dialTFO(ctx, *dialer, network, address) } + return dialer.DialContext(ctx, network, address) +} - return dialer.DialContext(ctx, network, net.JoinHostPort(destination.String(), port)) +func serialSingleStackDialContext(ctx context.Context, network string, ips []netip.Addr, port string, opt *option) (net.Conn, error) { + return serialDialContext(ctx, network, ips, port, opt) } -func dualStackDialContext(ctx context.Context, network, address string, opt *option) (net.Conn, error) { - host, port, err := net.SplitHostPort(address) - if err != nil { - return nil, err - } +func serialDualStackDialContext(ctx context.Context, network string, ips []netip.Addr, port string, opt *option) (net.Conn, error) { + return dualStackDialContext(ctx, serialDialContext, network, ips, port, opt) +} - returned := make(chan struct{}) - defer close(returned) +func concurrentSingleStackDialContext(ctx context.Context, network string, ips []netip.Addr, port string, opt *option) (net.Conn, error) { + return parallelDialContext(ctx, network, ips, port, opt) +} - type dialResult struct { - net.Conn - error - resolved bool - ipv6 bool - done bool +func concurrentDualStackDialContext(ctx context.Context, network string, ips []netip.Addr, port string, opt *option) (net.Conn, error) { + if opt.prefer != 4 && opt.prefer != 6 { + return parallelDialContext(ctx, network, ips, port, opt) } - results := make(chan dialResult) - var primary, fallback dialResult + return dualStackDialContext(ctx, parallelDialContext, network, ips, port, opt) +} - startRacer := func(ctx context.Context, network, host string, r resolver.Resolver, ipv6 bool) { - result := dialResult{ipv6: ipv6, done: true} +func dualStackDialContext(ctx context.Context, dialFn dialFunc, network string, ips []netip.Addr, port string, opt *option) (net.Conn, error) { + ipv4s, ipv6s := sortationAddr(ips) + preferIPVersion := opt.prefer + + fallbackTicker := time.NewTicker(fallbackTimeout) + defer fallbackTicker.Stop() + results := make(chan dialResult) + returned := make(chan struct{}) + defer close(returned) + racer := func(ips []netip.Addr, isPrimary bool) { + result := dialResult{isPrimary: isPrimary} defer func() { select { case results <- result: case <-returned: - if result.Conn != nil { + if result.Conn != nil && result.error == nil { _ = result.Conn.Close() } } }() - - var ip netip.Addr - if ipv6 { - if r == nil { - ip, result.error = resolver.ResolveIPv6ProxyServerHost(ctx, host) - } else { - ip, result.error = resolver.ResolveIPv6WithResolver(ctx, host, r) - } - } else { - if r == nil { - ip, result.error = resolver.ResolveIPv4ProxyServerHost(ctx, host) - } else { - ip, result.error = resolver.ResolveIPv4WithResolver(ctx, host, r) - } - } - if result.error != nil { - return - } - result.resolved = true - - result.Conn, result.error = dialContext(ctx, network, ip, port, opt) + result.Conn, result.error = dialFn(ctx, network, ips, port, opt) } - - go startRacer(ctx, network+"4", host, opt.resolver, false) - go startRacer(ctx, network+"6", host, opt.resolver, true) - - count := 2 - for i := 0; i < count; i++ { + go racer(ipv4s, preferIPVersion != 6) + go racer(ipv6s, preferIPVersion != 4) + var fallback dialResult + var errs []error + for i := 0; i < 2; { select { + case <-fallbackTicker.C: + if fallback.error == nil && fallback.Conn != nil { + return fallback.Conn, nil + } case res := <-results: + i++ if res.error == nil { - return res.Conn, nil - } - - if !res.ipv6 { - primary = res - } else { + if res.isPrimary { + return res.Conn, nil + } fallback = res - } - - if primary.done && fallback.done { - if primary.resolved { - return nil, primary.error - } else if fallback.resolved { - return nil, fallback.error + } else { + if res.isPrimary { + errs = append([]error{fmt.Errorf("connect failed: %w", res.error)}, errs...) } else { - return nil, primary.error + errs = append(errs, fmt.Errorf("connect failed: %w", res.error)) } } - case <-ctx.Done(): - err = ctx.Err() - break } } - - if err == nil { - err = fmt.Errorf("dual stack dial failed") - } else { - err = fmt.Errorf("dual stack dial failed:%w", err) + if fallback.error == nil && fallback.Conn != nil { + return fallback.Conn, nil } - return nil, err + return nil, errorsJoin(errs...) } -func concurrentDualStackDialContext(ctx context.Context, network, address string, opt *option) (net.Conn, error) { - host, port, err := net.SplitHostPort(address) - if err != nil { - return nil, err - } - - var ips []netip.Addr - if opt.resolver != nil { - ips, err = resolver.LookupIPWithResolver(ctx, host, opt.resolver) - } else { - ips, err = resolver.LookupIPProxyServerHost(ctx, host) - } - - if err != nil { - return nil, err +func parallelDialContext(ctx context.Context, network string, ips []netip.Addr, port string, opt *option) (net.Conn, error) { + if len(ips) == 0 { + return nil, ErrorNoIpAddress } - - return concurrentDialContext(ctx, network, ips, port, opt) -} - -func concurrentDialContext(ctx context.Context, network string, ips []netip.Addr, port string, opt *option) (net.Conn, error) { + results := make(chan dialResult) returned := make(chan struct{}) defer close(returned) - - type dialResult struct { - ip netip.Addr - net.Conn - error - isPrimary bool - done bool - } - - preferCount := atomic.NewInt32(0) - results := make(chan dialResult) - tcpRacer := func(ctx context.Context, ip netip.Addr) { - result := dialResult{ip: ip, done: true} - + racer := func(ctx context.Context, ip netip.Addr) { + result := dialResult{isPrimary: true, ip: ip} defer func() { select { case results <- result: case <-returned: - if result.Conn != nil { + if result.Conn != nil && result.error == nil { _ = result.Conn.Close() } } }() - if strings.Contains(network, "tcp") { - network = "tcp" - } else { - network = "udp" - } - - if ip.Is6() { - network += "6" - if opt.prefer != 4 { - result.isPrimary = true - } - } - - if ip.Is4() { - network += "4" - if opt.prefer != 6 { - result.isPrimary = true - } - } - - if result.isPrimary { - preferCount.Add(1) - } - result.Conn, result.error = dialContext(ctx, network, ip, port, opt) } for _, ip := range ips { - go tcpRacer(ctx, ip) + go racer(ctx, ip) } - - connCount := len(ips) - var fallback dialResult - var primaryError error - var finalError error - for i := 0; i < connCount; i++ { - select { - case res := <-results: - if res.error == nil { - if res.isPrimary { - return res.Conn, nil - } else { - if !fallback.done || fallback.error != nil { - fallback = res - } - } - } else { - if res.isPrimary { - primaryError = res.error - preferCount.Add(-1) - if preferCount.Load() == 0 && fallback.done && fallback.error == nil { - return fallback.Conn, nil - } - } - } - case <-ctx.Done(): - if fallback.done && fallback.error == nil { - return fallback.Conn, nil - } - finalError = ctx.Err() - break + var errs []error + for i := 0; i < len(ips); i++ { + res := <-results + if res.error == nil { + return res.Conn, nil } + errs = append(errs, res.error) } - if fallback.done && fallback.error == nil { - return fallback.Conn, nil - } - - if primaryError != nil { - return nil, primaryError + if len(errs) > 0 { + return nil, errorsJoin(errs...) } + return nil, os.ErrDeadlineExceeded +} - if fallback.error != nil { - return nil, fallback.error +func serialDialContext(ctx context.Context, network string, ips []netip.Addr, port string, opt *option) (net.Conn, error) { + if len(ips) == 0 { + return nil, ErrorNoIpAddress } - - if finalError == nil { - finalError = fmt.Errorf("all ips %v tcp shake hands failed", ips) - } else { - finalError = fmt.Errorf("concurrent dial failed:%w", finalError) + var errs []error + for _, ip := range ips { + if conn, err := dialContext(ctx, network, ip, port, opt); err == nil { + return conn, nil + } else { + errs = append(errs, err) + } } + return nil, errorsJoin(errs...) +} - return nil, finalError +type dialResult struct { + ip netip.Addr + net.Conn + error + isPrimary bool } -func singleDialContext(ctx context.Context, network string, address string, opt *option) (net.Conn, error) { +func parseAddr(ctx context.Context, network, address string, preferResolver resolver.Resolver) ([]netip.Addr, string, error) { host, port, err := net.SplitHostPort(address) if err != nil { - return nil, err + return nil, "-1", err } - var ip netip.Addr + var ips []netip.Addr switch network { case "tcp4", "udp4": - if opt.resolver == nil { - ip, err = resolver.ResolveIPv4ProxyServerHost(ctx, host) + if preferResolver == nil { + ips, err = resolver.LookupIPv4ProxyServerHost(ctx, host) } else { - ip, err = resolver.ResolveIPv4WithResolver(ctx, host, opt.resolver) + ips, err = resolver.LookupIPv4WithResolver(ctx, host, preferResolver) } - default: - if opt.resolver == nil { - ip, err = resolver.ResolveIPv6ProxyServerHost(ctx, host) + case "tcp6", "udp6": + if preferResolver == nil { + ips, err = resolver.LookupIPv6ProxyServerHost(ctx, host) } else { - ip, err = resolver.ResolveIPv6WithResolver(ctx, host, opt.resolver) + ips, err = resolver.LookupIPv6WithResolver(ctx, host, preferResolver) } - } - if err != nil { - return nil, err - } - - return dialContext(ctx, network, ip, port, opt) -} - -func concurrentSingleDialContext(ctx context.Context, network string, address string, opt *option) (net.Conn, error) { - switch network { - case "tcp4", "udp4": - return concurrentIPv4DialContext(ctx, network, address, opt) default: - return concurrentIPv6DialContext(ctx, network, address, opt) + if preferResolver == nil { + ips, err = resolver.LookupIPProxyServerHost(ctx, host) + } else { + ips, err = resolver.LookupIPWithResolver(ctx, host, preferResolver) + } } -} - -func concurrentIPv4DialContext(ctx context.Context, network, address string, opt *option) (net.Conn, error) { - host, port, err := net.SplitHostPort(address) if err != nil { - return nil, err - } - - var ips []netip.Addr - if opt.resolver == nil { - ips, err = resolver.LookupIPv4ProxyServerHost(ctx, host) - } else { - ips, err = resolver.LookupIPv4WithResolver(ctx, host, opt.resolver) + return nil, "-1", fmt.Errorf("dns resolve failed: %w", err) } - - if err != nil { - return nil, err + for i, ip := range ips { + if ip.Is4In6() { + ips[i] = ip.Unmap() + } } - - return concurrentDialContext(ctx, network, ips, port, opt) + return ips, port, nil } -func concurrentIPv6DialContext(ctx context.Context, network, address string, opt *option) (net.Conn, error) { - host, port, err := net.SplitHostPort(address) - if err != nil { - return nil, err - } - - var ips []netip.Addr - if opt.resolver == nil { - ips, err = resolver.LookupIPv6ProxyServerHost(ctx, host) - } else { - ips, err = resolver.LookupIPv6WithResolver(ctx, host, opt.resolver) - } - - if err != nil { - return nil, err +func sortationAddr(ips []netip.Addr) (ipv4s, ipv6s []netip.Addr) { + for _, v := range ips { + if v.Is4() { // 4in6 parse was in parseAddr + ipv4s = append(ipv4s, v) + } else { + ipv6s = append(ipv6s, v) + } } - - return concurrentDialContext(ctx, network, ips, port, opt) + return } type Dialer struct { - Opt option + opt option } func (d Dialer) DialContext(ctx context.Context, network, address string) (net.Conn, error) { - return DialContext(ctx, network, address, WithOption(d.Opt)) + return DialContext(ctx, network, address, WithOption(d.opt)) } func (d Dialer) ListenPacket(ctx context.Context, network, address string, rAddrPort netip.AddrPort) (net.PacketConn, error) { - return ListenPacket(ctx, ParseNetwork(network, rAddrPort.Addr()), address, WithOption(d.Opt)) + opt := WithOption(d.opt) + if rAddrPort.Addr().Unmap().IsLoopback() { + // avoid "The requested address is not valid in its context." + opt = WithInterface("") + } + return ListenPacket(ctx, ParseNetwork(network, rAddrPort.Addr()), address, opt) } func NewDialer(options ...Option) Dialer { opt := applyOptions(options...) - return Dialer{Opt: *opt} + return Dialer{opt: *opt} } diff --git a/component/dialer/error.go b/component/dialer/error.go new file mode 100644 index 0000000000..f2f6b4b740 --- /dev/null +++ b/component/dialer/error.go @@ -0,0 +1,18 @@ +package dialer + +import ( + "errors" + + E "github.com/sagernet/sing/common/exceptions" +) + +var ( + ErrorNoIpAddress = errors.New("no ip address") + ErrorInvalidedNetworkStack = errors.New("invalided network stack") +) + +func errorsJoin(errs ...error) error { + // compatibility with golang<1.20 + // maybe use errors.Join(errs...) is better after we drop the old version's support + return E.Errors(errs...) +} diff --git a/component/dialer/options.go b/component/dialer/options.go index 27adc84518..372a2e6310 100644 --- a/component/dialer/options.go +++ b/component/dialer/options.go @@ -1,6 +1,9 @@ package dialer import ( + "context" + "net" + "github.com/Dreamacro/clash/component/resolver" "go.uber.org/atomic" @@ -12,13 +15,19 @@ var ( DefaultRoutingMark = atomic.NewInt32(0) ) +type NetDialer interface { + DialContext(ctx context.Context, network, address string) (net.Conn, error) +} + type option struct { interfaceName string addrReuse bool routingMark int network int prefer int + tfo bool resolver resolver.Resolver + netDialer NetDialer } type Option func(opt *option) @@ -69,6 +78,18 @@ func WithOnlySingleStack(isIPv4 bool) Option { } } +func WithTFO(tfo bool) Option { + return func(opt *option) { + opt.tfo = tfo + } +} + +func WithNetDialer(netDialer NetDialer) Option { + return func(opt *option) { + opt.netDialer = netDialer + } +} + func WithOption(o option) Option { return func(opt *option) { *opt = o diff --git a/component/dialer/tfo.go b/component/dialer/tfo.go new file mode 100644 index 0000000000..0041a9766f --- /dev/null +++ b/component/dialer/tfo.go @@ -0,0 +1,123 @@ +package dialer + +import ( + "context" + "github.com/sagernet/tfo-go" + "io" + "net" + "time" +) + +type tfoConn struct { + net.Conn + closed bool + dialed chan bool + cancel context.CancelFunc + ctx context.Context + dialFn func(ctx context.Context, earlyData []byte) (net.Conn, error) +} + +func (c *tfoConn) Dial(earlyData []byte) (err error) { + c.Conn, err = c.dialFn(c.ctx, earlyData) + if err != nil { + return + } + c.dialed <- true + return err +} + +func (c *tfoConn) Read(b []byte) (n int, err error) { + if c.closed { + return 0, io.ErrClosedPipe + } + if c.Conn == nil { + select { + case <-c.ctx.Done(): + return 0, io.ErrUnexpectedEOF + case <-c.dialed: + } + } + return c.Conn.Read(b) +} + +func (c *tfoConn) Write(b []byte) (n int, err error) { + if c.closed { + return 0, io.ErrClosedPipe + } + if c.Conn == nil { + if err := c.Dial(b); err != nil { + return 0, err + } + return len(b), nil + } + + return c.Conn.Write(b) +} + +func (c *tfoConn) Close() error { + c.closed = true + c.cancel() + if c.Conn == nil { + return nil + } + return c.Conn.Close() +} + +func (c *tfoConn) LocalAddr() net.Addr { + if c.Conn == nil { + return nil + } + return c.Conn.LocalAddr() +} + +func (c *tfoConn) RemoteAddr() net.Addr { + if c.Conn == nil { + return nil + } + return c.Conn.RemoteAddr() +} + +func (c *tfoConn) SetDeadline(t time.Time) error { + if err := c.SetReadDeadline(t); err != nil { + return err + } + return c.SetWriteDeadline(t) +} + +func (c *tfoConn) SetReadDeadline(t time.Time) error { + if c.Conn == nil { + return nil + } + return c.Conn.SetReadDeadline(t) +} + +func (c *tfoConn) SetWriteDeadline(t time.Time) error { + if c.Conn == nil { + return nil + } + return c.Conn.SetWriteDeadline(t) +} + +func (c *tfoConn) Upstream() any { + if c.Conn == nil { // ensure return a nil interface not an interface with nil value + return nil + } + return c.Conn +} + +func (c *tfoConn) NeedHandshake() bool { + return c.Conn == nil +} + +func dialTFO(ctx context.Context, netDialer net.Dialer, network, address string) (net.Conn, error) { + ctx, cancel := context.WithCancel(ctx) + dialer := tfo.Dialer{Dialer: netDialer, DisableTFO: false} + return &tfoConn{ + dialed: make(chan bool, 1), + cancel: cancel, + ctx: ctx, + dialFn: func(ctx context.Context, earlyData []byte) (net.Conn, error) { + return dialer.DialContext(ctx, network, address, earlyData) + }, + }, nil +} diff --git a/component/geodata/geodata.go b/component/geodata/geodata.go index ac0f820e0d..9d0b0df02f 100644 --- a/component/geodata/geodata.go +++ b/component/geodata/geodata.go @@ -1,13 +1,10 @@ package geodata import ( - "errors" "fmt" - C "github.com/Dreamacro/clash/constant" - "strings" "github.com/Dreamacro/clash/component/geodata/router" - "github.com/Dreamacro/clash/log" + C "github.com/Dreamacro/clash/constant" ) type loader struct { @@ -15,47 +12,7 @@ type loader struct { } func (l *loader) LoadGeoSite(list string) ([]*router.Domain, error) { - return l.LoadGeoSiteWithAttr(C.GeositeName, list) -} - -func (l *loader) LoadGeoSiteWithAttr(file string, siteWithAttr string) ([]*router.Domain, error) { - parts := strings.Split(siteWithAttr, "@") - if len(parts) == 0 { - return nil, errors.New("empty rule") - } - list := strings.TrimSpace(parts[0]) - attrVal := parts[1:] - - if len(list) == 0 { - return nil, fmt.Errorf("empty listname in rule: %s", siteWithAttr) - } - - domains, err := l.LoadSiteByPath(file, list) - if err != nil { - return nil, err - } - - attrs := parseAttrs(attrVal) - if attrs.IsEmpty() { - if strings.Contains(siteWithAttr, "@") { - log.Warnln("empty attribute list: %s", siteWithAttr) - } - return domains, nil - } - - filteredDomains := make([]*router.Domain, 0, len(domains)) - hasAttrMatched := false - for _, domain := range domains { - if attrs.Match(domain) { - hasAttrMatched = true - filteredDomains = append(filteredDomains, domain) - } - } - if !hasAttrMatched { - log.Warnln("attribute match no rule: geosite: %s", siteWithAttr) - } - - return filteredDomains, nil + return l.LoadSiteByPath(C.GeositeName, list) } func (l *loader) LoadGeoIP(country string) ([]*router.CIDR, error) { diff --git a/component/geodata/geodataproto.go b/component/geodata/geodataproto.go index ffefc48489..34bdad7002 100644 --- a/component/geodata/geodataproto.go +++ b/component/geodata/geodataproto.go @@ -14,6 +14,5 @@ type LoaderImplementation interface { type Loader interface { LoaderImplementation LoadGeoSite(list string) ([]*router.Domain, error) - LoadGeoSiteWithAttr(file string, siteWithAttr string) ([]*router.Domain, error) LoadGeoIP(country string) ([]*router.CIDR, error) } diff --git a/component/geodata/memconservative/cache.go b/component/geodata/memconservative/cache.go index 28c2c2384c..ca78d19dd1 100644 --- a/component/geodata/memconservative/cache.go +++ b/component/geodata/memconservative/cache.go @@ -118,7 +118,7 @@ func (g GeoSiteCache) Unmarshal(filename, code string) (*router.GeoSite, error) case errFailedToReadBytes, errFailedToReadExpectedLenBytes, errInvalidGeodataFile, errInvalidGeodataVarintLength: - log.Warnln("failed to decode geoip file: %s%s", filename, ", fallback to the original ReadFile method") + log.Warnln("failed to decode geosite file: %s%s", filename, ", fallback to the original ReadFile method") geositeBytes, err = os.ReadFile(asset) if err != nil { return nil, err diff --git a/component/geodata/utils.go b/component/geodata/utils.go index 9e7e50b10d..04ccfa5156 100644 --- a/component/geodata/utils.go +++ b/component/geodata/utils.go @@ -1,9 +1,14 @@ package geodata import ( + "errors" "fmt" + "golang.org/x/sync/singleflight" + "strings" + "github.com/Dreamacro/clash/component/geodata/router" C "github.com/Dreamacro/clash/constant" + "github.com/Dreamacro/clash/log" ) var geoLoaderName = "memconservative" @@ -34,6 +39,8 @@ func Verify(name string) error { } } +var loadGeoSiteMatcherSF = singleflight.Group{} + func LoadGeoSiteMatcher(countryCode string) (*router.DomainMatcher, int, error) { if len(countryCode) == 0 { return nil, 0, fmt.Errorf("country code could not be empty") @@ -44,16 +51,53 @@ func LoadGeoSiteMatcher(countryCode string) (*router.DomainMatcher, int, error) not = true countryCode = countryCode[1:] } + countryCode = strings.ToLower(countryCode) - geoLoader, err := GetGeoDataLoader(geoLoaderName) - if err != nil { - return nil, 0, err + parts := strings.Split(countryCode, "@") + if len(parts) == 0 { + return nil, 0, errors.New("empty rule") } + listName := strings.TrimSpace(parts[0]) + attrVal := parts[1:] - domains, err := geoLoader.LoadGeoSite(countryCode) + if len(listName) == 0 { + return nil, 0, fmt.Errorf("empty listname in rule: %s", countryCode) + } + + v, err, shared := loadGeoSiteMatcherSF.Do(listName, func() (interface{}, error) { + geoLoader, err := GetGeoDataLoader(geoLoaderName) + if err != nil { + return nil, err + } + return geoLoader.LoadGeoSite(listName) + }) if err != nil { + if !shared { + loadGeoSiteMatcherSF.Forget(listName) // don't store the error result + } return nil, 0, err } + domains := v.([]*router.Domain) + + attrs := parseAttrs(attrVal) + if attrs.IsEmpty() { + if strings.Contains(countryCode, "@") { + log.Warnln("empty attribute list: %s", countryCode) + } + } else { + filteredDomains := make([]*router.Domain, 0, len(domains)) + hasAttrMatched := false + for _, domain := range domains { + if attrs.Match(domain) { + hasAttrMatched = true + filteredDomains = append(filteredDomains, domain) + } + } + if !hasAttrMatched { + log.Warnln("attribute match no rule: geosite: %s", countryCode) + } + domains = filteredDomains + } /** linear: linear algorithm @@ -68,25 +112,34 @@ func LoadGeoSiteMatcher(countryCode string) (*router.DomainMatcher, int, error) return matcher, len(domains), nil } +var loadGeoIPMatcherSF = singleflight.Group{} + func LoadGeoIPMatcher(country string) (*router.GeoIPMatcher, int, error) { if len(country) == 0 { return nil, 0, fmt.Errorf("country code could not be empty") } - geoLoader, err := GetGeoDataLoader(geoLoaderName) - if err != nil { - return nil, 0, err - } not := false if country[0] == '!' { not = true country = country[1:] } - - records, err := geoLoader.LoadGeoIP(country) + country = strings.ToLower(country) + + v, err, shared := loadGeoIPMatcherSF.Do(country, func() (interface{}, error) { + geoLoader, err := GetGeoDataLoader(geoLoaderName) + if err != nil { + return nil, err + } + return geoLoader.LoadGeoIP(country) + }) if err != nil { + if !shared { + loadGeoIPMatcherSF.Forget(country) // don't store the error result + } return nil, 0, err } + records := v.([]*router.CIDR) geoIP := &router.GeoIP{ CountryCode: country, @@ -98,6 +151,10 @@ func LoadGeoIPMatcher(country string) (*router.GeoIPMatcher, int, error) { if err != nil { return nil, 0, err } - return matcher, len(records), nil } + +func ClearCache() { + loadGeoSiteMatcherSF = singleflight.Group{} + loadGeoIPMatcherSF = singleflight.Group{} +} diff --git a/component/http/http.go b/component/http/http.go index 54a3daa948..ece7b442bd 100644 --- a/component/http/http.go +++ b/component/http/http.go @@ -2,14 +2,15 @@ package http import ( "context" - "github.com/Dreamacro/clash/component/tls" - "github.com/Dreamacro/clash/listener/inner" "io" "net" "net/http" URL "net/url" "strings" "time" + + "github.com/Dreamacro/clash/component/tls" + "github.com/Dreamacro/clash/listener/inner" ) const ( @@ -52,7 +53,7 @@ func HttpRequest(ctx context.Context, url, method string, header map[string][]st TLSHandshakeTimeout: 10 * time.Second, ExpectContinueTimeout: 1 * time.Second, DialContext: func(ctx context.Context, network, address string) (net.Conn, error) { - conn := inner.HandleTcp(address, urlRes.Hostname()) + conn := inner.HandleTcp(address, "") return conn, nil }, TLSClientConfig: tls.GetDefaultTLSConfig(), diff --git a/component/resolver/host.go b/component/resolver/host.go new file mode 100644 index 0000000000..3b7e9a3766 --- /dev/null +++ b/component/resolver/host.go @@ -0,0 +1,113 @@ +package resolver + +import ( + "errors" + "net/netip" + "strings" + + "github.com/Dreamacro/clash/common/utils" + "github.com/Dreamacro/clash/component/trie" + "github.com/zhangyunhao116/fastrand" +) + +type Hosts struct { + *trie.DomainTrie[HostValue] +} + +func NewHosts(hosts *trie.DomainTrie[HostValue]) Hosts { + return Hosts{ + hosts, + } +} + +// Return the search result and whether to match the parameter `isDomain` +func (h *Hosts) Search(domain string, isDomain bool) (*HostValue, bool) { + value := h.DomainTrie.Search(domain) + if value == nil { + return nil, false + } + hostValue := value.Data() + for { + if isDomain && hostValue.IsDomain { + return &hostValue, true + } else { + if node := h.DomainTrie.Search(hostValue.Domain); node != nil { + hostValue = node.Data() + } else { + break + } + } + } + if isDomain == hostValue.IsDomain { + return &hostValue, true + } + return &hostValue, false +} + +type HostValue struct { + IsDomain bool + IPs []netip.Addr + Domain string +} + +func NewHostValue(value any) (HostValue, error) { + isDomain := true + ips := make([]netip.Addr, 0) + domain := "" + if valueArr, err := utils.ToStringSlice(value); err != nil { + return HostValue{}, err + } else { + if len(valueArr) > 1 { + isDomain = false + for _, str := range valueArr { + if ip, err := netip.ParseAddr(str); err == nil { + ips = append(ips, ip) + } else { + return HostValue{}, err + } + } + } else if len(valueArr) == 1 { + host := valueArr[0] + if ip, err := netip.ParseAddr(host); err == nil { + ips = append(ips, ip) + isDomain = false + } else { + domain = host + } + } + } + if isDomain { + return NewHostValueByDomain(domain) + } else { + return NewHostValueByIPs(ips) + } +} + +func NewHostValueByIPs(ips []netip.Addr) (HostValue, error) { + if len(ips) == 0 { + return HostValue{}, errors.New("ip list is empty") + } + return HostValue{ + IsDomain: false, + IPs: ips, + }, nil +} + +func NewHostValueByDomain(domain string) (HostValue, error) { + domain = strings.Trim(domain, ".") + item := strings.Split(domain, ".") + if len(item) < 2 { + return HostValue{}, errors.New("invaild domain") + } + return HostValue{ + IsDomain: true, + Domain: domain, + }, nil +} + +func (hv HostValue) RandIP() (netip.Addr, error) { + if hv.IsDomain { + return netip.Addr{}, errors.New("value type is error") + } + return hv.IPs[fastrand.Intn(len(hv.IPs))], nil +} diff --git a/component/resolver/resolver.go b/component/resolver/resolver.go index fa1e7c02a5..f5872ad7fa 100644 --- a/component/resolver/resolver.go +++ b/component/resolver/resolver.go @@ -4,15 +4,16 @@ import ( "context" "errors" "fmt" - "math/rand" "net" "net/netip" "strings" "time" + "github.com/Dreamacro/clash/common/utils" "github.com/Dreamacro/clash/component/trie" "github.com/miekg/dns" + "github.com/zhangyunhao116/fastrand" ) var ( @@ -27,7 +28,7 @@ var ( DisableIPv6 = true // DefaultHosts aim to resolve hosts - DefaultHosts = trie.New[netip.Addr]() + DefaultHosts = NewHosts(trie.New[HostValue]()) // DefaultDNSTimeout defined the default dns request timeout DefaultDNSTimeout = time.Second * 5 @@ -51,9 +52,11 @@ type Resolver interface { // LookupIPv4WithResolver same as LookupIPv4, but with a resolver func LookupIPv4WithResolver(ctx context.Context, host string, r Resolver) ([]netip.Addr, error) { - if node := DefaultHosts.Search(host); node != nil { - if ip := node.Data(); ip.Is4() { - return []netip.Addr{node.Data()}, nil + if node, ok := DefaultHosts.Search(host, false); ok { + if addrs := utils.Filter(node.IPs, func(ip netip.Addr) bool { + return ip.Is4() + }); len(addrs) > 0 { + return addrs, nil } } @@ -69,10 +72,6 @@ func LookupIPv4WithResolver(ctx context.Context, host string, r Resolver) ([]net return r.LookupIPv4(ctx, host) } - if DefaultResolver != nil { - return DefaultResolver.LookupIPv4(ctx, host) - } - ipAddrs, err := net.DefaultResolver.LookupNetIP(ctx, "ip4", host) if err != nil { return nil, err @@ -96,7 +95,7 @@ func ResolveIPv4WithResolver(ctx context.Context, host string, r Resolver) (neti } else if len(ips) == 0 { return netip.Addr{}, fmt.Errorf("%w: %s", ErrIPNotFound, host) } - return ips[rand.Intn(len(ips))], nil + return ips[fastrand.Intn(len(ips))], nil } // ResolveIPv4 with a host, return ipv4 @@ -110,9 +109,11 @@ func LookupIPv6WithResolver(ctx context.Context, host string, r Resolver) ([]net return nil, ErrIPv6Disabled } - if node := DefaultHosts.Search(host); node != nil { - if ip := node.Data(); ip.Is6() { - return []netip.Addr{ip}, nil + if node, ok := DefaultHosts.Search(host, false); ok { + if addrs := utils.Filter(node.IPs, func(ip netip.Addr) bool { + return ip.Is6() + }); len(addrs) > 0 { + return addrs, nil } } @@ -126,9 +127,6 @@ func LookupIPv6WithResolver(ctx context.Context, host string, r Resolver) ([]net if r != nil { return r.LookupIPv6(ctx, host) } - if DefaultResolver != nil { - return DefaultResolver.LookupIPv6(ctx, host) - } ipAddrs, err := net.DefaultResolver.LookupNetIP(ctx, "ip6", host) if err != nil { @@ -153,7 +151,7 @@ func ResolveIPv6WithResolver(ctx context.Context, host string, r Resolver) (neti } else if len(ips) == 0 { return netip.Addr{}, fmt.Errorf("%w: %s", ErrIPNotFound, host) } - return ips[rand.Intn(len(ips))], nil + return ips[fastrand.Intn(len(ips))], nil } func ResolveIPv6(ctx context.Context, host string) (netip.Addr, error) { @@ -162,8 +160,8 @@ func ResolveIPv6(ctx context.Context, host string) (netip.Addr, error) { // LookupIPWithResolver same as LookupIP, but with a resolver func LookupIPWithResolver(ctx context.Context, host string, r Resolver) ([]netip.Addr, error) { - if node := DefaultHosts.Search(host); node != nil { - return []netip.Addr{node.Data()}, nil + if node, ok := DefaultHosts.Search(host, false); ok { + return node.IPs, nil } if r != nil { @@ -172,7 +170,7 @@ func LookupIPWithResolver(ctx context.Context, host string, r Resolver) ([]netip } return r.LookupIP(ctx, host) } else if DisableIPv6 { - return LookupIPv4(ctx, host) + return LookupIPv4WithResolver(ctx, host, r) } if ip, err := netip.ParseAddr(host); err == nil { @@ -202,7 +200,7 @@ func ResolveIPWithResolver(ctx context.Context, host string, r Resolver) (netip. } else if len(ips) == 0 { return netip.Addr{}, fmt.Errorf("%w: %s", ErrIPNotFound, host) } - return ips[rand.Intn(len(ips))], nil + return ips[fastrand.Intn(len(ips))], nil } // ResolveIP with a host, return ip diff --git a/component/sniffer/dispatcher.go b/component/sniffer/dispatcher.go index f4511b97d9..97d448ce11 100644 --- a/component/sniffer/dispatcher.go +++ b/component/sniffer/dispatcher.go @@ -36,12 +36,7 @@ type SnifferDispatcher struct { parsePureIp bool } -func (sd *SnifferDispatcher) TCPSniff(conn net.Conn, metadata *C.Metadata) { - bufConn, ok := conn.(*N.BufferedConn) - if !ok { - return - } - +func (sd *SnifferDispatcher) TCPSniff(conn *N.BufferedConn, metadata *C.Metadata) { if (metadata.Host == "" && sd.parsePureIp) || sd.forceDomain.Search(metadata.Host) != nil || (metadata.DNSMode == C.DNSMapping && sd.forceDnsMapping) { port, err := strconv.ParseUint(metadata.DstPort, 10, 16) if err != nil { @@ -74,7 +69,7 @@ func (sd *SnifferDispatcher) TCPSniff(conn net.Conn, metadata *C.Metadata) { } sd.rwMux.RUnlock() - if host, err := sd.sniffDomain(bufConn, metadata); err != nil { + if host, err := sd.sniffDomain(conn, metadata); err != nil { sd.cacheSniffFailed(metadata) log.Debugln("[Sniffer] All sniffing sniff failed with from [%s:%s] to [%s:%s]", metadata.SrcIP, metadata.SrcPort, metadata.String(), metadata.DstPort) return diff --git a/component/tls/config.go b/component/tls/config.go index 39d1b1fd40..91b89f1dc6 100644 --- a/component/tls/config.go +++ b/component/tls/config.go @@ -11,31 +11,42 @@ import ( "strings" "sync" - CN "github.com/Dreamacro/clash/common/net" - xtls "github.com/xtls/go" ) -var tlsCertificates = make([]tls.Certificate, 0) +var trustCerts []*x509.Certificate var mutex sync.RWMutex var errNotMacth error = errors.New("certificate fingerprints do not match") -func AddCertificate(privateKey, certificate string) error { +func AddCertificate(certificate string) error { mutex.Lock() defer mutex.Unlock() - if cert, err := CN.ParseCert(certificate, privateKey); err != nil { - return err + if certificate == "" { + return fmt.Errorf("certificate is empty") + } + if cert, err := x509.ParseCertificate([]byte(certificate)); err == nil { + trustCerts = append(trustCerts, cert) + return nil } else { - tlsCertificates = append(tlsCertificates, cert) + return fmt.Errorf("add certificate failed") } - return nil } -func GetCertificates() []tls.Certificate { - mutex.RLock() - defer mutex.RUnlock() - return tlsCertificates +func ResetCertificate() { + mutex.Lock() + defer mutex.Unlock() + trustCerts = nil +} + +func getCertPool() *x509.CertPool { + certPool, err := x509.SystemCertPool() + if err == nil { + for _, cert := range trustCerts { + certPool.AddCert(cert) + } + } + return certPool } func verifyFingerprint(fingerprint *[32]byte) func(rawCerts [][]byte, verifiedChains [][]*x509.Certificate) error { @@ -85,12 +96,13 @@ func GetSpecifiedFingerprintTLSConfig(tlsConfig *tls.Config, fingerprint string) } func GetGlobalTLSConfig(tlsConfig *tls.Config) *tls.Config { + certPool := getCertPool() if tlsConfig == nil { return &tls.Config{ - Certificates: tlsCertificates, + RootCAs: certPool, } } - tlsConfig.Certificates = append(tlsConfig.Certificates, tlsCertificates...) + tlsConfig.RootCAs = certPool return tlsConfig } @@ -107,29 +119,13 @@ func GetSpecifiedFingerprintXTLSConfig(tlsConfig *xtls.Config, fingerprint strin } func GetGlobalXTLSConfig(tlsConfig *xtls.Config) *xtls.Config { - xtlsCerts := make([]xtls.Certificate, len(tlsCertificates)) - for _, cert := range tlsCertificates { - tlsSsaList := make([]xtls.SignatureScheme, len(cert.SupportedSignatureAlgorithms)) - for _, ssa := range cert.SupportedSignatureAlgorithms { - tlsSsa := xtls.SignatureScheme(ssa) - tlsSsaList = append(tlsSsaList, tlsSsa) - } - xtlsCert := xtls.Certificate{ - Certificate: cert.Certificate, - PrivateKey: cert.PrivateKey, - OCSPStaple: cert.OCSPStaple, - SignedCertificateTimestamps: cert.SignedCertificateTimestamps, - Leaf: cert.Leaf, - SupportedSignatureAlgorithms: tlsSsaList, - } - xtlsCerts = append(xtlsCerts, xtlsCert) - } + certPool := getCertPool() if tlsConfig == nil { return &xtls.Config{ - Certificates: xtlsCerts, + RootCAs: certPool, } } - tlsConfig.Certificates = xtlsCerts + tlsConfig.RootCAs = certPool return tlsConfig } diff --git a/component/tls/reality.go b/component/tls/reality.go new file mode 100644 index 0000000000..dd4f3af8a3 --- /dev/null +++ b/component/tls/reality.go @@ -0,0 +1,164 @@ +package tls + +import ( + "bytes" + "context" + "crypto/aes" + "crypto/cipher" + "crypto/ed25519" + "crypto/hmac" + "crypto/sha256" + "crypto/sha512" + "crypto/tls" + "crypto/x509" + "encoding/binary" + "errors" + "net" + "net/http" + "reflect" + "strings" + "time" + "unsafe" + + "github.com/Dreamacro/clash/common/utils" + "github.com/Dreamacro/clash/log" + + utls "github.com/sagernet/utls" + "github.com/zhangyunhao116/fastrand" + "golang.org/x/crypto/curve25519" + "golang.org/x/crypto/hkdf" + "golang.org/x/net/http2" +) + +const RealityMaxShortIDLen = 8 + +type RealityConfig struct { + PublicKey [curve25519.ScalarSize]byte + ShortID [RealityMaxShortIDLen]byte +} + +func GetRealityConn(ctx context.Context, conn net.Conn, ClientFingerprint string, tlsConfig *tls.Config, realityConfig *RealityConfig) (net.Conn, error) { + if fingerprint, exists := GetFingerprint(ClientFingerprint); exists { + verifier := &realityVerifier{ + serverName: tlsConfig.ServerName, + } + uConfig := &utls.Config{ + ServerName: tlsConfig.ServerName, + InsecureSkipVerify: true, + SessionTicketsDisabled: true, + VerifyPeerCertificate: verifier.VerifyPeerCertificate, + } + clientID := utls.ClientHelloID{ + Client: fingerprint.Client, + Version: fingerprint.Version, + Seed: fingerprint.Seed, + } + uConn := utls.UClient(conn, uConfig, clientID) + verifier.UConn = uConn + err := uConn.BuildHandshakeState() + if err != nil { + return nil, err + } + + hello := uConn.HandshakeState.Hello + for i := range hello.SessionId { // https://github.com/golang/go/issues/5373 + hello.SessionId[i] = 0 + } + copy(hello.Raw[39:], hello.SessionId) + + binary.BigEndian.PutUint64(hello.SessionId, uint64(time.Now().Unix())) + + hello.SessionId[0] = 1 + hello.SessionId[1] = 8 + hello.SessionId[2] = 0 + copy(hello.SessionId[8:], realityConfig.ShortID[:]) + + //log.Debugln("REALITY hello.sessionId[:16]: %v", hello.SessionId[:16]) + + authKey := uConn.HandshakeState.State13.EcdheParams.SharedKey(realityConfig.PublicKey[:]) + if authKey == nil { + return nil, errors.New("nil auth_key") + } + verifier.authKey = authKey + _, err = hkdf.New(sha256.New, authKey, hello.Random[:20], []byte("REALITY")).Read(authKey) + if err != nil { + return nil, err + } + aesBlock, _ := aes.NewCipher(authKey) + aesGcmCipher, _ := cipher.NewGCM(aesBlock) + aesGcmCipher.Seal(hello.SessionId[:0], hello.Random[20:], hello.SessionId[:16], hello.Raw) + copy(hello.Raw[39:], hello.SessionId) + //log.Debugln("REALITY hello.sessionId: %v", hello.SessionId) + //log.Debugln("REALITY uConn.AuthKey: %v", authKey) + + err = uConn.HandshakeContext(ctx) + if err != nil { + return nil, err + } + + log.Debugln("REALITY Authentication: %v", verifier.verified) + + if !verifier.verified { + go realityClientFallback(uConn, uConfig.ServerName, clientID) + return nil, errors.New("REALITY authentication failed") + } + + return uConn, nil + } + return nil, errors.New("unknown uTLS fingerprint") +} + +func realityClientFallback(uConn net.Conn, serverName string, fingerprint utls.ClientHelloID) { + defer uConn.Close() + client := http.Client{ + Transport: &http2.Transport{ + DialTLSContext: func(ctx context.Context, network, addr string, config *tls.Config) (net.Conn, error) { + return uConn, nil + }, + }, + } + request, _ := http.NewRequest("GET", "https://"+serverName, nil) + request.Header.Set("User-Agent", fingerprint.Client) + request.AddCookie(&http.Cookie{Name: "padding", Value: strings.Repeat("0", fastrand.Intn(32)+30)}) + response, err := client.Do(request) + if err != nil { + return + } + //_, _ = io.Copy(io.Discard, response.Body) + time.Sleep(time.Duration(5+fastrand.Int63n(10)) * time.Second) + response.Body.Close() + client.CloseIdleConnections() +} + +type realityVerifier struct { + *utls.UConn + serverName string + authKey []byte + verified bool +} + +var pOffset = utils.MustOK(reflect.TypeOf((*utls.UConn)(nil)).Elem().FieldByName("peerCertificates")).Offset + +func (c *realityVerifier) VerifyPeerCertificate(rawCerts [][]byte, verifiedChains [][]*x509.Certificate) error { + //p, _ := reflect.TypeOf(c.Conn).Elem().FieldByName("peerCertificates") + certs := *(*[]*x509.Certificate)(unsafe.Pointer(uintptr(unsafe.Pointer(c.Conn)) + pOffset)) + if pub, ok := certs[0].PublicKey.(ed25519.PublicKey); ok { + h := hmac.New(sha512.New, c.authKey) + h.Write(pub) + if bytes.Equal(h.Sum(nil), certs[0].Signature) { + c.verified = true + return nil + } + } + opts := x509.VerifyOptions{ + DNSName: c.serverName, + Intermediates: x509.NewCertPool(), + } + for _, cert := range certs[1:] { + opts.Intermediates.AddCert(cert) + } + if _, err := certs[0].Verify(opts); err != nil { + return err + } + return nil +} diff --git a/component/tls/utls.go b/component/tls/utls.go index f965fc6499..7ea2ad06ad 100644 --- a/component/tls/utls.go +++ b/component/tls/utls.go @@ -7,7 +7,7 @@ import ( "github.com/Dreamacro/clash/log" "github.com/mroth/weightedrand/v2" - utls "github.com/refraction-networking/utls" + utls "github.com/sagernet/utls" ) type UConn struct { @@ -45,8 +45,13 @@ func GetFingerprint(ClientFingerprint string) (UClientHelloID, bool) { } fingerprint, ok := Fingerprints[ClientFingerprint] - log.Debugln("use specified fingerprint:%s", fingerprint.Client) - return fingerprint, ok + if ok { + log.Debugln("use specified fingerprint:%s", fingerprint.Client) + return fingerprint, ok + } else { + log.Warnln("wrong ClientFingerprint:%s", ClientFingerprint) + return UClientHelloID{}, false + } } func RollFingerprint() (UClientHelloID, bool) { @@ -67,7 +72,22 @@ var Fingerprints = map[string]UClientHelloID{ "firefox": {&utls.HelloFirefox_Auto}, "safari": {&utls.HelloSafari_Auto}, "ios": {&utls.HelloIOS_Auto}, - "randomized": {&utls.HelloRandomized}, + "android": {&utls.HelloAndroid_11_OkHttp}, + "edge": {&utls.HelloEdge_Auto}, + "360": {&utls.Hello360_Auto}, + "qq": {&utls.HelloQQ_Auto}, + "random": {nil}, + "randomized": {nil}, +} + +func init() { + weights := utls.DefaultWeights + weights.TLSVersMax_Set_VersionTLS13 = 1 + weights.FirstKeyShare_Set_CurveP256 = 0 + randomized := utls.HelloRandomized + randomized.Seed, _ = utls.NewPRNGSeed() + randomized.Weights = &weights + Fingerprints["randomized"] = UClientHelloID{&randomized} } func copyConfig(c *tls.Config) *utls.Config { @@ -112,10 +132,7 @@ func SetGlobalUtlsClient(Client string) { } func HaveGlobalFingerprint() bool { - if len(initUtlsClient) != 0 && initUtlsClient != "none" { - return true - } - return false + return len(initUtlsClient) != 0 && initUtlsClient != "none" } func GetGlobalFingerprint() string { diff --git a/config/config.go b/config/config.go index d71fc63f42..c407aad520 100644 --- a/config/config.go +++ b/config/config.go @@ -4,12 +4,11 @@ import ( "container/list" "errors" "fmt" - "net" "net/netip" "net/url" "os" - "reflect" + "regexp" "runtime" "strconv" "strings" @@ -26,6 +25,7 @@ import ( "github.com/Dreamacro/clash/component/geodata" "github.com/Dreamacro/clash/component/geodata/router" P "github.com/Dreamacro/clash/component/process" + "github.com/Dreamacro/clash/component/resolver" SNIFF "github.com/Dreamacro/clash/component/sniffer" tlsC "github.com/Dreamacro/clash/component/tls" "github.com/Dreamacro/clash/component/trie" @@ -92,6 +92,7 @@ type DNS struct { Enable bool `yaml:"enable"` PreferH3 bool `yaml:"prefer-h3"` IPv6 bool `yaml:"ipv6"` + IPv6Timeout uint `yaml:"ipv6-timeout"` NameServer []dns.NameServer `yaml:"nameserver"` Fallback []dns.NameServer `yaml:"fallback"` FallbackFilter FallbackFilter `yaml:"fallback-filter"` @@ -99,7 +100,7 @@ type DNS struct { EnhancedMode C.DNSMode `yaml:"enhanced-mode"` DefaultNameserver []dns.NameServer `yaml:"default-nameserver"` FakeIPRange *fakeip.Pool - Hosts *trie.DomainTrie[netip.Addr] + Hosts *trie.DomainTrie[resolver.HostValue] NameServerPolicy map[string][]dns.NameServer ProxyServerNameserver []dns.NameServer } @@ -120,13 +121,9 @@ type Profile struct { } type TLS struct { - RawCert `yaml:",inline"` - CustomTrustCert []RawCert `yaml:"custom-certifactes"` -} - -type RawCert struct { - Certificate string `yaml:"certificate"` - PrivateKey string `yaml:"private-key"` + Certificate string `yaml:"certificate"` + PrivateKey string `yaml:"private-key"` + CustomTrustCert []string `yaml:"custom-certifactes"` } // IPTables config @@ -157,7 +154,7 @@ type Config struct { IPTables *IPTables DNS *DNS Experimental *Experimental - Hosts *trie.DomainTrie[netip.Addr] + Hosts *trie.DomainTrie[resolver.HostValue] Profile *Profile Rules []C.Rule SubRules map[string][]C.Rule @@ -175,6 +172,7 @@ type RawDNS struct { Enable bool `yaml:"enable"` PreferH3 bool `yaml:"prefer-h3"` IPv6 bool `yaml:"ipv6"` + IPv6Timeout uint `yaml:"ipv6-timeout"` UseHosts bool `yaml:"use-hosts"` NameServer []string `yaml:"nameserver"` Fallback []string `yaml:"fallback"` @@ -220,6 +218,7 @@ type RawTun struct { ExcludePackage []string `yaml:"exclude-package" json:"exclude_package,omitempty"` EndpointIndependentNat bool `yaml:"endpoint-independent-nat" json:"endpoint_independent_nat,omitempty"` UDPTimeout int64 `yaml:"udp-timeout" json:"udp_timeout,omitempty"` + FileDescriptor int `yaml:"file-descriptor" json:"file-descriptor"` } type RawTuicServer struct { @@ -267,7 +266,7 @@ type RawConfig struct { Sniffer RawSniffer `yaml:"sniffer"` ProxyProvider map[string]map[string]any `yaml:"proxy-providers"` RuleProvider map[string]map[string]any `yaml:"rule-providers"` - Hosts map[string]string `yaml:"hosts"` + Hosts map[string]any `yaml:"hosts"` DNS RawDNS `yaml:"dns"` Tun RawTun `yaml:"tun"` TuicServer RawTuicServer `yaml:"tuic-server"` @@ -341,7 +340,7 @@ func UnmarshalRawConfig(buf []byte) (*RawConfig, error) { UnifiedDelay: false, Authentication: []string{}, LogLevel: log.INFO, - Hosts: map[string]string{}, + Hosts: map[string]any{}, Rule: []string{}, Proxy: []map[string]any{}, ProxyGroup: []map[string]any{}, @@ -381,6 +380,7 @@ func UnmarshalRawConfig(buf []byte) (*RawConfig, error) { Enable: false, IPv6: false, UseHosts: true, + IPv6Timeout: 100, EnhancedMode: C.DNSMapping, FakeIPRange: "198.18.0.1/16", FallbackFilter: RawFallbackFilter{ @@ -419,9 +419,9 @@ func UnmarshalRawConfig(buf []byte) (*RawConfig, error) { StoreSelected: true, }, GeoXUrl: RawGeoXUrl{ - GeoIp: "https://ghproxy.com/https://raw.githubusercontent.com/Loyalsoldier/v2ray-rules-dat/release/geoip.dat", - Mmdb: "https://ghproxy.com/https://raw.githubusercontent.com/Loyalsoldier/geoip/release/Country.mmdb", - GeoSite: "https://ghproxy.com/https://raw.githubusercontent.com/Loyalsoldier/v2ray-rules-dat/release/geosite.dat", + Mmdb: "https://testingcf.jsdelivr.net/gh/MetaCubeX/meta-rules-dat@release/country.mmdb", + GeoIp: "https://testingcf.jsdelivr.net/gh/MetaCubeX/meta-rules-dat@release/geoip.dat", + GeoSite: "https://testingcf.jsdelivr.net/gh/MetaCubeX/meta-rules-dat@release/geosite.dat", }, } @@ -447,7 +447,11 @@ func ParseRawConfig(rawCfg *RawConfig) (*Config, error) { } config.General = general - dialer.DefaultInterface.Store(config.General.Interface) + if len(config.General.GlobalClientFingerprint) != 0 { + log.Debugln("GlobalClientFingerprint:%s", config.General.GlobalClientFingerprint) + tlsC.SetGlobalUtlsClient(config.General.GlobalClientFingerprint) + } + proxies, providers, err := parseProxies(rawCfg) if err != nil { return nil, err @@ -522,11 +526,6 @@ func ParseRawConfig(rawCfg *RawConfig) (*Config, error) { elapsedTime := time.Since(startTime) / time.Millisecond // duration in ms log.Infoln("Initial configuration complete, total time: %dms", elapsedTime) //Segment finished in xxm - if len(config.General.GlobalClientFingerprint) != 0 { - log.Debugln("GlobalClientFingerprint:%s", config.General.GlobalClientFingerprint) - tlsC.SetGlobalUtlsClient(config.General.GlobalClientFingerprint) - } - return config, nil } @@ -828,21 +827,47 @@ func parseRules(rulesConfig []string, proxies map[string]C.Proxy, subRules map[s return rules, nil } -func parseHosts(cfg *RawConfig) (*trie.DomainTrie[netip.Addr], error) { - tree := trie.New[netip.Addr]() +func parseHosts(cfg *RawConfig) (*trie.DomainTrie[resolver.HostValue], error) { + tree := trie.New[resolver.HostValue]() // add default hosts - if err := tree.Insert("localhost", netip.AddrFrom4([4]byte{127, 0, 0, 1})); err != nil { + hostValue, _ := resolver.NewHostValueByIPs( + []netip.Addr{netip.AddrFrom4([4]byte{127, 0, 0, 1})}) + if err := tree.Insert("localhost", hostValue); err != nil { log.Errorln("insert localhost to host error: %s", err.Error()) } if len(cfg.Hosts) != 0 { - for domain, ipStr := range cfg.Hosts { - ip, err := netip.ParseAddr(ipStr) + for domain, anyValue := range cfg.Hosts { + if str, ok := anyValue.(string); ok && str == "clash" { + if addrs, err := net.InterfaceAddrs(); err != nil { + log.Errorln("insert clash to host error: %s", err) + } else { + ips := make([]netip.Addr, 0) + for _, addr := range addrs { + if ipnet, ok := addr.(*net.IPNet); ok && !ipnet.IP.IsLoopback() { + if ip, err := netip.ParseAddr(ipnet.IP.String()); err == nil { + ips = append(ips, ip) + } + } + } + anyValue = ips + } + } + value, err := resolver.NewHostValue(anyValue) if err != nil { - return nil, fmt.Errorf("%s is not a valid IP", ipStr) + return nil, fmt.Errorf("%s is not a valid value", anyValue) } - _ = tree.Insert(domain, ip) + if value.IsDomain { + node := tree.Search(value.Domain) + for node != nil && node.Data().IsDomain { + if node.Data().Domain == domain { + return nil, fmt.Errorf("%s, there is a cycle in domain name mapping", domain) + } + node = tree.Search(node.Data().Domain) + } + } + _ = tree.Insert(domain, value) } } tree.Optimize() @@ -960,26 +985,36 @@ func parsePureDNSServer(server string) string { } func parseNameServerPolicy(nsPolicy map[string]any, preferH3 bool) (map[string][]dns.NameServer, error) { policy := map[string][]dns.NameServer{} - - for domain, server := range nsPolicy { - var ( - nameservers []dns.NameServer - err error - ) - - switch reflect.TypeOf(server).Kind() { - case reflect.Slice, reflect.Array: - origin := reflect.ValueOf(server) - servers := make([]string, 0) - for i := 0; i < origin.Len(); i++ { - servers = append(servers, fmt.Sprintf("%v", origin.Index(i))) + updatedPolicy := make(map[string]interface{}) + re := regexp.MustCompile(`[a-zA-Z0-9\-]+\.[a-zA-Z]{2,}(\.[a-zA-Z]{2,})?`) + + for k, v := range nsPolicy { + if strings.Contains(k, ",") { + if strings.Contains(k, "geosite:") { + subkeys := strings.Split(k, ":") + subkeys = subkeys[1:] + subkeys = strings.Split(subkeys[0], ",") + for _, subkey := range subkeys { + newKey := "geosite:" + subkey + updatedPolicy[newKey] = v + } + } else if re.MatchString(k) { + subkeys := strings.Split(k, ",") + for _, subkey := range subkeys { + updatedPolicy[subkey] = v + } } - nameservers, err = parseNameServer(servers, preferH3) - case reflect.String: - nameservers, err = parseNameServer([]string{fmt.Sprintf("%v", server)}, preferH3) - default: - return nil, errors.New("server format error, must be string or array") + } else { + updatedPolicy[k] = v + } + } + + for domain, server := range updatedPolicy { + servers, err := utils.ToStringSlice(server) + if err != nil { + return nil, err } + nameservers, err := parseNameServer(servers, preferH3) if err != nil { return nil, err } @@ -1042,7 +1077,7 @@ func parseFallbackGeoSite(countries []string, rules []C.Rule) ([]*router.DomainM return sites, nil } -func parseDNS(rawCfg *RawConfig, hosts *trie.DomainTrie[netip.Addr], rules []C.Rule) (*DNS, error) { +func parseDNS(rawCfg *RawConfig, hosts *trie.DomainTrie[resolver.HostValue], rules []C.Rule) (*DNS, error) { cfg := rawCfg.DNS if cfg.Enable && len(cfg.NameServer) == 0 { return nil, fmt.Errorf("if DNS configuration is turned on, NameServer cannot be empty") @@ -1052,6 +1087,7 @@ func parseDNS(rawCfg *RawConfig, hosts *trie.DomainTrie[netip.Addr], rules []C.R Enable: cfg.Enable, Listen: cfg.Listen, PreferH3: cfg.PreferH3, + IPv6Timeout: cfg.IPv6Timeout, IPv6: cfg.IPv6, EnhancedMode: cfg.EnhancedMode, FallbackFilter: FallbackFilter{ @@ -1204,6 +1240,7 @@ func parseTun(rawTun RawTun, general *General) error { ExcludePackage: rawTun.ExcludePackage, EndpointIndependentNat: rawTun.EndpointIndependentNat, UDPTimeout: rawTun.UDPTimeout, + FileDescriptor: rawTun.FileDescriptor, } return nil @@ -1259,8 +1296,10 @@ func parseSniffer(snifferRaw RawSniffer) (*Sniffer, error) { } } } else { - // Deprecated: Use Sniff instead - log.Warnln("Deprecated: Use Sniff instead") + if sniffer.Enable { + // Deprecated: Use Sniff instead + log.Warnln("Deprecated: Use Sniff instead") + } globalPorts, err := parsePortRange(snifferRaw.Ports) if err != nil { return nil, err diff --git a/config/updateGeo.go b/config/updateGeo.go index a5f7b17b55..698bd52d77 100644 --- a/config/updateGeo.go +++ b/config/updateGeo.go @@ -63,6 +63,8 @@ func UpdateGeoDatabases() error { return fmt.Errorf("can't save GeoSite database file: %w", err) } + geodata.ClearCache() + return nil } diff --git a/constant/context.go b/constant/context.go index e641ed1485..da1e415507 100644 --- a/constant/context.go +++ b/constant/context.go @@ -3,6 +3,8 @@ package constant import ( "net" + N "github.com/Dreamacro/clash/common/net" + "github.com/gofrs/uuid" ) @@ -13,7 +15,7 @@ type PlainContext interface { type ConnContext interface { PlainContext Metadata() *Metadata - Conn() net.Conn + Conn() *N.BufferedConn } type PacketConnContext interface { diff --git a/context/conn.go b/context/conn.go index 08bbe3c7c7..c547778017 100644 --- a/context/conn.go +++ b/context/conn.go @@ -1,6 +1,7 @@ package context import ( + "github.com/Dreamacro/clash/common/utils" "net" N "github.com/Dreamacro/clash/common/net" @@ -12,14 +13,12 @@ import ( type ConnContext struct { id uuid.UUID metadata *C.Metadata - conn net.Conn + conn *N.BufferedConn } func NewConnContext(conn net.Conn, metadata *C.Metadata) *ConnContext { - id, _ := uuid.NewV4() - return &ConnContext{ - id: id, + id: utils.NewUUIDV4(), metadata: metadata, conn: N.NewBufferedConn(conn), } @@ -36,6 +35,6 @@ func (c *ConnContext) Metadata() *C.Metadata { } // Conn implement C.ConnContext Conn -func (c *ConnContext) Conn() net.Conn { +func (c *ConnContext) Conn() *N.BufferedConn { return c.conn } diff --git a/context/dns.go b/context/dns.go index 5913096187..c41de724ca 100644 --- a/context/dns.go +++ b/context/dns.go @@ -2,6 +2,7 @@ package context import ( "context" + "github.com/Dreamacro/clash/common/utils" "github.com/gofrs/uuid" "github.com/miekg/dns" @@ -22,11 +23,10 @@ type DNSContext struct { } func NewDNSContext(ctx context.Context, msg *dns.Msg) *DNSContext { - id, _ := uuid.NewV4() return &DNSContext{ Context: ctx, - id: id, + id: utils.NewUUIDV4(), msg: msg, } } diff --git a/context/packetconn.go b/context/packetconn.go index 3b0051415d..b9afef41be 100644 --- a/context/packetconn.go +++ b/context/packetconn.go @@ -3,6 +3,7 @@ package context import ( "net" + "github.com/Dreamacro/clash/common/utils" C "github.com/Dreamacro/clash/constant" "github.com/gofrs/uuid" @@ -15,9 +16,8 @@ type PacketConnContext struct { } func NewPacketConnContext(metadata *C.Metadata) *PacketConnContext { - id, _ := uuid.NewV4() return &PacketConnContext{ - id: id, + id: utils.NewUUIDV4(), metadata: metadata, } } diff --git a/dns/client.go b/dns/client.go index c5a5228142..936a58825a 100644 --- a/dns/client.go +++ b/dns/client.go @@ -4,7 +4,6 @@ import ( "context" "crypto/tls" "fmt" - "math/rand" "net" "net/netip" "strings" @@ -16,6 +15,7 @@ import ( "github.com/Dreamacro/clash/component/resolver" D "github.com/miekg/dns" + "github.com/zhangyunhao116/fastrand" ) type client struct { @@ -68,7 +68,7 @@ func (c *client) ExchangeContext(ctx context.Context, m *D.Msg) (*D.Msg, error) } else if len(ips) == 0 { return nil, fmt.Errorf("%w: %s", resolver.ErrIPNotFound, c.host) } - ip = ips[rand.Intn(len(ips))] + ip = ips[fastrand.Intn(len(ips))] } network := "udp" diff --git a/dns/filters.go b/dns/filters.go index b51e6402d0..58b261ac7a 100644 --- a/dns/filters.go +++ b/dns/filters.go @@ -29,29 +29,10 @@ func (gf *geoipFilter) Match(ip netip.Addr) bool { } if geoIPMatcher == nil { - countryCode := "cn" - geoLoader, err := geodata.GetGeoDataLoader(geodata.LoaderName()) + var err error + geoIPMatcher, _, err = geodata.LoadGeoIPMatcher("CN") if err != nil { - log.Errorln("[GeoIPFilter] GetGeoDataLoader error: %s", err.Error()) - return false - } - - records, err := geoLoader.LoadGeoIP(countryCode) - if err != nil { - log.Errorln("[GeoIPFilter] LoadGeoIP error: %s", err.Error()) - return false - } - - geoIP := &router.GeoIP{ - CountryCode: countryCode, - Cidr: records, - ReverseMatch: false, - } - - geoIPMatcher, err = router.NewGeoIPMatcher(geoIP) - - if err != nil { - log.Errorln("[GeoIPFilter] NewGeoIPMatcher error: %s", err.Error()) + log.Errorln("[GeoIPFilter] LoadGeoIPMatcher error: %s", err.Error()) return false } } @@ -92,6 +73,10 @@ type geoSiteFilter struct { } func NewGeoSite(group string) (fallbackDomainFilter, error) { + if err := geodata.InitGeoSite(); err != nil { + log.Errorln("can't initial GeoSite: %s", err) + return nil, err + } matcher, _, err := geodata.LoadGeoSiteMatcher(group) if err != nil { return nil, err diff --git a/dns/middleware.go b/dns/middleware.go index 7dc9622d8a..f2dd9c96f1 100644 --- a/dns/middleware.go +++ b/dns/middleware.go @@ -8,7 +8,7 @@ import ( "github.com/Dreamacro/clash/common/cache" "github.com/Dreamacro/clash/common/nnip" "github.com/Dreamacro/clash/component/fakeip" - "github.com/Dreamacro/clash/component/trie" + R "github.com/Dreamacro/clash/component/resolver" C "github.com/Dreamacro/clash/constant" "github.com/Dreamacro/clash/context" "github.com/Dreamacro/clash/log" @@ -21,7 +21,7 @@ type ( middleware func(next handler) handler ) -func withHosts(hosts *trie.DomainTrie[netip.Addr], mapping *cache.LruCache[netip.Addr, string]) middleware { +func withHosts(hosts R.Hosts, mapping *cache.LruCache[netip.Addr, string]) middleware { return func(next handler) handler { return func(ctx *context.DNSContext, r *D.Msg) (*D.Msg, error) { q := r.Question[0] @@ -31,40 +31,68 @@ func withHosts(hosts *trie.DomainTrie[netip.Addr], mapping *cache.LruCache[netip } host := strings.TrimRight(q.Name, ".") - - record := hosts.Search(host) - if record == nil { + handleCName := func(resp *D.Msg, domain string) { + rr := &D.CNAME{} + rr.Hdr = D.RR_Header{Name: q.Name, Rrtype: D.TypeCNAME, Class: D.ClassINET, Ttl: 10} + rr.Target = domain + "." + resp.Answer = append([]D.RR{rr}, resp.Answer...) + } + record, ok := hosts.Search(host, q.Qtype != D.TypeA && q.Qtype != D.TypeAAAA) + if !ok { + if record != nil && record.IsDomain { + // replace request domain + newR := r.Copy() + newR.Question[0].Name = record.Domain + "." + resp, err := next(ctx, newR) + if err == nil { + resp.Id = r.Id + resp.Question = r.Question + handleCName(resp, record.Domain) + } + return resp, err + } return next(ctx, r) } - ip := record.Data() msg := r.Copy() - - if ip.Is4() && q.Qtype == D.TypeA { - rr := &D.A{} - rr.Hdr = D.RR_Header{Name: q.Name, Rrtype: D.TypeA, Class: D.ClassINET, Ttl: 10} - rr.A = ip.AsSlice() - - msg.Answer = []D.RR{rr} - } else if q.Qtype == D.TypeAAAA { - rr := &D.AAAA{} - rr.Hdr = D.RR_Header{Name: q.Name, Rrtype: D.TypeAAAA, Class: D.ClassINET, Ttl: 10} - ip := ip.As16() - rr.AAAA = ip[:] - msg.Answer = []D.RR{rr} - } else { - return next(ctx, r) + handleIPs := func() { + for _, ipAddr := range record.IPs { + if ipAddr.Is4() && q.Qtype == D.TypeA { + rr := &D.A{} + rr.Hdr = D.RR_Header{Name: q.Name, Rrtype: D.TypeA, Class: D.ClassINET, Ttl: 10} + rr.A = ipAddr.AsSlice() + msg.Answer = append(msg.Answer, rr) + if mapping != nil { + mapping.SetWithExpire(ipAddr, host, time.Now().Add(time.Second*10)) + } + } else if q.Qtype == D.TypeAAAA { + rr := &D.AAAA{} + rr.Hdr = D.RR_Header{Name: q.Name, Rrtype: D.TypeAAAA, Class: D.ClassINET, Ttl: 10} + ip := ipAddr.As16() + rr.AAAA = ip[:] + msg.Answer = append(msg.Answer, rr) + if mapping != nil { + mapping.SetWithExpire(ipAddr, host, time.Now().Add(time.Second*10)) + } + } + } } - if mapping != nil { - mapping.SetWithExpire(ip, host, time.Now().Add(time.Second*10)) + switch q.Qtype { + case D.TypeA: + handleIPs() + case D.TypeAAAA: + handleIPs() + case D.TypeCNAME: + handleCName(r, record.Domain) + default: + return next(ctx, r) } ctx.SetType(context.DNSTypeHost) msg.SetRcode(r, D.RcodeSuccess) msg.Authoritative = true msg.RecursionAvailable = true - return msg, nil } } @@ -149,6 +177,7 @@ func withFakeIP(fakePool *fakeip.Pool) middleware { func withResolver(resolver *Resolver) handler { return func(ctx *context.DNSContext, r *D.Msg) (*D.Msg, error) { ctx.SetType(context.DNSTypeRaw) + q := r.Question[0] // return a empty AAAA msg when ipv6 disabled @@ -183,7 +212,7 @@ func NewHandler(resolver *Resolver, mapper *ResolverEnhancer) handler { middlewares := []middleware{} if resolver.hosts != nil { - middlewares = append(middlewares, withHosts(resolver.hosts, mapper.mapping)) + middlewares = append(middlewares, withHosts(R.NewHosts(resolver.hosts), mapper.mapping)) } if mapper.mode == C.DNSFakeIP { diff --git a/dns/resolver.go b/dns/resolver.go index ac8917ca77..c16aad4007 100644 --- a/dns/resolver.go +++ b/dns/resolver.go @@ -4,7 +4,6 @@ import ( "context" "errors" "fmt" - "math/rand" "net/netip" "strings" "time" @@ -20,6 +19,7 @@ import ( "github.com/Dreamacro/clash/log" D "github.com/miekg/dns" + "github.com/zhangyunhao116/fastrand" "golang.org/x/sync/singleflight" ) @@ -42,7 +42,8 @@ type geositePolicyRecord struct { type Resolver struct { ipv6 bool - hosts *trie.DomainTrie[netip.Addr] + ipv6Timeout time.Duration + hosts *trie.DomainTrie[resolver.HostValue] main []dnsClient fallback []dnsClient fallbackDomainFilters []fallbackDomainFilter @@ -91,14 +92,20 @@ func (r *Resolver) LookupIP(ctx context.Context, host string) (ips []netip.Addr, }() ips, err = r.lookupIP(ctx, host, D.TypeA) - + var waitIPv6 *time.Timer + if r != nil { + waitIPv6 = time.NewTimer(r.ipv6Timeout) + } else { + waitIPv6 = time.NewTimer(100 * time.Millisecond) + } + defer waitIPv6.Stop() select { case ipv6s, open := <-ch: if !open && err != nil { return nil, resolver.ErrIPNotFound } ips = append(ips, ipv6s...) - case <-time.After(30 * time.Millisecond): + case <-waitIPv6.C: // wait ipv6 result } @@ -113,7 +120,7 @@ func (r *Resolver) ResolveIP(ctx context.Context, host string) (ip netip.Addr, e } else if len(ips) == 0 { return netip.Addr{}, fmt.Errorf("%w: %s", resolver.ErrIPNotFound, host) } - return ips[rand.Intn(len(ips))], nil + return ips[fastrand.Intn(len(ips))], nil } // LookupIPv4 request with TypeA @@ -129,7 +136,7 @@ func (r *Resolver) ResolveIPv4(ctx context.Context, host string) (ip netip.Addr, } else if len(ips) == 0 { return netip.Addr{}, fmt.Errorf("%w: %s", resolver.ErrIPNotFound, host) } - return ips[rand.Intn(len(ips))], nil + return ips[fastrand.Intn(len(ips))], nil } // LookupIPv6 request with TypeAAAA @@ -145,7 +152,7 @@ func (r *Resolver) ResolveIPv6(ctx context.Context, host string) (ip netip.Addr, } else if len(ips) == 0 { return netip.Addr{}, fmt.Errorf("%w: %s", resolver.ErrIPNotFound, host) } - return ips[rand.Intn(len(ips))], nil + return ips[fastrand.Intn(len(ips))], nil } func (r *Resolver) shouldIPFallback(ip netip.Addr) bool { @@ -419,24 +426,27 @@ type Config struct { Default []NameServer ProxyServer []NameServer IPv6 bool + IPv6Timeout uint EnhancedMode C.DNSMode FallbackFilter FallbackFilter Pool *fakeip.Pool - Hosts *trie.DomainTrie[netip.Addr] + Hosts *trie.DomainTrie[resolver.HostValue] Policy map[string][]NameServer } func NewResolver(config Config) *Resolver { defaultResolver := &Resolver{ - main: transform(config.Default, nil), - lruCache: cache.New(cache.WithSize[string, *D.Msg](4096), cache.WithStale[string, *D.Msg](true)), + main: transform(config.Default, nil), + lruCache: cache.New(cache.WithSize[string, *D.Msg](4096), cache.WithStale[string, *D.Msg](true)), + ipv6Timeout: time.Duration(config.IPv6Timeout) * time.Millisecond, } r := &Resolver{ - ipv6: config.IPv6, - main: transform(config.Main, defaultResolver), - lruCache: cache.New(cache.WithSize[string, *D.Msg](4096), cache.WithStale[string, *D.Msg](true)), - hosts: config.Hosts, + ipv6: config.IPv6, + main: transform(config.Main, defaultResolver), + lruCache: cache.New(cache.WithSize[string, *D.Msg](4096), cache.WithStale[string, *D.Msg](true)), + hosts: config.Hosts, + ipv6Timeout: time.Duration(config.IPv6Timeout) * time.Millisecond, } if len(config.Fallback) != 0 { @@ -502,11 +512,12 @@ func NewResolver(config Config) *Resolver { func NewProxyServerHostResolver(old *Resolver) *Resolver { r := &Resolver{ - ipv6: old.ipv6, - main: old.proxyServer, - lruCache: old.lruCache, - hosts: old.hosts, - policy: old.policy, + ipv6: old.ipv6, + main: old.proxyServer, + lruCache: old.lruCache, + hosts: old.hosts, + policy: trie.New[*Policy](), + ipv6Timeout: old.ipv6Timeout, } return r } diff --git a/dns/util.go b/dns/util.go index 203ab615f8..4821195d40 100644 --- a/dns/util.go +++ b/dns/util.go @@ -66,7 +66,7 @@ func setMsgTTL(msg *D.Msg, ttl uint32) { } func isIPRequest(q D.Question) bool { - return q.Qclass == D.ClassINET && (q.Qtype == D.TypeA || q.Qtype == D.TypeAAAA) + return q.Qclass == D.ClassINET && (q.Qtype == D.TypeA || q.Qtype == D.TypeAAAA || q.Qtype == D.TypeCNAME) } func transform(servers []NameServer, resolver *Resolver) []dnsClient { diff --git a/docker/file-name.sh b/docker/file-name.sh index fb87cad012..1ac2cee0fb 100644 --- a/docker/file-name.sh +++ b/docker/file-name.sh @@ -1,26 +1,25 @@ #!/bin/sh os="clash.meta-linux-" -arch=`uname -m` -case $arch in - "x86_64") +case $TARGETPLATFORM in + "linux/amd64") arch="amd64-compatible" ;; - "x86") - arch="386-cgo" + "linux/386") + arch="386" ;; - "aarch64") + "linux/arm64") arch="arm64" ;; - "armv7l") + "linux/arm/v7") arch="armv7" ;; "riscv64") - arch="riscv64-cgo" + arch="riscv64" ;; *) echo "Unknown architecture" exit 1 ;; esac -file_name="$os$arch" +file_name="$os$arch-$(cat bin/version.txt)" echo $file_name \ No newline at end of file diff --git a/docs/config.yaml b/docs/config.yaml index 35e85ecc5d..3ef0934294 100644 --- a/docs/config.yaml +++ b/docs/config.yaml @@ -7,22 +7,17 @@ mixed-port: 10801 # HTTP(S) 和 SOCKS 代理混合端口 # tproxy-port: 7893 allow-lan: true # 允许局域网连接 -bind-address: "*" # 绑定IP地址,仅作用于 allow-lan 为 true,'*'表示所有地址 +bind-address: "*" # 绑定 IP 地址,仅作用于 allow-lan 为 true,'*'表示所有地址 -# find-process-mode has 3 values: always, strict, off +# find-process-mode has 3 values:always, strict, off # - always, 开启,强制匹配所有进程 -# - strict, 默认,由clash判断是否开启 +# - strict, 默认,由 clash 判断是否开启 # - off, 不匹配进程,推荐在路由器上使用此模式 find-process-mode: strict -# global-client-fingerprint:全局TLS指纹,优先低于proxy内的 client-fingerprint -# accepts "chrome","firefox","safari","ios","random","none" options. -# Utls is currently support TLS transport in TCP/grpc/WS/HTTP for VLESS/Vmess and trojan. -global-client-fingerprint: chrome - mode: rule -#自定义 geox-url +#自定义 geodata url geox-url: geoip: "https://cdn.jsdelivr.net/gh/Loyalsoldier/v2ray-rules-dat@release/geoip.dat" geosite: "https://cdn.jsdelivr.net/gh/Loyalsoldier/v2ray-rules-dat@release/geosite.dat" @@ -32,16 +27,30 @@ log-level: debug # 日志等级 silent/error/warning/info/debug ipv6: true # 开启 IPv6 总开关,关闭阻断所有 IPv6 链接和屏蔽 DNS 请求 AAAA 记录 +tls: + certificate: string # 证书 PEM 格式,或者 证书的路径 + private-key: string # 证书对应的私钥 PEM 格式,或者私钥路径 + custom-certifactes: + - | + -----BEGIN CERTIFICATE----- + format/pem... + -----END CERTIFICATE----- + external-controller: 0.0.0.0:9093 # RESTful API 监听地址 external-controller-tls: 0.0.0.0:9443 # RESTful API HTTPS 监听地址,需要配置 tls 部分配置文件 -# secret: "123456" # `Authorization: Bearer ${secret}` +# secret: "123456" # `Authorization:Bearer ${secret}` -# tcp-concurrent: true # TCP并发连接所有IP, 将使用最快握手的TCP -external-ui: /path/to/ui/folder # 配置WEB UI目录,使用http://{{external-controller}}/ui 访问 +# tcp-concurrent: true # TCP 并发连接所有 IP, 将使用最快握手的 TCP +external-ui: /path/to/ui/folder # 配置 WEB UI 目录,使用 http://{{external-controller}}/ui 访问 # interface-name: en0 # 设置出口网卡 -# routing-mark: 6666 # 配置 fwmark 仅用于Linux +# 全局 TLS 指纹,优先低于 proxy 内的 client-fingerprint +# 可选: "chrome","firefox","safari","ios","random","none" options. +# Utls is currently support TLS transport in TCP/grpc/WS/HTTP for VLESS/Vmess and trojan. +global-client-fingerprint: chrome + +# routing-mark:6666 # 配置 fwmark 仅用于 Linux experimental: # 类似于 /etc/hosts, 仅支持配置单个 IP @@ -49,6 +58,15 @@ hosts: # '*.clash.dev': 127.0.0.1 # '.dev': 127.0.0.1 # 'alpha.clash.dev': '::1' +# test.com: [1.1.1.1, 2.2.2.2] +# clash.lan: clash # clash 为特别字段,将加入本地所有网卡的地址 +# baidu.com: google.com # 只允许配置一个别名 + +profile: # 存储 select 选择记录 + store-selected: false + + # 持久化 fake-ip + store-fake-ip: true # Tun 配置 tun: @@ -75,10 +93,10 @@ tun: #- 1000 # exclude_uid_range: # 排除路由的的用户范围 # - 1000-99999 - + # Android 用户和应用规则仅在 Android 下被支持 # 并且需要 auto_route - + # include_android_user: # 限制被路由的 Android 用户 # - 0 # - 10 @@ -105,15 +123,13 @@ sniffer: # 是否使用嗅探结果作为实际访问,默认 true # 全局配置,优先级低于 sniffer.sniff 实际配置 override-destination: false - sniff: - # TLS 默认如果不配置 ports 默认嗅探 443 + sniff: # TLS 默认如果不配置 ports 默认嗅探 443 TLS: # ports: [443, 8443] - + # 默认嗅探 80 - HTTP: - # 需要嗅探的端口 - + HTTP: # 需要嗅探的端口 + ports: [80, 8080-8880] # 可覆盖 sniffer.override-destination override-destination: true @@ -128,7 +144,7 @@ sniffer: - tls - http # 强制对此域名进行嗅探 - + # 仅对白名单中的端口进行嗅探,默认为 443,80 # 已废弃,若 sniffer.sniff 配置则此项无效 port-whitelist: @@ -136,27 +152,8 @@ sniffer: - "443" # - 8000-9999 -# shadowsocks,vmess 入口配置(传入流量将和socks,mixed等入口一样按照mode所指定的方式进行匹配处理) -# ss-config: ss://2022-blake3-aes-256-gcm:vlmpIPSyHH6f4S8WVPdRIHIlzmB+GIRfoH3aNJ/t9Gg=@:23456 -# vmess-config: vmess://1:9d0cb9d0-964f-4ef6-897d-6c6b3ccf9e68@:12345 -# tuic服务器入口(传入流量将和socks,mixed等入口一样按照mode所指定的方式进行匹配处理) -#tuic-server: -# enable: true -# listen: 127.0.0.1:10443 -# token: -# - TOKEN -# certificate: ./server.crt -# private-key: ./server.key -# congestion-controller: bbr -# max-idle-time: 15000 -# authentication-timeout: 1000 -# alpn: -# - h3 -# max-udp-relay-packet-size: 1500 - -tunnels: - # one line config +tunnels: # one line config - tcp/udp,127.0.0.1:6553,114.114.114.114:53,proxy - tcp,127.0.0.1:6666,rds.mysql.com:3306,vpn # full yaml config @@ -165,12 +162,6 @@ tunnels: target: target.com proxy: proxy -profile: - # 存储select选择记录 - store-selected: false - - # 持久化fake-ip - store-fake-ip: true # DNS配置 dns: @@ -178,7 +169,7 @@ dns: prefer-h3: true # 开启 DoH 支持 HTTP/3,将并发尝试 listen: 0.0.0.0:53 # 开启 DNS 服务器监听 # ipv6: false # false 将返回 AAAA 的空结果 - + # ipv6-timeout: 300 # 单位:ms,内部双栈并发时,向上游查询 AAAA 时,等待 AAAA 的时间,默认 100ms # 用于解析 nameserver,fallback 以及其他DNS服务器配置的,DNS 服务域名 # 只能使用纯 IP 地址,可使用加密 DNS default-nameserver: @@ -187,16 +178,16 @@ dns: - tls://1.12.12.12:853 - tls://223.5.5.5:853 enhanced-mode: fake-ip # or redir-host - + fake-ip-range: 198.18.0.1/16 # fake-ip 池设置 - + # use-hosts: true # 查询 hosts - + # 配置不使用fake-ip的域名 # fake-ip-filter: # - '*.lan' # - localhost.ptlogin2.qq.com - + # DNS主要域名配置 # 支持 UDP,TCP,DoT,DoH,DoQ # 这部分为主要 DNS 配置,影响所有直连,确保使用对大陆解析精准的 DNS @@ -210,20 +201,20 @@ dns: - dhcp://en0 # dns from dhcp - quic://dns.adguard.com:784 # DNS over QUIC # - '8.8.8.8#en0' # 兼容指定DNS出口网卡 - + # 当配置 fallback 时,会查询 nameserver 中返回的 IP 是否为 CN,非必要配置 # 当不是 CN,则使用 fallback 中的 DNS 查询结果 # 确保配置 fallback 时能够正常查询 # fallback: # - tcp://1.1.1.1 # - 'tcp://1.1.1.1#ProxyGroupName' # 指定 DNS 过代理查询,ProxyGroupName 为策略组名或节点名,过代理配置优先于配置出口网卡,当找不到策略组或节点名则设置为出口网卡 - + # 专用于节点域名解析的 DNS 服务器,非必要配置项 # 配置服务器若查询失败将使用 nameserver,非并发查询 # proxy-server-nameserver: # - https://dns.google/dns-query # - tls://one.one.one.one - + # 配置 fallback 使用条件 # fallback-filter: # geoip: true # 配置是否使用 geoip @@ -238,14 +229,54 @@ dns: # - '+.google.com' # - '+.facebook.com' # - '+.youtube.com' - + # 配置查询域名使用的 DNS 服务器 nameserver-policy: # 'www.baidu.com': '114.114.114.114' # '+.internal.crop.com': '10.0.0.1' - "geosite:cn": "https://doh.pub/dns-query" - "www.baidu.com": [https://doh.pub/dns-query,https://dns.alidns.com/dns-query] -proxies: + "geosite:cn,private,apple": + - https://doh.pub/dns-query + - https://dns.alidns.com/dns-query + "www.baidu.com,+.google.cn": [223.5.5.5, https://dns.alidns.com/dns-query] + +proxies: # socks5 + - name: "socks" + type: socks5 + server: server + port: 443 + # username: username + # password: password + # tls: true + # fingerprint: xxxx + # skip-cert-verify: true + # udp: true + # ip-version: ipv6 + + # http + - name: "http" + type: http + server: server + port: 443 + # username: username + # password: password + # tls: true # https + # skip-cert-verify: true + # sni: custom.com + # fingerprint: xxxx # 同 experimental.fingerprints 使用 sha256 指纹,配置协议独立的指纹,将忽略 experimental.fingerprints + # ip-version: dual + + # Snell + # Beware that there's currently no UDP support yet + - name: "snell" + type: snell + server: server + port: 44046 + psk: yourpsk + # version: 2 + # obfs-opts: + # mode: http # or tls + # host: bing.com + # Shadowsocks # cipher支持: # aes-128-gcm aes-192-gcm aes-256-gcm @@ -268,6 +299,7 @@ proxies: # UDP 则为双栈解析,获取结果中的第一个 IPv4 # ipv6-prefer 同 ipv4-prefer # 现有协议都支持此参数,TCP 效果仅在开启 tcp-concurrent 生效 + - name: "ss2" type: ss server: server @@ -278,7 +310,7 @@ proxies: plugin-opts: mode: tls # or http # host: bing.com - + - name: "ss3" type: ss server: server @@ -288,28 +320,67 @@ proxies: plugin: v2ray-plugin plugin-opts: mode: websocket # no QUIC now - # tls: true # wss - # 可使用 openssl x509 -noout -fingerprint -sha256 -inform pem -in yourcert.pem 获取 - # 配置指纹将实现 SSL Pining 效果 - # fingerprint: xxxx - # skip-cert-verify: true - # host: bing.com - # path: "/" - # mux: true - # headers: - # custom: value - - - name: "ss4" + # tls: true # wss + # 可使用 openssl x509 -noout -fingerprint -sha256 -inform pem -in yourcert.pem 获取 + # 配置指纹将实现 SSL Pining 效果 + # fingerprint: xxxx + # skip-cert-verify: true + # host: bing.com + # path: "/" + # mux: true + # headers: + # custom: value + + - 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 - name: "vmess" @@ -332,7 +403,7 @@ proxies: # Host: v2ray.com # max-early-data: 2048 # early-data-header-name: Sec-WebSocket-Protocol - + - name: "vmess-h2" type: vmess server: server @@ -348,7 +419,7 @@ proxies: - http.example.com - http-alt.example.com path: / - + - name: "vmess-http" type: vmess server: server @@ -359,15 +430,15 @@ proxies: # udp: true # network: http # http-opts: - # # method: "GET" - # # path: - # # - '/' - # # - '/video' - # # headers: - # # Connection: - # # - keep-alive + # method: "GET" + # path: + # - '/' + # - '/video' + # headers: + # Connection: + # - keep-alive # ip-version: ipv4 # 设置使用 IP 类型偏好,可选:ipv4,ipv6,dual,默认值:dual - + - name: vmess-grpc server: server port: 443 @@ -383,45 +454,83 @@ proxies: grpc-opts: grpc-service-name: "example" # ip-version: ipv4 - - # socks5 - - name: "socks" - type: socks5 + + # vless + - name: "vless-tcp" + type: vless server: server port: 443 - # username: username - # password: password - # tls: true + uuid: uuid + network: tcp + servername: example.com # AKA SNI + # flow: xtls-rprx-direct # xtls-rprx-origin # enable XTLS + # skip-cert-verify: true + # fingerprint: xxxx + # client-fingerprint: random # Available: "chrome","firefox","safari","random","none" + + - name: "vless-vision" + type: vless + server: server + port: 443 + uuid: uuid + network: tcp + tls: true + udp: true + flow: xtls-rprx-vision + client-fingerprint: chrome # fingerprint: xxxx # skip-cert-verify: true - # udp: true - # ip-version: ipv6 - - # http - - name: "http" - type: http + + - name: "vless-reality-vision" + type: vless server: server port: 443 - # username: username - # password: password - # tls: true # https + uuid: uuid + network: tcp + tls: true + udp: true + flow: xtls-rprx-vision + servername: www.microsoft.com # REALITY servername + reality-opts: + public-key: xxx + short-id: xxx # optional + client-fingerprint: chrome # cannot be empty + + - name: "vless-reality-grpc" + type: vless + server: server + port: 443 + uuid: uuid + network: grpc + tls: true + udp: true + flow: # skip-cert-verify: true - # sni: custom.com - # fingerprint: xxxx # 同 experimental.fingerprints 使用 sha256 指纹,配置协议独立的指纹,将忽略 experimental.fingerprints - # ip-version: dual - - # Snell - # Beware that there's currently no UDP support yet - - name: "snell" - type: snell + client-fingerprint: chrome + servername: testingcf.jsdelivr.net + grpc-opts: + grpc-service-name: "grpc" + reality-opts: + public-key: CrrQSjAG_YkHLwvM2M-7XkKJilgL5upBKCp0od0tLhE + short-id: 10f897e26c4b9478 + + - name: "vless-ws" + type: vless server: server - port: 44046 - psk: yourpsk - # version: 2 - # obfs-opts: - # mode: http # or tls - # host: bing.com - + port: 443 + uuid: uuid + udp: true + tls: true + network: ws + # client-fingerprint: random # Available: "chrome","firefox","safari","random","none" + servername: example.com # priority over wss host + # skip-cert-verify: true + # fingerprint: xxxx + ws-opts: + path: "/" + headers: + Host: example.com + # Trojan - name: "trojan" type: trojan @@ -436,7 +545,7 @@ proxies: # - h2 # - http/1.1 # skip-cert-verify: true - + - name: trojan-grpc server: server port: 443 @@ -449,7 +558,7 @@ proxies: udp: true grpc-opts: grpc-service-name: "example" - + - name: trojan-ws server: server port: 443 @@ -464,7 +573,7 @@ proxies: # path: /path # headers: # Host: example.com - + - name: "trojan-xtls" type: trojan server: server @@ -476,37 +585,7 @@ proxies: # sni: example.com # aka server name # skip-cert-verify: true # fingerprint: xxxx - - # vless - - name: "vless-tcp" - type: vless - server: server - port: 443 - uuid: uuid - network: tcp - servername: example.com # AKA SNI - # flow: xtls-rprx-direct # xtls-rprx-origin # enable XTLS - # skip-cert-verify: true - # fingerprint: xxxx - # client-fingerprint: random # Available: "chrome","firefox","safari","random","none" - - - name: "vless-ws" - type: vless - server: server - port: 443 - uuid: uuid - udp: true - tls: true - network: ws - # client-fingerprint: random # Available: "chrome","firefox","safari","random","none" - servername: example.com # priority over wss host - # skip-cert-verify: true - # fingerprint: xxxx - ws-opts: - path: "/" - headers: - Host: example.com - + #hysteria - name: "hysteria" type: hysteria @@ -532,7 +611,8 @@ proxies: # disable_mtu_discovery: false # fingerprint: xxxx # fast-open: true # 支持 TCP 快速打开,默认为 false - + + # wireguard - name: "wg" type: wireguard server: 162.159.192.1 @@ -542,7 +622,11 @@ proxies: private-key: eCtXsJZ27+4PbhDkHnB923tkUn2Gj59wZw5wFA75MnU= public-key: Cr8hWlKvtDt7nrvf+f0brNQQzabAqrjfBvas9pmowjo= udp: true - # reserved: 'U4An' + reserved: "U4An" + # 数组格式也是合法的 + # reserved: [209,98,59] + + # tuic - name: tuic server: www.example.com port: 10443 @@ -551,16 +635,17 @@ proxies: # ip: 127.0.0.1 # for overwriting the DNS lookup result of the server address set in option 'server' # heartbeat-interval: 10000 # alpn: [h3] - # disable-sni: true + disable-sni: true reduce-rtt: true - # request-timeout: 8000 + request-timeout: 8000 udp-relay-mode: native # Available: "native", "quic". Default: "native" # congestion-controller: bbr # Available: "cubic", "new_reno", "bbr". Default: "cubic" # max-udp-relay-packet-size: 1500 # fast-open: true # skip-cert-verify: true # max-open-streams: 20 # default 100, too many open streams may hurt performance - + # sni: example.com + # ShadowsocksR # The supported ciphers (encryption methods): all stream ciphers in ss # The supported obfses: @@ -581,8 +666,7 @@ proxies: # protocol-param: "#" # udp: true -proxy-groups: - # 代理链,若落地协议支持 UDP over TCP 则可支持 UDP +proxy-groups: # 代理链,若落地协议支持 UDP over TCP 则可支持 UDP # Traffic: clash <-> http <-> vmess <-> ss1 <-> ss2 <-> Internet - name: "relay" type: relay @@ -591,7 +675,7 @@ proxy-groups: - vmess - ss1 - ss2 - + # url-test 将按照 url 测试结果使用延迟最低节点 - name: "auto" type: url-test @@ -603,7 +687,7 @@ proxy-groups: # lazy: true url: "https://cp.cloudflare.com/generate_204" interval: 300 - + # fallback 将按照 url 测试结果按照节点顺序选择 - name: "fallback-auto" type: fallback @@ -613,7 +697,7 @@ proxy-groups: - vmess1 url: "https://cp.cloudflare.com/generate_204" interval: 300 - + # load-balance 将按照算法随机选择节点 - name: "load-balance" type: load-balance @@ -623,8 +707,8 @@ proxy-groups: - vmess1 url: "https://cp.cloudflare.com/generate_204" interval: 300 - # strategy: consistent-hashing # 可选 round-robin 和 sticky-sessions - + # strategy: consistent-hashing # 可选 round-robin 和 sticky-sessions + # select 用户自行选择节点 - name: Proxy type: select @@ -634,7 +718,7 @@ proxy-groups: - ss2 - vmess1 - auto - + # 配置指定 interface-name 和 fwmark 的 DIRECT - name: en1 type: select @@ -642,7 +726,7 @@ proxy-groups: routing-mark: 6667 proxies: - DIRECT - + - name: UseProvider type: select filter: "HK|TW" # 正则表达式,过滤 provider1 中节点名包含 HK 或 TW @@ -689,7 +773,8 @@ rules: - DOMAIN-KEYWORD,google,ss1 - IP-CIDR,1.1.1.1/32,ss1 - IP-CIDR6,2409::/64,DIRECT - - SUB-RULE,(OR,((NETWORK,TCP),(NETWORK,UDP))),sub-rule-name1 # 当满足条件是 TCP 或 UDP 流量时,使用名为 sub-rule-name1 当规则集 + # 当满足条件是 TCP 或 UDP 流量时,使用名为 sub-rule-name1 的规则集 + - SUB-RULE,(OR,((NETWORK,TCP),(NETWORK,UDP))),sub-rule-name1 - SUB-RULE,(AND,((NETWORK,UDP))),sub-rule-name2 # 定义多个子规则集,规则将以分叉匹配,使用 SUB-RULE 使用 # google.com(not match)--> baidu.com(match) @@ -716,15 +801,6 @@ sub-rules: - IP-CIDR,8.8.8.8/32,ss1 - DOMAIN,dns.alidns.com,REJECT -tls: - certificate: string # 证书 PEM 格式,或者 证书的路径 - private-key: string # 证书对应的私钥 PEM 格式,或者私钥路径 - # 自定义证书验证,将加入 Clash 证书验证中,绝大多数 TLS 相关支持,如:DNS - # 可用于自定义证书的验证 - custom-certificates: - - certificate: string # 证书 PEM 格式,或者 证书的路径 - private-key: string # 证书对应的私钥 PEM 格式,或者私钥路径 - # 流量入站 listeners: - name: socks5-in-1 @@ -734,14 +810,14 @@ listeners: # rule: sub-rule-name1 # 默认使用 rules,如果未找到 sub-rule 则直接使用 rules # proxy: proxy # 如果不为空则直接将该入站流量交由指定proxy处理 # udp: false # 默认 true - + - name: http-in-1 type: http port: 10809 listen: 0.0.0.0 # rule: sub-rule-name1 # 默认使用 rules,如果未找到 sub-rule 则直接使用 rules # proxy: proxy # 如果不为空则直接将该入站流量交由指定proxy处理(当proxy不为空时,这里的proxy名称必须合法,否则会出错) - + - name: mixed-in-1 type: mixed # HTTP(S) 和 SOCKS 代理混合 port: 10810 @@ -749,14 +825,14 @@ listeners: # rule: sub-rule-name1 # 默认使用 rules,如果未找到 sub-rule 则直接使用 rules # proxy: proxy # 如果不为空则直接将该入站流量交由指定proxy处理(当proxy不为空时,这里的proxy名称必须合法,否则会出错) # udp: false # 默认 true - + - name: reidr-in-1 type: redir port: 10811 listen: 0.0.0.0 # rule: sub-rule-name1 # 默认使用 rules,如果未找到 sub-rule 则直接使用 rules # proxy: proxy # 如果不为空则直接将该入站流量交由指定proxy处理(当proxy不为空时,这里的proxy名称必须合法,否则会出错) - + - name: tproxy-in-1 type: tproxy port: 10812 @@ -764,7 +840,7 @@ listeners: # rule: sub-rule-name1 # 默认使用 rules,如果未找到 sub-rule 则直接使用 rules # proxy: proxy # 如果不为空则直接将该入站流量交由指定proxy处理(当proxy不为空时,这里的proxy名称必须合法,否则会出错) # udp: false # 默认 true - + - name: shadowsocks-in-1 type: shadowsocks port: 10813 @@ -773,7 +849,7 @@ listeners: # proxy: proxy # 如果不为空则直接将该入站流量交由指定proxy处理(当proxy不为空时,这里的proxy名称必须合法,否则会出错) password: vlmpIPSyHH6f4S8WVPdRIHIlzmB+GIRfoH3aNJ/t9Gg= cipher: 2022-blake3-aes-256-gcm - + - name: vmess-in-1 type: vmess port: 10814 @@ -784,7 +860,7 @@ listeners: - username: 1 uuid: 9d0cb9d0-964f-4ef6-897d-6c6b3ccf9e68 alterId: 1 - + - name: tuic-in-1 type: tuic port: 10815 @@ -801,7 +877,7 @@ listeners: # alpn: # - h3 # max-udp-relay-packet-size: 1500 - + - name: tunnel-in-1 type: tunnel port: 10816 @@ -810,7 +886,7 @@ listeners: # proxy: proxy # 如果不为空则直接将该入站流量交由指定proxy处理(当proxy不为空时,这里的proxy名称必须合法,否则会出错) network: [tcp, udp] target: target.com - + - name: tun-in-1 type: tun # rule: sub-rule-name1 # 默认使用 rules,如果未找到 sub-rule 则直接使用 rules @@ -826,25 +902,25 @@ listeners: inet6-address: # 必须手动设置ipv6地址段 - "fdfe:dcba:9877::1/126" # strict_route: true # 将所有连接路由到tun来防止泄漏,但你的设备将无法其他设备被访问 - # inet4_route_address: # 启用 auto_route 时使用自定义路由而不是默认路由 - # - 0.0.0.0/1 - # - 128.0.0.0/1 - # inet6_route_address: # 启用 auto_route 时使用自定义路由而不是默认路由 - # - "::/1" - # - "8000::/1" + # inet4_route_address: # 启用 auto_route 时使用自定义路由而不是默认路由 + # - 0.0.0.0/1 + # - 128.0.0.0/1 + # inet6_route_address: # 启用 auto_route 时使用自定义路由而不是默认路由 + # - "::/1" + # - "8000::/1" # endpoint_independent_nat: false # 启用独立于端点的 NAT # include_uid: # UID 规则仅在 Linux 下被支持,并且需要 auto_route # - 0 # include_uid_range: # 限制被路由的的用户范围 # - 1000-99999 # exclude_uid: # 排除路由的的用户 - #- 1000 + # - 1000 # exclude_uid_range: # 排除路由的的用户范围 # - 1000-99999 - + # Android 用户和应用规则仅在 Android 下被支持 # 并且需要 auto_route - + # include_android_user: # 限制被路由的 Android 用户 # - 0 # - 10 @@ -852,3 +928,23 @@ listeners: # - com.android.chrome # exclude_package: # 排除被路由的 Android 应用包名 # - com.android.captiveportallogin + +# 入口配置与 Listener 等价,传入流量将和 socks,mixed 等入口一样按照 mode 所指定的方式进行匹配处理 +# shadowsocks,vmess 入口配置(传入流量将和socks,mixed等入口一样按照mode所指定的方式进行匹配处理) +# ss-config: ss://2022-blake3-aes-256-gcm:vlmpIPSyHH6f4S8WVPdRIHIlzmB+GIRfoH3aNJ/t9Gg=@:23456 +# vmess-config: vmess://1:9d0cb9d0-964f-4ef6-897d-6c6b3ccf9e68@:12345 + +# tuic服务器入口(传入流量将和socks,mixed等入口一样按照mode所指定的方式进行匹配处理) +# tuic-server: +# enable: true +# listen: 127.0.0.1:10443 +# token: +# - TOKEN +# certificate: ./server.crt +# private-key: ./server.key +# congestion-controller: bbr +# max-idle-time: 15000 +# authentication-timeout: 1000 +# alpn: +# - h3 +# max-udp-relay-packet-size: 1500 diff --git a/flake.nix b/flake.nix index 88a6eacbc0..ffd186294a 100644 --- a/flake.nix +++ b/flake.nix @@ -28,7 +28,7 @@ inherit version; src = ./.; - vendorSha256 = "sha256-8cbcE9gKJjU14DNTLPc6nneEPZg7Akt+FlSDlPRvG5k="; + vendorSha256 = "sha256-W5oiPtTRin0731QQWr98xZ2Vpk97HYcBtKoi1OKZz+w="; # Do not build testing suit excludedPackages = [ "./test" ]; diff --git a/go.mod b/go.mod index c2a979dc7d..c8eb6e108c 100644 --- a/go.mod +++ b/go.mod @@ -3,6 +3,7 @@ module github.com/Dreamacro/clash go 1.19 require ( + github.com/3andne/restls-client-go v0.1.4 github.com/aead/chacha20 v0.0.0-20180709150244-8b13a72661da github.com/cilium/ebpf v0.9.3 github.com/coreos/go-iptables v0.6.0 @@ -10,7 +11,7 @@ require ( github.com/go-chi/chi/v5 v5.0.8 github.com/go-chi/cors v1.2.1 github.com/go-chi/render v1.0.2 - github.com/gofrs/uuid v4.3.1+incompatible + github.com/gofrs/uuid v4.4.0+incompatible github.com/google/gopacket v1.1.19 github.com/gorilla/websocket v1.5.0 github.com/hashicorp/golang-lru v0.5.4 @@ -18,39 +19,41 @@ require ( github.com/jpillora/backoff v1.0.0 github.com/lunixbochs/struc v0.0.0-20200707160740-784aaebc1d40 github.com/mdlayher/netlink v1.7.2-0.20221213171556-9881fafed8c7 - github.com/metacubex/quic-go v0.32.0 - github.com/metacubex/sing-shadowsocks v0.1.1-0.20230202072246-e2bef5f088c7 - github.com/metacubex/sing-tun v0.1.1-0.20230213124625-28d27a0c236b - github.com/metacubex/sing-wireguard v0.0.0-20230213124601-d04406a109b4 - github.com/miekg/dns v1.1.50 + github.com/metacubex/quic-go v0.33.3-0.20230322045857-901b636b4594 + github.com/metacubex/sing-shadowsocks v0.1.1-0.20230226153717-4e80da7e6947 + github.com/metacubex/sing-tun v0.1.3-0.20230323115055-7935ba0ac8b3 + github.com/metacubex/sing-wireguard v0.0.0-20230310035749-f7595fcae5cb + github.com/miekg/dns v1.1.52 github.com/mroth/weightedrand/v2 v2.0.0 github.com/oschwald/geoip2-golang v1.8.0 - github.com/refraction-networking/utls v1.2.0 github.com/sagernet/netlink v0.0.0-20220905062125-8043b4a9aa97 - github.com/sagernet/sing v0.1.7-0.20230207063819-27d2950cdbe9 - github.com/sagernet/sing-vmess v0.1.1-0.20230212211128-cb4e47dd0acb - github.com/sagernet/tfo-go v0.0.0-20230207095944-549363a7327d + github.com/sagernet/sing v0.2.1-0.20230323071235-f8038854d286 + github.com/sagernet/sing-shadowtls v0.1.0 + github.com/sagernet/sing-vmess v0.1.3 + github.com/sagernet/tfo-go v0.0.0-20230303015439-ffcfd8c41cf9 + github.com/sagernet/utls v0.0.0-20230309024959-6732c2ab36f2 github.com/sagernet/wireguard-go v0.0.0-20221116151939-c99467f53f2c github.com/samber/lo v1.37.0 github.com/sirupsen/logrus v1.9.0 - github.com/stretchr/testify v1.8.1 + github.com/stretchr/testify v1.8.2 github.com/xtls/go v0.0.0-20220914232946-0441cf4cf837 + github.com/zhangyunhao116/fastrand v0.3.0 go.etcd.io/bbolt v1.3.6 go.uber.org/atomic v1.10.0 go.uber.org/automaxprocs v1.5.1 - golang.org/x/crypto v0.5.0 - golang.org/x/exp v0.0.0-20221205204356-47842c84f3db - golang.org/x/net v0.5.0 + golang.org/x/crypto v0.7.0 + golang.org/x/exp v0.0.0-20230321023759-10a507213a29 + golang.org/x/net v0.8.0 golang.org/x/sync v0.1.0 - golang.org/x/sys v0.4.0 - google.golang.org/protobuf v1.28.1 + golang.org/x/sys v0.6.0 + google.golang.org/protobuf v1.28.2-0.20230118093459-a9481185b34d gopkg.in/yaml.v3 v3.0.1 lukechampine.com/blake3 v1.1.7 ) require ( github.com/ajg/form v1.5.1 // indirect - github.com/andybalholm/brotli v1.0.4 // indirect + github.com/andybalholm/brotli v1.0.5 // indirect github.com/davecgh/go-spew v1.1.1 // indirect github.com/fsnotify/fsnotify v1.6.0 // indirect github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0 // indirect @@ -59,25 +62,23 @@ require ( github.com/google/go-cmp v0.5.9 // indirect github.com/google/pprof v0.0.0-20210407192527-94a9f03dee38 // indirect github.com/josharian/native v1.1.0 // indirect - github.com/klauspost/compress v1.15.12 // indirect + github.com/klauspost/compress v1.15.15 // indirect github.com/klauspost/cpuid/v2 v2.0.12 // indirect github.com/mdlayher/socket v0.4.0 // indirect - github.com/metacubex/gvisor v0.0.0-20230213124051-7a16c835d80e // indirect + github.com/metacubex/gvisor v0.0.0-20230323114922-412956fb6a03 // indirect github.com/onsi/ginkgo/v2 v2.2.0 // indirect github.com/oschwald/maxminddb-golang v1.10.0 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect github.com/quic-go/qpack v0.4.0 // indirect - github.com/quic-go/qtls-go1-18 v0.2.0 // indirect - github.com/quic-go/qtls-go1-19 v0.2.0 // indirect - github.com/quic-go/qtls-go1-20 v0.1.0 // indirect - github.com/sagernet/abx-go v0.0.0-20220819185957-dba1257d738e // indirect + github.com/quic-go/qtls-go1-19 v0.2.1 // indirect + github.com/quic-go/qtls-go1-20 v0.1.1 // indirect github.com/sagernet/go-tun2socks v1.16.12-0.20220818015926-16cb67876a61 // indirect github.com/u-root/uio v0.0.0-20221213070652-c3537552635f // indirect github.com/vishvananda/netns v0.0.0-20211101163701-50045581ed74 // indirect - golang.org/x/mod v0.6.0 // indirect - golang.org/x/text v0.6.0 // indirect - golang.org/x/time v0.0.0-20191024005414-555d28b269f0 // indirect - golang.org/x/tools v0.2.0 // indirect + golang.org/x/mod v0.8.0 // indirect + golang.org/x/text v0.8.0 // indirect + golang.org/x/time v0.0.0-20220210224613-90d013bbcef8 // indirect + golang.org/x/tools v0.6.0 // indirect ) replace go.uber.org/atomic v1.10.0 => github.com/metacubex/uber-atomic v0.0.0-20230202125923-feb10b770370 diff --git a/go.sum b/go.sum index 076442ab71..f56aed7a29 100644 --- a/go.sum +++ b/go.sum @@ -1,9 +1,11 @@ +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= github.com/ajg/form v1.5.1/go.mod h1:uL1WgH+h2mgNtvBq0339dVnzXdBETtL2LeUXaIv25UY= -github.com/andybalholm/brotli v1.0.4 h1:V7DdXeJtZscaqfNuAdSRuRFzuiKlHSC/Zh3zl9qY3JY= -github.com/andybalholm/brotli v1.0.4/go.mod h1:fO7iG3H7G2nSZ7m0zPUDn85XEX2GTukHGRSepvi9Eig= +github.com/andybalholm/brotli v1.0.5 h1:8uQZIdzKmjc/iuPu7O2ioW48L81FgatrcpfFmiq/cCs= +github.com/andybalholm/brotli v1.0.5/go.mod h1:fO7iG3H7G2nSZ7m0zPUDn85XEX2GTukHGRSepvi9Eig= github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= @@ -28,8 +30,8 @@ github.com/go-chi/render v1.0.2 h1:4ER/udB0+fMWB2Jlf15RV3F4A2FDuYi/9f+lFttR/Lg= github.com/go-chi/render v1.0.2/go.mod h1:/gr3hVkmYR0YlEy3LxCuVRFzEu9Ruok+gFqbIofjao0= github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0 h1:p104kn46Q8WdvHunIJ9dAyjPVtrBPhSr3KT2yUst43I= github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0/go.mod h1:fyg7847qk6SyHyPtNmDHnmrv/HOrqktSC+C9fM+CJOE= -github.com/gofrs/uuid v4.3.1+incompatible h1:0/KbAdpx3UXAx1kEOWHJeOkpbgRFGHVgv+CFIY7dBJI= -github.com/gofrs/uuid v4.3.1+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM= +github.com/gofrs/uuid v4.4.0+incompatible h1:3qXRTX8/NbyulANqlc0lchS1gqAVxRgsuW1YrTJupqA= +github.com/gofrs/uuid v4.4.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM= github.com/golang/mock v1.6.0 h1:ErTB+efbowRARo13NNdxyJji2egdxLGQhRaY+DUumQc= github.com/golang/mock v1.6.0/go.mod h1:p6yTPP+5HYm5mzsMV8JkE6ZKdX+/wYM6Hr+LicevLPs= github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= @@ -67,8 +69,8 @@ github.com/jsimonetti/rtnetlink v0.0.0-20200117123717-f846d4f6c1f4/go.mod h1:WGu github.com/jsimonetti/rtnetlink v0.0.0-20201009170750-9c6f07d100c1/go.mod h1:hqoO/u39cqLeBLebZ8fWdE96O7FxrAsRYhnVOdgHxok= github.com/jsimonetti/rtnetlink v0.0.0-20201110080708-d2c240429e6c/go.mod h1:huN4d1phzjhlOsNIjFsw2SVRbwIHj3fJDMEU2SDPTmg= github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU= -github.com/klauspost/compress v1.15.12 h1:YClS/PImqYbn+UILDnqxQCZ3RehC9N318SU3kElDUEM= -github.com/klauspost/compress v1.15.12/go.mod h1:QPwzmACJjUTFsnSHH934V6woptycfrDDJnH7hvFVbGM= +github.com/klauspost/compress v1.15.15 h1:EF27CXIuDsYJ6mmvtBRlEuB2UVOqHG1tAXgZ7yIO+lw= +github.com/klauspost/compress v1.15.15/go.mod h1:ZcK2JAFqKOpnBlxcLsJzYfrS9X1akm9fHZNnD9+Vo/4= github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= github.com/klauspost/cpuid/v2 v2.0.12 h1:p9dKCg8i4gmOxtv35DvrYoWqYzQrvEVdjQ762Y0OqZE= github.com/klauspost/cpuid/v2 v2.0.12/go.mod h1:g2LTdtYhdyuGPqyWyv7qRAmj1WBqxuObKfj5c0PQa7c= @@ -87,20 +89,20 @@ github.com/mdlayher/raw v0.0.0-20190606142536-fef19f00fc18/go.mod h1:7EpbotpCmVZ github.com/mdlayher/raw v0.0.0-20191009151244-50f2db8cc065/go.mod h1:7EpbotpCmVZcu+KCX4g9WaRNuu11uyhiW7+Le1dKawg= github.com/mdlayher/socket v0.4.0 h1:280wsy40IC9M9q1uPGcLBwXpcTQDtoGwVt+BNoITxIw= github.com/mdlayher/socket v0.4.0/go.mod h1:xxFqz5GRCUN3UEOm9CZqEJsAbe1C8OwSK46NlmWuVoc= -github.com/metacubex/gvisor v0.0.0-20230213124051-7a16c835d80e h1:j4j2dlV2d//FAsQlRUriH6nvv36AEAhECbNy7narf1M= -github.com/metacubex/gvisor v0.0.0-20230213124051-7a16c835d80e/go.mod h1:abc7OdNmWlhcNHz84ECEosd5ND5pnWQmD8W55p/4cuc= -github.com/metacubex/quic-go v0.32.0 h1:dSD8LB4MSeBuD4otd8y1DUZcRdDcEB0Ax5esPOqn2Hw= -github.com/metacubex/quic-go v0.32.0/go.mod h1:yParIzDYUd/t/pzFlDtZKhnvSqbUu0bPChlKEGmJStA= -github.com/metacubex/sing-shadowsocks v0.1.1-0.20230202072246-e2bef5f088c7 h1:MNCGIpXhxXn9ck5bxfm/cW9Nr2FGQ5cakcGK0yKZcak= -github.com/metacubex/sing-shadowsocks v0.1.1-0.20230202072246-e2bef5f088c7/go.mod h1:8pBSYDKVxTtqUtGZyEh4ZpFJXwP6wBVVKrs6oQiOwmQ= -github.com/metacubex/sing-tun v0.1.1-0.20230213124625-28d27a0c236b h1:ZF/oNrSCaxIFoZmFQCiUx67t9aENZjyuqw2n4zw3L2o= -github.com/metacubex/sing-tun v0.1.1-0.20230213124625-28d27a0c236b/go.mod h1:TjuaYuR/g1MaY3um89xTfRNt61FJ2IcI/m5zD8QBxw4= -github.com/metacubex/sing-wireguard v0.0.0-20230213124601-d04406a109b4 h1:d96mCF/LYyC9kULd2xwcXfP0Jd8klrOngmRxuUIZg/8= -github.com/metacubex/sing-wireguard v0.0.0-20230213124601-d04406a109b4/go.mod h1:p2VpJuxRefgVMxc8cmatMGSFNvYbjMYMsXJOe7qFstw= +github.com/metacubex/gvisor v0.0.0-20230323114922-412956fb6a03 h1:gREIdurac9fpyBMBRPPMF/Sk3gKfPfdNCa4GQyR9FoA= +github.com/metacubex/gvisor v0.0.0-20230323114922-412956fb6a03/go.mod h1:wqEuzdImyqD2MCGE8CYRJXbB77oSEJeoSSXXdwKjnsE= +github.com/metacubex/quic-go v0.33.3-0.20230322045857-901b636b4594 h1:KD96JPdTIayTGGgRl6PuVqo2Bpo6+x3LqDDyqrYDDXw= +github.com/metacubex/quic-go v0.33.3-0.20230322045857-901b636b4594/go.mod h1:9nOiGX6kqV3+ZbkDKdTNzdFD726QQHPH6WDb36jUSpA= +github.com/metacubex/sing-shadowsocks v0.1.1-0.20230226153717-4e80da7e6947 h1:NnjC2+aIiyzzvFlo+C2WzBOJdsp+HAtu18FZomqYhUE= +github.com/metacubex/sing-shadowsocks v0.1.1-0.20230226153717-4e80da7e6947/go.mod h1:U2gwhxzqgbhKCgn2B4z3t0Cj0LpMWFl/02BGCoG421w= +github.com/metacubex/sing-tun v0.1.3-0.20230323115055-7935ba0ac8b3 h1:LnKcLs0HI0HX4xH/2XerX+1BLXS1Uj6Xvzn20xFuCOk= +github.com/metacubex/sing-tun v0.1.3-0.20230323115055-7935ba0ac8b3/go.mod h1:0i22nk0tgkQz/N96hrhPib1O/C5AjxSnco7Mwi2YSF0= +github.com/metacubex/sing-wireguard v0.0.0-20230310035749-f7595fcae5cb h1:uhvzbtOvyg2c1k1H2EeVPuPvTEjDHCq4+U0AljG40P8= +github.com/metacubex/sing-wireguard v0.0.0-20230310035749-f7595fcae5cb/go.mod h1:7mPG9qYln+CLKBcDt7Dk4c7b3S53VzEfexMVPe6T6FM= github.com/metacubex/uber-atomic v0.0.0-20230202125923-feb10b770370 h1:UkViS4DCESAUEYgbIEQdD02hyMacFt6Dny+1MOJtNIo= github.com/metacubex/uber-atomic v0.0.0-20230202125923-feb10b770370/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0= -github.com/miekg/dns v1.1.50 h1:DQUfb9uc6smULcREF09Uc+/Gd46YWqJd5DbpPE9xkcA= -github.com/miekg/dns v1.1.50/go.mod h1:e3IlAVfNqAllflbibAZEWOXOQ+Ynzk/dDozDxY7XnME= +github.com/miekg/dns v1.1.52 h1:Bmlc/qsNNULOe6bpXcUTsuOajd0DzRHwup6D9k1An0c= +github.com/miekg/dns v1.1.52/go.mod h1:uInx36IzPl7FYnDcMeVWxj9byh7DutNykX4G9Sj60FY= github.com/mroth/weightedrand/v2 v2.0.0 h1:ADehnByWbliEDIazDAKFdBHoqgHSXAkgyKqM/9YsPoo= github.com/mroth/weightedrand/v2 v2.0.0/go.mod h1:f2faGsfOGOwc1p94wzHKKZyTpcJUW7OJ/9U4yfiNAOU= github.com/onsi/ginkgo/v2 v2.2.0 h1:3ZNA3L1c5FYDFTTxbFeVGGD8jYvjYauHD30YgLxVsNI= @@ -115,28 +117,26 @@ github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZN github.com/prashantv/gostub v1.1.0 h1:BTyx3RfQjRHnUWaGF9oQos79AlQ5k8WNktv7VGvVH4g= github.com/quic-go/qpack v0.4.0 h1:Cr9BXA1sQS2SmDUWjSofMPNKmvF6IiIfDRmgU0w1ZCo= github.com/quic-go/qpack v0.4.0/go.mod h1:UZVnYIfi5GRk+zI9UMaCPsmZ2xKJP7XBUvVyT1Knj9A= -github.com/quic-go/qtls-go1-18 v0.2.0 h1:5ViXqBZ90wpUcZS0ge79rf029yx0dYB0McyPJwqqj7U= -github.com/quic-go/qtls-go1-18 v0.2.0/go.mod h1:moGulGHK7o6O8lSPSZNoOwcLvJKJ85vVNc7oJFD65bc= -github.com/quic-go/qtls-go1-19 v0.2.0 h1:Cvn2WdhyViFUHoOqK52i51k4nDX8EwIh5VJiVM4nttk= -github.com/quic-go/qtls-go1-19 v0.2.0/go.mod h1:ySOI96ew8lnoKPtSqx2BlI5wCpUVPT05RMAlajtnyOI= -github.com/quic-go/qtls-go1-20 v0.1.0 h1:d1PK3ErFy9t7zxKsG3NXBJXZjp/kMLoIb3y/kV54oAI= -github.com/quic-go/qtls-go1-20 v0.1.0/go.mod h1:JKtK6mjbAVcUTN/9jZpvLbGxvdWIKS8uT7EiStoU1SM= -github.com/refraction-networking/utls v1.2.0 h1:U5f8wkij2NVinfLuJdFP3gCMwIHs+EzvhxmYdXgiapo= -github.com/refraction-networking/utls v1.2.0/go.mod h1:NPq+cVqzH7D1BeOkmOcb5O/8iVewAsiVt2x1/eO0hgQ= +github.com/quic-go/qtls-go1-19 v0.2.1 h1:aJcKNMkH5ASEJB9FXNeZCyTEIHU1J7MmHyz1Q1TSG1A= +github.com/quic-go/qtls-go1-19 v0.2.1/go.mod h1:ySOI96ew8lnoKPtSqx2BlI5wCpUVPT05RMAlajtnyOI= +github.com/quic-go/qtls-go1-20 v0.1.1 h1:KbChDlg82d3IHqaj2bn6GfKRj84Per2VGf5XV3wSwQk= +github.com/quic-go/qtls-go1-20 v0.1.1/go.mod h1:JKtK6mjbAVcUTN/9jZpvLbGxvdWIKS8uT7EiStoU1SM= github.com/rogpeppe/go-internal v1.6.1 h1:/FiVV8dS/e+YqF2JvO3yXRFbBLTIuSDkuC7aBOAvL+k= -github.com/sagernet/abx-go v0.0.0-20220819185957-dba1257d738e h1:5CFRo8FJbCuf5s/eTBdZpmMbn8Fe2eSMLNAYfKanA34= -github.com/sagernet/abx-go v0.0.0-20220819185957-dba1257d738e/go.mod h1:qbt0dWObotCfcjAJJ9AxtFPNSDUfZF+6dCpgKEOBn/g= github.com/sagernet/go-tun2socks v1.16.12-0.20220818015926-16cb67876a61 h1:5+m7c6AkmAylhauulqN/c5dnh8/KssrE9c93TQrXldA= github.com/sagernet/go-tun2socks v1.16.12-0.20220818015926-16cb67876a61/go.mod h1:QUQ4RRHD6hGGHdFMEtR8T2P6GS6R3D/CXKdaYHKKXms= github.com/sagernet/netlink v0.0.0-20220905062125-8043b4a9aa97 h1:iL5gZI3uFp0X6EslacyapiRz7LLSJyr4RajF/BhMVyE= github.com/sagernet/netlink v0.0.0-20220905062125-8043b4a9aa97/go.mod h1:xLnfdiJbSp8rNqYEdIW/6eDO4mVoogml14Bh2hSiFpM= github.com/sagernet/sing v0.0.0-20220817130738-ce854cda8522/go.mod h1:QVsS5L/ZA2Q5UhQwLrn0Trw+msNd/NPGEhBKR/ioWiY= -github.com/sagernet/sing v0.1.7-0.20230207063819-27d2950cdbe9 h1:qnXh4RjHsNjdZXkfbqwVqAzYUfc160gfkS5gepmsA+A= -github.com/sagernet/sing v0.1.7-0.20230207063819-27d2950cdbe9/go.mod h1:JLSXsPTGRJFo/3X7EcAOCUgJH2/gAoxSJgBsnCZRp/w= -github.com/sagernet/sing-vmess v0.1.1-0.20230212211128-cb4e47dd0acb h1:oyd3w17fXNmWVYFUe17YVHJW5CLW9X2mxJFDP/IWrAM= -github.com/sagernet/sing-vmess v0.1.1-0.20230212211128-cb4e47dd0acb/go.mod h1:9KkmnQzTL4Gvv8U2TRAH2BOITCGsGPpHtUPP5sxn5sY= -github.com/sagernet/tfo-go v0.0.0-20230207095944-549363a7327d h1:trP/l6ZPWvQ/5Gv99Z7/t/v8iYy06akDMejxW1sznUk= -github.com/sagernet/tfo-go v0.0.0-20230207095944-549363a7327d/go.mod h1:jk6Ii8Y3En+j2KQDLgdgQGwb3M6y7EL567jFnGYhN9g= +github.com/sagernet/sing v0.2.1-0.20230323071235-f8038854d286 h1:0Td2b5l1KgrdlOnbRWgFFWsyb0TLoq/tP6j9Lut4JN0= +github.com/sagernet/sing v0.2.1-0.20230323071235-f8038854d286/go.mod h1:9uHswk2hITw8leDbiLS/xn0t9nzBcbePxzm9PJhwdlw= +github.com/sagernet/sing-shadowtls v0.1.0 h1:05MYce8aR5xfKIn+y7xRFsdKhKt44QZTSEQW+lG5IWQ= +github.com/sagernet/sing-shadowtls v0.1.0/go.mod h1:Kn1VUIprdkwCgkS6SXYaLmIpKzQbqBIKJBMY+RvBhYc= +github.com/sagernet/sing-vmess v0.1.3 h1:q/+tsF46dvvapL6CpQBgPHJ6nQrDUZqEtLHCbsjO7iM= +github.com/sagernet/sing-vmess v0.1.3/go.mod h1:GVXqAHwe9U21uS+Voh4YBIrADQyE4F9v0ayGSixSQAE= +github.com/sagernet/tfo-go v0.0.0-20230303015439-ffcfd8c41cf9 h1:2ItpW1nMNkPzmBTxV0/eClCklHrFSQMnUGcpUmJxVeE= +github.com/sagernet/tfo-go v0.0.0-20230303015439-ffcfd8c41cf9/go.mod h1:FUyTEc5ye5NjKnDTDMuiLF2M6T4BE6y6KZuax//UCEg= +github.com/sagernet/utls v0.0.0-20230309024959-6732c2ab36f2 h1:kDUqhc9Vsk5HJuhfIATJ8oQwBmpOZJuozQG7Vk88lL4= +github.com/sagernet/utls v0.0.0-20230309024959-6732c2ab36f2/go.mod h1:JKQMZq/O2qnZjdrt+B57olmfgEmLtY9iiSIEYtWvoSM= github.com/sagernet/wireguard-go v0.0.0-20221116151939-c99467f53f2c h1:vK2wyt9aWYHHvNLWniwijBu/n4pySypiKRhN32u/JGo= github.com/sagernet/wireguard-go v0.0.0-20221116151939-c99467f53f2c/go.mod h1:euOmN6O5kk9dQmgSS8Df4psAl3TCjxOz0NW60EWkSaI= github.com/samber/lo v1.37.0 h1:XjVcB8g6tgUp8rsPsJ2CvhClfImrpL04YpQHXeHPhRw= @@ -153,8 +153,8 @@ github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/ github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= -github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk= -github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= +github.com/stretchr/testify v1.8.2 h1:+h33VjcLVPDHtOdpUCuF+7gSuG3yGIftsP1YvFihtJ8= +github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= github.com/u-root/uio v0.0.0-20221213070652-c3537552635f h1:dpx1PHxYqAnXzbryJrWP1NQLzEjwcVgFLhkknuFQ7ww= github.com/u-root/uio v0.0.0-20221213070652-c3537552635f/go.mod h1:IogEAUBXDEwX7oR/BMmCctShYs80ql4hF0ySdzGxf7E= github.com/vishvananda/netns v0.0.0-20211101163701-50045581ed74 h1:gga7acRE695APm9hlsSMoOoE65U4/TcqNj90mc69Rlg= @@ -162,6 +162,8 @@ github.com/vishvananda/netns v0.0.0-20211101163701-50045581ed74/go.mod h1:DD4vA1 github.com/xtls/go v0.0.0-20220914232946-0441cf4cf837 h1:AHhUwwFJGl27E46OpdJHplZkK09m7aETNBNzhT6t15M= github.com/xtls/go v0.0.0-20220914232946-0441cf4cf837/go.mod h1:YJTRELIWrGxR1s8xcEBgxcxBfwQfMGjdvNLTjN9XFgY= github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= +github.com/zhangyunhao116/fastrand v0.3.0 h1:7bwe124xcckPulX6fxtr2lFdO2KQqaefdtbk+mqO/Ig= +github.com/zhangyunhao116/fastrand v0.3.0/go.mod h1:0v5KgHho0VE6HU192HnY15de/oDS8UrbBChIFjIhBtc= go.etcd.io/bbolt v1.3.6 h1:/ecaJf0sk1l4l6V4awd65v2C3ILy7MSj+s/x1ADCIMU= go.etcd.io/bbolt v1.3.6/go.mod h1:qXsaaIqmgQH0T+OPdb99Bf+PKfBBQVAdyD6TY9G8XM4= go.uber.org/automaxprocs v1.5.1 h1:e1YG66Lrk73dn4qhg8WFSvhF0JuFQF0ERIp4rpuV8Qk= @@ -169,15 +171,15 @@ go.uber.org/automaxprocs v1.5.1/go.mod h1:BF4eumQw0P9GtnuxxovUd06vwm1o18oMzFtK66 golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= -golang.org/x/crypto v0.5.0 h1:U/0M97KRkSFvyD/3FSmdP5W5swImpNgle/EHFhOsQPE= -golang.org/x/crypto v0.5.0/go.mod h1:NK/OQwhpMQP3MwtdjgLlYHnH9ebylxKWv3e0fK+mkQU= -golang.org/x/exp v0.0.0-20221205204356-47842c84f3db h1:D/cFflL63o2KSLJIwjlcIt8PR064j/xsmdEJL/YvY/o= -golang.org/x/exp v0.0.0-20221205204356-47842c84f3db/go.mod h1:CxIveKay+FTh1D0yPZemJVgC/95VzuuOLq5Qi4xnoYc= +golang.org/x/crypto v0.7.0 h1:AvwMYaRytfdeVt3u6mLaxYtErKYjxA2OXjJ1HHq6t3A= +golang.org/x/crypto v0.7.0/go.mod h1:pYwdfH91IfpZVANVyUOhSIPZaFoJGxTFbZhFTx+dXZU= +golang.org/x/exp v0.0.0-20230321023759-10a507213a29 h1:ooxPy7fPvB4kwsA2h+iBNHkAbp/4JxTSwCmvdjEYmug= +golang.org/x/exp v0.0.0-20230321023759-10a507213a29/go.mod h1:CxIveKay+FTh1D0yPZemJVgC/95VzuuOLq5Qi4xnoYc= golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= -golang.org/x/mod v0.6.0 h1:b9gGHsz9/HhJ3HF5DHQytPpuwocVTChQJK3AvoLRD5I= -golang.org/x/mod v0.6.0/go.mod h1:4mET923SAdbXp2ki8ey+zGs1SLqsuM2Y0uvdZR/fUNI= +golang.org/x/mod v0.8.0 h1:LUYupSeNrTNCGzR/hVBk2NHZO4hXcVaW1k4Qx7rjPx8= +golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190419010253-1f3472d942ba/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= @@ -189,9 +191,8 @@ golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLL golang.org/x/net v0.0.0-20201010224723-4f7140c49acb/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= golang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM= -golang.org/x/net v0.0.0-20210726213435-c6fcb2dbf985/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= -golang.org/x/net v0.5.0 h1:GyT4nK/YDHSqa1c4753ouYCDajOYKTja9Xb/OHtgvSw= -golang.org/x/net v0.5.0/go.mod h1:DivGGAXEgPSlEBzxGzZI+ZLohi+xUj054jfeKui00ws= +golang.org/x/net v0.8.0 h1:Zrh2ngAOFYneWTAIAPethzeaQLuHwhuBkuV6ZiRnUaQ= +golang.org/x/net v0.8.0/go.mod h1:QVkue5JL9kW//ek3r6jTKnTFis1tRmNAW2P1shuFdJc= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.1.0 h1:wsuoTGHzEhffawBOhz5CYhcrV4IdKZbEyZjBMuTp12o= @@ -213,38 +214,34 @@ golang.org/x/sys v0.0.0-20201009025420-dfb3f7c4e634/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20201101102859-da207088b7d1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220622161953-175b2fd9d664/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220731174439-a90be440212d/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220908164124-27713097b956/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.4.0 h1:Zr2JFtRQNX3BCZ8YtxRE9hNJYC8J6I1MVbMg6owUp18= -golang.org/x/sys v0.4.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.6.0 h1:MVltZSvRTcU2ljQOhs94SXPftV6DCNnZViHeQps87pQ= +golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.6.0 h1:3XmdazWV+ubf7QgHSTWeykHOci5oeekaGJBLkrkaw4k= -golang.org/x/text v0.6.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= -golang.org/x/time v0.0.0-20191024005414-555d28b269f0 h1:/5xXl8Y5W96D+TtHSlonuFqGHIWVuyCkGJLwGh9JJFs= -golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/text v0.8.0 h1:57P1ETyNKtuIjB4SRd15iJxuhj8Gc416Y78H3qgMh68= +golang.org/x/text v0.8.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= +golang.org/x/time v0.0.0-20220210224613-90d013bbcef8 h1:vVKdlvoWBphwdxWKrFZEuM0kGgGLxUOYcY4U/2Vjg44= +golang.org/x/time v0.0.0-20220210224613-90d013bbcef8/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= golang.org/x/tools v0.1.1/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= -golang.org/x/tools v0.1.6-0.20210726203631-07bc1bf47fb2/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= -golang.org/x/tools v0.2.0 h1:G6AHpWxTMGY1KyEYoAQ5WTtIekUUvDNjan3ugu60JvE= -golang.org/x/tools v0.2.0/go.mod h1:y4OqIKeOV/fWJetJ8bXPU1sEVniLMIyDAZWeHdV+NTA= +golang.org/x/tools v0.6.0 h1:BOw41kyTf3PuCW1pVQf8+Cyg8pMlkYB1oo9iJ6D/lKM= +golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= -google.golang.org/protobuf v1.28.1 h1:d0NfwRgPtno5B1Wa6L2DAG+KivqkdutMf1UhdNx175w= -google.golang.org/protobuf v1.28.1/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= +google.golang.org/protobuf v1.28.2-0.20230118093459-a9481185b34d h1:qp0AnQCvRCMlu9jBjtdbTaaEmThIgZOrbVyDEOcmKhQ= +google.golang.org/protobuf v1.28.2-0.20230118093459-a9481185b34d/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= diff --git a/hub/executor/executor.go b/hub/executor/executor.go index b3e33f9865..1b2ec57279 100644 --- a/hub/executor/executor.go +++ b/hub/executor/executor.go @@ -75,24 +75,38 @@ func ParseWithBytes(buf []byte) (*config.Config, error) { func ApplyConfig(cfg *config.Config, force bool) { mux.Lock() defer mux.Unlock() - preUpdateExperimental(cfg) + + tunnel.OnSuspend() + + CTLS.ResetCertificate() + for _, c := range cfg.TLS.CustomTrustCert { + if err := CTLS.AddCertificate(c); err != nil { + log.Warnln("%s\nadd error: %s", c, err.Error()) + } + } + updateUsers(cfg.Users) updateProxies(cfg.Proxies, cfg.Providers) updateRules(cfg.Rules, cfg.SubRules, cfg.RuleProviders) updateSniffer(cfg.Sniffer) updateHosts(cfg.Hosts) - initInnerTcp() + updateGeneral(cfg.General) updateDNS(cfg.DNS, cfg.General.IPv6) - loadProxyProvider(cfg.Providers) - updateProfile(cfg) - loadRuleProvider(cfg.RuleProviders) - updateGeneral(cfg.General, force) - updateListeners(cfg.Listeners) + updateListeners(cfg.General, cfg.Listeners, force) updateIPTables(cfg) updateTun(cfg.General) updateExperimental(cfg) updateTunnels(cfg.Tunnels) + tunnel.OnInnerLoading() + + initInnerTcp() + loadProxyProvider(cfg.Providers) + updateProfile(cfg) + loadRuleProvider(cfg.RuleProviders) + + tunnel.OnRunning() + log.SetLevel(cfg.General.LogLevel) } @@ -128,31 +142,42 @@ func GetGeneral() *config.General { GeodataLoader: G.LoaderName(), Interface: dialer.DefaultInterface.Load(), Sniffing: tunnel.IsSniffing(), - TCPConcurrent: dialer.GetDial(), + TCPConcurrent: dialer.GetTcpConcurrent(), } return general } -func updateListeners(listeners map[string]C.InboundListener) { +func updateListeners(general *config.General, listeners map[string]C.InboundListener, force bool) { tcpIn := tunnel.TCPIn() udpIn := tunnel.UDPIn() natTable := tunnel.NatTable() listener.PatchInboundListeners(listeners, tcpIn, udpIn, natTable, true) + if !force { + return + } + + allowLan := general.AllowLan + listener.SetAllowLan(allowLan) + + bindAddress := general.BindAddress + listener.SetBindAddress(bindAddress) + listener.ReCreateHTTP(general.Port, tcpIn) + listener.ReCreateSocks(general.SocksPort, tcpIn, udpIn) + listener.ReCreateRedir(general.RedirPort, tcpIn, udpIn, natTable) + listener.ReCreateAutoRedir(general.EBpf.AutoRedir, tcpIn, udpIn) + listener.ReCreateTProxy(general.TProxyPort, tcpIn, udpIn, natTable) + listener.ReCreateMixed(general.MixedPort, tcpIn, udpIn) + listener.ReCreateShadowSocks(general.ShadowSocksConfig, tcpIn, udpIn) + listener.ReCreateVmess(general.VmessConfig, tcpIn, udpIn) + listener.ReCreateTuic(LC.TuicServer(general.TuicServer), tcpIn, udpIn) } func updateExperimental(c *config.Config) { runtime.GC() } -func preUpdateExperimental(c *config.Config) { - CTLS.AddCertificate(c.TLS.PrivateKey, c.TLS.Certificate) - for _, c := range c.TLS.CustomTrustCert { - CTLS.AddCertificate(c.PrivateKey, c.Certificate) - } -} - func updateDNS(c *config.DNS, generalIPv6 bool) { if !c.Enable { resolver.DefaultResolver = nil @@ -166,6 +191,7 @@ func updateDNS(c *config.DNS, generalIPv6 bool) { Main: c.NameServer, Fallback: c.Fallback, IPv6: c.IPv6 && generalIPv6, + IPv6Timeout: c.IPv6Timeout, EnhancedMode: c.EnhancedMode, Pool: c.FakeIPRange, Hosts: c.Hosts, @@ -201,8 +227,8 @@ func updateDNS(c *config.DNS, generalIPv6 bool) { dns.ReCreateServer(c.Listen, r, m) } -func updateHosts(tree *trie.DomainTrie[netip.Addr]) { - resolver.DefaultHosts = tree +func updateHosts(tree *trie.DomainTrie[resolver.HostValue]) { + resolver.DefaultHosts = resolver.NewHosts(tree) } func updateProxies(proxies map[string]C.Proxy, providers map[string]provider.ProxyProvider) { @@ -304,62 +330,29 @@ func updateTunnels(tunnels []LC.Tunnel) { listener.PatchTunnel(tunnels, tunnel.TCPIn(), tunnel.UDPIn()) } -func updateGeneral(general *config.General, force bool) { +func updateGeneral(general *config.General) { tunnel.SetMode(general.Mode) tunnel.SetFindProcessMode(general.FindProcessMode) - dialer.DisableIPv6 = !general.IPv6 - if !dialer.DisableIPv6 { - log.Infoln("Use IPv6") - } - resolver.DisableIPv6 = dialer.DisableIPv6 + resolver.DisableIPv6 = !general.IPv6 if general.TCPConcurrent { - dialer.SetDial(general.TCPConcurrent) + dialer.SetTcpConcurrent(general.TCPConcurrent) log.Infoln("Use tcp concurrent") } - adapter.UnifiedDelay.Store(general.UnifiedDelay) - dialer.DefaultInterface.Store(general.Interface) + inbound.SetTfo(general.InboundTfo) - if dialer.DefaultInterface.Load() != "" { - log.Infoln("Use interface name: %s", general.Interface) - } + adapter.UnifiedDelay.Store(general.UnifiedDelay) + dialer.DefaultInterface.Store(general.Interface) dialer.DefaultRoutingMark.Store(int32(general.RoutingMark)) if general.RoutingMark > 0 { log.Infoln("Use routing mark: %#x", general.RoutingMark) } iface.FlushCache() - - if !force { - return - } - geodataLoader := general.GeodataLoader G.SetLoader(geodataLoader) - - allowLan := general.AllowLan - listener.SetAllowLan(allowLan) - - bindAddress := general.BindAddress - listener.SetBindAddress(bindAddress) - - inbound.SetTfo(general.InboundTfo) - - tcpIn := tunnel.TCPIn() - udpIn := tunnel.UDPIn() - natTable := tunnel.NatTable() - - listener.ReCreateHTTP(general.Port, tcpIn) - listener.ReCreateSocks(general.SocksPort, tcpIn, udpIn) - listener.ReCreateRedir(general.RedirPort, tcpIn, udpIn, natTable) - listener.ReCreateAutoRedir(general.EBpf.AutoRedir, tcpIn, udpIn) - listener.ReCreateTProxy(general.TProxyPort, tcpIn, udpIn, natTable) - listener.ReCreateMixed(general.MixedPort, tcpIn, udpIn) - listener.ReCreateShadowSocks(general.ShadowSocksConfig, tcpIn, udpIn) - listener.ReCreateVmess(general.VmessConfig, tcpIn, udpIn) - listener.ReCreateTuic(LC.TuicServer(general.TuicServer), tcpIn, udpIn) } func updateUsers(users []auth.AuthUser) { diff --git a/hub/hub.go b/hub/hub.go index 1e925bfebb..bd228fad1e 100644 --- a/hub/hub.go +++ b/hub/hub.go @@ -4,6 +4,7 @@ import ( "github.com/Dreamacro/clash/config" "github.com/Dreamacro/clash/hub/executor" "github.com/Dreamacro/clash/hub/route" + "github.com/Dreamacro/clash/log" ) type Option func(*config.Config) @@ -43,7 +44,7 @@ func Parse(options ...Option) error { if cfg.General.ExternalController != "" { go route.Start(cfg.General.ExternalController, cfg.General.ExternalControllerTLS, - cfg.General.Secret, cfg.TLS.Certificate, cfg.TLS.PrivateKey) + cfg.General.Secret, cfg.TLS.Certificate, cfg.TLS.PrivateKey, cfg.General.LogLevel == log.DEBUG) } executor.ApplyConfig(cfg, true) diff --git a/hub/route/configs.go b/hub/route/configs.go index 9e630b29e8..afafe80e1b 100644 --- a/hub/route/configs.go +++ b/hub/route/configs.go @@ -80,6 +80,7 @@ type tunSchema struct { ExcludePackage *[]string `yaml:"exclude-package" json:"exclude-package,omitempty"` EndpointIndependentNat *bool `yaml:"endpoint-independent-nat" json:"endpoint-independent-nat,omitempty"` UDPTimeout *int64 `yaml:"udp-timeout" json:"udp-timeout,omitempty"` + FileDescriptor *int `yaml:"file-descriptor" json:"file-descriptor"` } type tuicServerSchema struct { @@ -169,6 +170,9 @@ func pointerOrDefaultTun(p *tunSchema, def LC.Tun) LC.Tun { if p.UDPTimeout != nil { def.UDPTimeout = *p.UDPTimeout } + if p.FileDescriptor != nil { + def.FileDescriptor = *p.FileDescriptor + } } return def } @@ -228,7 +232,7 @@ func patchConfigs(w http.ResponseWriter, r *http.Request) { } if general.TcpConcurrent != nil { - dialer.SetDial(*general.TcpConcurrent) + dialer.SetTcpConcurrent(*general.TcpConcurrent) } if general.InterfaceName != nil { diff --git a/hub/route/restart.go b/hub/route/restart.go new file mode 100644 index 0000000000..9539296eed --- /dev/null +++ b/hub/route/restart.go @@ -0,0 +1,62 @@ +package route + +import ( + "fmt" + "net/http" + "os" + "os/exec" + "runtime" + "syscall" + + "github.com/Dreamacro/clash/log" + + "github.com/go-chi/chi/v5" + "github.com/go-chi/render" +) + +func restartRouter() http.Handler { + r := chi.NewRouter() + r.Post("/", restart) + return r +} + +func restart(w http.ResponseWriter, r *http.Request) { + // modify from https://github.com/AdguardTeam/AdGuardHome/blob/595484e0b3fb4c457f9bb727a6b94faa78a66c5f/internal/home/controlupdate.go#L108 + execPath, err := os.Executable() + if err != nil { + render.Status(r, http.StatusInternalServerError) + render.JSON(w, r, newError(fmt.Sprintf("getting path: %s", err))) + return + } + + render.JSON(w, r, render.M{"status": "ok"}) + if f, ok := w.(http.Flusher); ok { + f.Flush() + } + + // modify from https://github.com/AdguardTeam/AdGuardHome/blob/595484e0b3fb4c457f9bb727a6b94faa78a66c5f/internal/home/controlupdate.go#L180 + // The background context is used because the underlying functions wrap it + // with timeout and shut down the server, which handles current request. It + // also should be done in a separate goroutine for the same reason. + go func() { + if runtime.GOOS == "windows" { + cmd := exec.Command(execPath, os.Args[1:]...) + log.Infoln("restarting: %q %q", execPath, os.Args[1:]) + cmd.Stdin = os.Stdin + cmd.Stdout = os.Stdout + cmd.Stderr = os.Stderr + err = cmd.Start() + if err != nil { + log.Fatalln("restarting: %s", err) + } + + os.Exit(0) + } + + log.Infoln("restarting: %q %q", execPath, os.Args[1:]) + err = syscall.Exec(execPath, os.Args, os.Environ()) + if err != nil { + log.Fatalln("restarting: %s", err) + } + }() +} diff --git a/hub/route/server.go b/hub/route/server.go index 0d6a47acd3..848face9e1 100644 --- a/hub/route/server.go +++ b/hub/route/server.go @@ -5,6 +5,7 @@ import ( "crypto/tls" "encoding/json" "net/http" + "runtime/debug" "strings" "time" @@ -13,8 +14,8 @@ import ( C "github.com/Dreamacro/clash/constant" "github.com/Dreamacro/clash/log" "github.com/Dreamacro/clash/tunnel/statistic" - "github.com/go-chi/chi/v5" + "github.com/go-chi/chi/v5/middleware" "github.com/go-chi/cors" "github.com/go-chi/render" "github.com/gorilla/websocket" @@ -43,7 +44,7 @@ func SetUIPath(path string) { } func Start(addr string, tlsAddr string, secret string, - certificat, privateKey string) { + certificat, privateKey string, isDebug bool) { if serverAddr != "" { return } @@ -59,6 +60,17 @@ func Start(addr string, tlsAddr string, secret string, MaxAge: 300, }) r.Use(corsM.Handler) + if isDebug { + r.Mount("/debug", func() http.Handler { + r := chi.NewRouter() + r.Put("/gc", func(w http.ResponseWriter, r *http.Request) { + debug.FreeOSMemory() + }) + handler := middleware.Profiler + r.Mount("/", handler()) + return r + }()) + } r.Group(func(r chi.Router) { r.Use(authentication) r.Get("/", hello) @@ -74,6 +86,9 @@ func Start(addr string, tlsAddr string, secret string, r.Mount("/providers/rules", ruleProviderRouter()) r.Mount("/cache", cacheRouter()) r.Mount("/dns", dnsRouter()) + r.Mount("/restart", restartRouter()) + r.Mount("/upgrade", upgradeRouter()) + }) if uiPath != "" { diff --git a/hub/route/upgrade.go b/hub/route/upgrade.go new file mode 100644 index 0000000000..5adf79eb33 --- /dev/null +++ b/hub/route/upgrade.go @@ -0,0 +1,28 @@ +package route + +import ( + "net/http" + + "github.com/Dreamacro/clash/hub/updater" + "github.com/Dreamacro/clash/log" + + "github.com/go-chi/chi/v5" +) + +func upgradeRouter() http.Handler { + r := chi.NewRouter() + r.Post("/", upgrade) + return r +} + +func upgrade(w http.ResponseWriter, r *http.Request) { + // modify from https://github.com/AdguardTeam/AdGuardHome/blob/595484e0b3fb4c457f9bb727a6b94faa78a66c5f/internal/home/controlupdate.go#L108 + log.Infoln("start update") + err := updater.Update() + if err != nil { + log.Errorln("err:%s", err) + return + } + + restart(w, r) +} diff --git a/hub/updater/limitedreader.go b/hub/updater/limitedreader.go new file mode 100644 index 0000000000..c31db601d0 --- /dev/null +++ b/hub/updater/limitedreader.go @@ -0,0 +1,67 @@ +package updater + +import ( + "fmt" + "io" + + "golang.org/x/exp/constraints" +) + +// LimitReachedError records the limit and the operation that caused it. +type LimitReachedError struct { + Limit int64 +} + +// Error implements the [error] interface for *LimitReachedError. +// +// TODO(a.garipov): Think about error string format. +func (lre *LimitReachedError) Error() string { + return fmt.Sprintf("attempted to read more than %d bytes", lre.Limit) +} + +// limitedReader is a wrapper for [io.Reader] limiting the input and dealing +// with errors package. +type limitedReader struct { + r io.Reader + limit int64 + n int64 +} + +// Read implements the [io.Reader] interface. +func (lr *limitedReader) Read(p []byte) (n int, err error) { + if lr.n == 0 { + return 0, &LimitReachedError{ + Limit: lr.limit, + } + } + + p = p[:Min(lr.n, int64(len(p)))] + + n, err = lr.r.Read(p) + lr.n -= int64(n) + + return n, err +} + +// LimitReader wraps Reader to make it's Reader stop with ErrLimitReached after +// n bytes read. +func LimitReader(r io.Reader, n int64) (limited io.Reader, err error) { + if n < 0 { + return nil, &updateError{Message: "limit must be non-negative"} + } + + return &limitedReader{ + r: r, + limit: n, + n: n, + }, nil +} + +// Min returns the smaller of x or y. +func Min[T constraints.Integer | ~string](x, y T) (res T) { + if x < y { + return x + } + + return y +} diff --git a/hub/updater/updater.go b/hub/updater/updater.go new file mode 100644 index 0000000000..077818fd45 --- /dev/null +++ b/hub/updater/updater.go @@ -0,0 +1,475 @@ +package updater + +import ( + "archive/zip" + "compress/gzip" + "context" + "fmt" + "io" + "net/http" + "os" + "path/filepath" + "runtime" + "strings" + "sync" + "time" + + clashHttp "github.com/Dreamacro/clash/component/http" + "github.com/Dreamacro/clash/constant" + "github.com/Dreamacro/clash/log" +) + +// modify from https://github.com/AdguardTeam/AdGuardHome/blob/595484e0b3fb4c457f9bb727a6b94faa78a66c5f/internal/updater/updater.go +// Updater is the Clash.Meta updater. +var ( + goarch string + goos string + goarm string + gomips string + + workDir string + + // mu protects all fields below. + mu sync.RWMutex + + currentExeName string // 当前可执行文件 + updateDir string // 更新目录 + packageName string // 更新压缩文件 + backupDir string // 备份目录 + backupExeName string // 备份文件名 + updateExeName string // 更新后的可执行文件 + + baseURL string = "https://github.com/MetaCubeX/Clash.Meta/releases/download/Prerelease-Alpha/clash.meta" + versionURL string = "https://github.com/MetaCubeX/Clash.Meta/releases/download/Prerelease-Alpha/version.txt" + packageURL string + latestVersion string +) + +type updateError struct { + Message string +} + +func (e *updateError) Error() string { + return fmt.Sprintf("error: %s", e.Message) +} + +// Update performs the auto-updater. It returns an error if the updater failed. +// If firstRun is true, it assumes the configuration file doesn't exist. +func Update() (err error) { + mu.Lock() + defer mu.Unlock() + + goos = runtime.GOOS + goarch = runtime.GOARCH + latestVersion, err = getLatestVersion() + if err != nil { + err := &updateError{Message: err.Error()} + return err + } + + log.Infoln("current version alpha-%s, latest version alpha-%s", constant.Version, latestVersion) + + if latestVersion == constant.Version { + err := &updateError{Message: "Already using latest version"} + return err + } + + updateDownloadURL() + + defer func() { + if err != nil { + log.Errorln("updater: failed: %v", err) + } else { + log.Infoln("updater: finished") + } + }() + + execPath, err := os.Executable() + if err != nil { + return fmt.Errorf("getting executable path: %w", err) + } + + workDir = filepath.Dir(execPath) + + err = prepare(execPath) + if err != nil { + return fmt.Errorf("preparing: %w", err) + } + + defer clean() + + err = downloadPackageFile() + if err != nil { + return fmt.Errorf("downloading package file: %w", err) + } + + err = unpack() + if err != nil { + return fmt.Errorf("unpacking: %w", err) + } + + err = backup() + if err != nil { + return fmt.Errorf("replacing: %w", err) + } + + err = replace() + if err != nil { + return fmt.Errorf("replacing: %w", err) + } + + return nil +} + +// prepare fills all necessary fields in Updater object. +func prepare(exePath string) (err error) { + updateDir = filepath.Join(workDir, "meta-update") + currentExeName = exePath + _, pkgNameOnly := filepath.Split(packageURL) + if pkgNameOnly == "" { + return fmt.Errorf("invalid PackageURL: %q", packageURL) + } + + packageName = filepath.Join(updateDir, pkgNameOnly) + //log.Infoln(packageName) + backupDir = filepath.Join(workDir, "meta-backup") + + if goos == "windows" { + updateExeName = "clash.meta" + "-" + goos + "-" + goarch + ".exe" + } else { + updateExeName = "clash.meta" + "-" + goos + "-" + goarch + } + + log.Infoln("updateExeName: %s ", updateExeName) + + backupExeName = filepath.Join(backupDir, filepath.Base(exePath)) + updateExeName = filepath.Join(updateDir, updateExeName) + + log.Infoln( + "updater: updating using url: %s", + packageURL, + ) + + currentExeName = exePath + _, err = os.Stat(currentExeName) + if err != nil { + return fmt.Errorf("checking %q: %w", currentExeName, err) + } + + return nil +} + +// unpack extracts the files from the downloaded archive. +func unpack() error { + var err error + _, pkgNameOnly := filepath.Split(packageURL) + + log.Debugln("updater: unpacking package") + if strings.HasSuffix(pkgNameOnly, ".zip") { + _, err = zipFileUnpack(packageName, updateDir) + if err != nil { + return fmt.Errorf(".zip unpack failed: %w", err) + } + + } else if strings.HasSuffix(pkgNameOnly, ".gz") { + _, err = gzFileUnpack(packageName, updateDir) + if err != nil { + return fmt.Errorf(".gz unpack failed: %w", err) + } + + } else { + return fmt.Errorf("unknown package extension") + } + + return nil +} + +// backup makes a backup of the current configuration and supporting files. It +// ignores the configuration file if firstRun is true. +func backup() (err error) { + log.Infoln("updater: backing up current Exefile") + _ = os.Mkdir(backupDir, 0o755) + + err = copyFile(currentExeName, backupExeName) + if err != nil { + return fmt.Errorf("copySupportingFiles(%s, %s) failed: %w", currentExeName, backupExeName, err) + } + + return nil +} + +// replace moves the current executable with the updated one and also copies the +// supporting files. +func replace() error { + var err error + + // log.Infoln("updater: renaming: %s to %s", currentExeName, backupExeName) + // err := os.Rename(currentExeName, backupExeName) + // if err != nil { + // return err + // } + + if goos == "windows" { + // rename fails with "File in use" error + log.Infoln("copying: %s to %s", updateExeName, currentExeName) + err = copyFile(updateExeName, currentExeName) + } else { + log.Infoln("copying: %s to %s", updateExeName, currentExeName) + err = os.Rename(updateExeName, currentExeName) + } + if err != nil { + return err + } + + return nil +} + +// clean removes the temporary directory itself and all it's contents. +func clean() { + _ = os.RemoveAll(updateDir) +} + +// MaxPackageFileSize is a maximum package file length in bytes. The largest +// package whose size is limited by this constant currently has the size of +// approximately 9 MiB. +const MaxPackageFileSize = 32 * 1024 * 1024 + +// Download package file and save it to disk +func downloadPackageFile() (err error) { + // var resp *http.Response + // resp, err = client.Get(packageURL) + ctx, cancel := context.WithTimeout(context.Background(), time.Second*90) + defer cancel() + resp, err := clashHttp.HttpRequest(ctx, packageURL, http.MethodGet, http.Header{"User-Agent": {"clash"}}, nil) + if err != nil { + return fmt.Errorf("http request failed: %w", err) + } + + defer func() { + closeErr := resp.Body.Close() + if closeErr != nil && err == nil { + err = closeErr + } + }() + + var r io.Reader + r, err = LimitReader(resp.Body, MaxPackageFileSize) + if err != nil { + return fmt.Errorf("http request failed: %w", err) + } + + log.Debugln("updater: reading http body") + // This use of ReadAll is now safe, because we limited body's Reader. + body, err := io.ReadAll(r) + if err != nil { + return fmt.Errorf("io.ReadAll() failed: %w", err) + } + + log.Debugln("updateDir %s", updateDir) + err = os.Mkdir(updateDir, 0o755) + if err != nil { + return fmt.Errorf("mkdir error: %w", err) + } + + log.Debugln("updater: saving package to file %s", packageName) + err = os.WriteFile(packageName, body, 0o644) + if err != nil { + return fmt.Errorf("os.WriteFile() failed: %w", err) + } + return nil +} + +// Unpack a single .gz file to the specified directory +// Existing files are overwritten +// All files are created inside outDir, subdirectories are not created +// Return the output file name +func gzFileUnpack(gzfile, outDir string) (string, error) { + f, err := os.Open(gzfile) + if err != nil { + return "", fmt.Errorf("os.Open(): %w", err) + } + + defer func() { + closeErr := f.Close() + if closeErr != nil && err == nil { + err = closeErr + } + }() + + gzReader, err := gzip.NewReader(f) + if err != nil { + return "", fmt.Errorf("gzip.NewReader(): %w", err) + } + + defer func() { + closeErr := gzReader.Close() + if closeErr != nil && err == nil { + err = closeErr + } + }() + // Get the original file name from the .gz file header + originalName := gzReader.Header.Name + if originalName == "" { + // Fallback: remove the .gz extension from the input file name if the header doesn't provide the original name + originalName = filepath.Base(gzfile) + originalName = strings.TrimSuffix(originalName, ".gz") + } + + outputName := filepath.Join(outDir, originalName) + + // Create the output file + wc, err := os.OpenFile( + outputName, + os.O_WRONLY|os.O_CREATE|os.O_TRUNC, + 0o755, + ) + if err != nil { + return "", fmt.Errorf("os.OpenFile(%s): %w", outputName, err) + } + + defer func() { + closeErr := wc.Close() + if closeErr != nil && err == nil { + err = closeErr + } + }() + + // Copy the contents of the gzReader to the output file + _, err = io.Copy(wc, gzReader) + if err != nil { + return "", fmt.Errorf("io.Copy(): %w", err) + } + + return outputName, nil +} + +// Unpack a single file from .zip file to the specified directory +// Existing files are overwritten +// All files are created inside 'outDir', subdirectories are not created +// Return the output file name +func zipFileUnpack(zipfile, outDir string) (string, error) { + zrc, err := zip.OpenReader(zipfile) + if err != nil { + return "", fmt.Errorf("zip.OpenReader(): %w", err) + } + + defer func() { + closeErr := zrc.Close() + if closeErr != nil && err == nil { + err = closeErr + } + }() + if len(zrc.File) == 0 { + return "", fmt.Errorf("no files in the zip archive") + } + + // Assuming the first file in the zip archive is the target file + zf := zrc.File[0] + var rc io.ReadCloser + rc, err = zf.Open() + if err != nil { + return "", fmt.Errorf("zip file Open(): %w", err) + } + + defer func() { + closeErr := rc.Close() + if closeErr != nil && err == nil { + err = closeErr + } + }() + fi := zf.FileInfo() + name := fi.Name() + outputName := filepath.Join(outDir, name) + + if fi.IsDir() { + return "", fmt.Errorf("the target file is a directory") + } + + var wc io.WriteCloser + wc, err = os.OpenFile(outputName, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, fi.Mode()) + if err != nil { + return "", fmt.Errorf("os.OpenFile(): %w", err) + } + + defer func() { + closeErr := wc.Close() + if closeErr != nil && err == nil { + err = closeErr + } + }() + _, err = io.Copy(wc, rc) + if err != nil { + return "", fmt.Errorf("io.Copy(): %w", err) + } + + return outputName, nil +} + +// Copy file on disk +func copyFile(src, dst string) error { + d, e := os.ReadFile(src) + if e != nil { + return e + } + e = os.WriteFile(dst, d, 0o644) + if e != nil { + return e + } + return nil +} + +func getLatestVersion() (version string, err error) { + ctx, cancel := context.WithTimeout(context.Background(), time.Second*5) + defer cancel() + resp, err := clashHttp.HttpRequest(ctx, versionURL, http.MethodGet, http.Header{"User-Agent": {"clash"}}, nil) + if err != nil { + return "", fmt.Errorf("get Latest Version fail: %w", err) + } + defer func() { + closeErr := resp.Body.Close() + if closeErr != nil && err == nil { + err = closeErr + } + }() + + body, err := io.ReadAll(resp.Body) + if err != nil { + return "", fmt.Errorf("get Latest Version fail: %w", err) + } + content := strings.TrimRight(string(body), "\n") + return content, nil +} + +func updateDownloadURL() { + var middle string + + if goarch == "arm" && goarm != "" { + middle = fmt.Sprintf("-%s-%sv%s-%s", goos, goarch, goarm, latestVersion) + } else if isMIPS(goarch) && gomips != "" { + middle = fmt.Sprintf("-%s-%s-%s-%s", goos, goarch, gomips, latestVersion) + } else { + middle = fmt.Sprintf("-%s-%s-%s", goos, goarch, latestVersion) + } + + if goos == "windows" { + middle += ".zip" + } else { + middle += ".gz" + } + packageURL = baseURL + middle + //log.Infoln(packageURL) +} + +// isMIPS returns true if arch is any MIPS architecture. +func isMIPS(arch string) (ok bool) { + switch arch { + case + "mips", + "mips64", + "mips64le", + "mipsle": + return true + default: + return false + } +} diff --git a/listener/config/shadowsocks.go b/listener/config/shadowsocks.go index cfe31f622f..60540bbd28 100644 --- a/listener/config/shadowsocks.go +++ b/listener/config/shadowsocks.go @@ -9,6 +9,7 @@ type ShadowsocksServer struct { Listen string Password string Cipher string + Udp bool } func (t ShadowsocksServer) String() string { diff --git a/listener/config/tuic.go b/listener/config/tuic.go index c584bbf5ea..991a04c969 100644 --- a/listener/config/tuic.go +++ b/listener/config/tuic.go @@ -15,6 +15,7 @@ type TuicServer struct { AuthenticationTimeout int `yaml:"authentication-timeout" json:"authentication-timeout,omitempty"` ALPN []string `yaml:"alpn" json:"alpn,omitempty"` MaxUdpRelayPacketSize int `yaml:"max-udp-relay-packet-size" json:"max-udp-relay-packet-size,omitempty"` + MaxDatagramFrameSize int `yaml:"max-datagram-frame-size" json:"max-datagram-frame-size,omitempty"` } func (t TuicServer) String() string { diff --git a/listener/config/tun.go b/listener/config/tun.go index 2e1d1a7175..50f5cf7d91 100644 --- a/listener/config/tun.go +++ b/listener/config/tun.go @@ -95,4 +95,5 @@ type Tun struct { ExcludePackage []string `yaml:"exclude-package" json:"exclude-package,omitempty"` EndpointIndependentNat bool `yaml:"endpoint-independent-nat" json:"endpoint-independent-nat,omitempty"` UDPTimeout int64 `yaml:"udp-timeout" json:"udp-timeout,omitempty"` + FileDescriptor int `yaml:"file-descriptor" json:"file-descriptor"` } diff --git a/listener/inbound/shadowsocks.go b/listener/inbound/shadowsocks.go index 4090748515..4659f4d78c 100644 --- a/listener/inbound/shadowsocks.go +++ b/listener/inbound/shadowsocks.go @@ -11,6 +11,7 @@ type ShadowSocksOption struct { BaseOption Password string `inbound:"password"` Cipher string `inbound:"cipher"` + UDP bool `inbound:"udp,omitempty"` } func (o ShadowSocksOption) Equal(config C.InboundConfig) bool { @@ -37,6 +38,7 @@ func NewShadowSocks(options *ShadowSocksOption) (*ShadowSocks, error) { Listen: base.RawAddress(), Password: options.Password, Cipher: options.Cipher, + Udp: options.UDP, }, }, nil } diff --git a/listener/inbound/tproxy.go b/listener/inbound/tproxy.go index fa458d2c8f..00cd08499b 100644 --- a/listener/inbound/tproxy.go +++ b/listener/inbound/tproxy.go @@ -56,13 +56,10 @@ func (t *TProxy) Listen(tcpIn chan<- C.ConnContext, udpIn chan<- C.PacketAdapter return err } if t.udp { - if t.lUDP != nil { - t.lUDP, err = tproxy.NewUDP(t.RawAddress(), udpIn, natTable, t.Additions()...) - if err != nil { - return err - } + t.lUDP, err = tproxy.NewUDP(t.RawAddress(), udpIn, natTable, t.Additions()...) + if err != nil { + return err } - } log.Infoln("TProxy[%s] proxy listening at: %s", t.Name(), t.Address()) return nil diff --git a/listener/inbound/tun.go b/listener/inbound/tun.go index ad215989e5..eb16d2ddfa 100644 --- a/listener/inbound/tun.go +++ b/listener/inbound/tun.go @@ -33,6 +33,7 @@ type TunOption struct { ExcludePackage []string `inbound:"exclude_package,omitempty"` EndpointIndependentNat bool `inbound:"endpoint_independent_nat,omitempty"` UDPTimeout int64 `inbound:"udp_timeout,omitempty"` + FileDescriptor int `inbound:"file-descriptor,omitempty"` } func (o TunOption) Equal(config C.InboundConfig) bool { @@ -96,6 +97,7 @@ func NewTun(options *TunOption) (*Tun, error) { ExcludePackage: options.ExcludePackage, EndpointIndependentNat: options.EndpointIndependentNat, UDPTimeout: options.UDPTimeout, + FileDescriptor: options.FileDescriptor, }, }, nil } diff --git a/listener/listener.go b/listener/listener.go index d8eb5c0c2b..8f0088db95 100644 --- a/listener/listener.go +++ b/listener/listener.go @@ -271,6 +271,7 @@ func ReCreateShadowSocks(shadowSocksConfig string, tcpIn chan<- C.ConnContext, u Listen: addr, Password: password, Cipher: cipher, + Udp: true, } } @@ -821,7 +822,8 @@ func hasTunConfigChange(tunConf *LC.Tun) bool { LastTunConf.MTU != tunConf.MTU || LastTunConf.StrictRoute != tunConf.StrictRoute || LastTunConf.EndpointIndependentNat != tunConf.EndpointIndependentNat || - LastTunConf.UDPTimeout != tunConf.UDPTimeout { + LastTunConf.UDPTimeout != tunConf.UDPTimeout || + LastTunConf.FileDescriptor != tunConf.FileDescriptor { return true } diff --git a/listener/parse.go b/listener/parse.go index aa9e39ac74..c8e1ddf714 100644 --- a/listener/parse.go +++ b/listener/parse.go @@ -73,7 +73,7 @@ func ParseListener(mapping map[string]any) (C.InboundListener, error) { } listener, err = IN.NewTun(tunOption) case "shadowsocks": - shadowsocksOption := &IN.ShadowSocksOption{} + shadowsocksOption := &IN.ShadowSocksOption{UDP: true} err = decoder.Decode(mapping, shadowsocksOption) if err != nil { return nil, err diff --git a/listener/shadowsocks/tcp.go b/listener/shadowsocks/tcp.go index 21db5b6330..c0fd490f68 100644 --- a/listener/shadowsocks/tcp.go +++ b/listener/shadowsocks/tcp.go @@ -33,12 +33,14 @@ func New(config LC.ShadowsocksServer, tcpIn chan<- C.ConnContext, udpIn chan<- C for _, addr := range strings.Split(config.Listen, ",") { addr := addr - //UDP - ul, err := NewUDP(addr, pickCipher, udpIn) - if err != nil { - return nil, err + if config.Udp { + //UDP + ul, err := NewUDP(addr, pickCipher, udpIn) + if err != nil { + return nil, err + } + sl.udpListeners = append(sl.udpListeners, ul) } - sl.udpListeners = append(sl.udpListeners, ul) //TCP l, err := inbound.Listen("tcp", addr) diff --git a/listener/sing/log.go b/listener/sing/log.go deleted file mode 100644 index 4847e0638c..0000000000 --- a/listener/sing/log.go +++ /dev/null @@ -1,41 +0,0 @@ -package sing - -import ( - "fmt" - - "github.com/Dreamacro/clash/log" - - L "github.com/sagernet/sing/common/logger" -) - -type logger struct{} - -func (l logger) Trace(args ...any) { - log.Debugln(fmt.Sprint(args...)) -} - -func (l logger) Debug(args ...any) { - log.Debugln(fmt.Sprint(args...)) -} - -func (l logger) Info(args ...any) { - log.Infoln(fmt.Sprint(args...)) -} - -func (l logger) Warn(args ...any) { - log.Warnln(fmt.Sprint(args...)) -} - -func (l logger) Error(args ...any) { - log.Errorln(fmt.Sprint(args...)) -} - -func (l logger) Fatal(args ...any) { - log.Fatalln(fmt.Sprint(args...)) -} - -func (l logger) Panic(args ...any) { - log.Fatalln(fmt.Sprint(args...)) -} - -var Logger L.Logger = logger{} diff --git a/listener/sing/sing.go b/listener/sing/sing.go index a3e15154c9..7046272804 100644 --- a/listener/sing/sing.go +++ b/listener/sing/sing.go @@ -5,6 +5,7 @@ import ( "errors" "golang.org/x/exp/slices" "net" + "net/netip" "sync" "time" @@ -24,10 +25,11 @@ import ( const UDPTimeout = 5 * time.Minute type ListenerHandler struct { - TcpIn chan<- C.ConnContext - UdpIn chan<- C.PacketAdapter - Type C.Type - Additions []inbound.Addition + TcpIn chan<- C.ConnContext + UdpIn chan<- C.PacketAdapter + Type C.Type + Additions []inbound.Addition + UDPTimeout time.Duration } type waitCloseConn struct { @@ -61,9 +63,16 @@ func (h *ListenerHandler) NewConnection(ctx context.Context, conn net.Conn, meta switch metadata.Destination.Fqdn { case vmess.MuxDestination.Fqdn: return vmess.HandleMuxConnection(ctx, conn, h) - case uot.UOTMagicAddress: - metadata.Destination = M.Socksaddr{} - return h.NewPacketConnection(ctx, uot.NewClientConn(conn), metadata) + case uot.MagicAddress: + request, err := uot.ReadRequest(conn) + if err != nil { + return E.Cause(err, "read UoT request") + } + metadata.Destination = request.Destination + return h.NewPacketConnection(ctx, uot.NewConn(conn, *request), metadata) + case uot.LegacyMagicAddress: + metadata.Destination = M.Socksaddr{Addr: netip.IPv4Unspecified()} + return h.NewPacketConnection(ctx, uot.NewConn(conn, uot.Request{}), metadata) } target := socks5.ParseAddr(metadata.Destination.String()) wg := &sync.WaitGroup{} @@ -151,6 +160,9 @@ func (c *packet) WriteBack(b []byte, addr net.Addr) (n int, err error) { return } err = conn.WritePacket(buff, M.SocksaddrFromNet(addr)) + if err != nil { + return + } return } diff --git a/listener/sing_shadowsocks/server.go b/listener/sing_shadowsocks/server.go index 305a9496cf..c7e05bb599 100644 --- a/listener/sing_shadowsocks/server.go +++ b/listener/sing_shadowsocks/server.go @@ -5,6 +5,7 @@ import ( "fmt" "net" "strings" + "time" "github.com/Dreamacro/clash/adapter/inbound" "github.com/Dreamacro/clash/common/sockopt" @@ -63,7 +64,7 @@ func New(config LC.ShadowsocksServer, tcpIn chan<- C.ConnContext, udpIn chan<- C case common.Contains(shadowaead.List, config.Cipher): sl.service, err = shadowaead.NewService(config.Cipher, nil, config.Password, udpTimeout, h) case common.Contains(shadowaead_2022.List, config.Cipher): - sl.service, err = shadowaead_2022.NewServiceWithPassword(config.Cipher, config.Password, udpTimeout, h) + sl.service, err = shadowaead_2022.NewServiceWithPassword(config.Cipher, config.Password, udpTimeout, h, time.Now) default: err = fmt.Errorf("shadowsocks: unsupported method: %s", config.Cipher) return embedSS.New(config, tcpIn, udpIn) @@ -75,37 +76,39 @@ func New(config LC.ShadowsocksServer, tcpIn chan<- C.ConnContext, udpIn chan<- C for _, addr := range strings.Split(config.Listen, ",") { addr := addr - //UDP - ul, err := net.ListenPacket("udp", addr) - if err != nil { - return nil, err - } - - err = sockopt.UDPReuseaddr(ul.(*net.UDPConn)) - if err != nil { - log.Warnln("Failed to Reuse UDP Address: %s", err) - } + if config.Udp { + //UDP + ul, err := net.ListenPacket("udp", addr) + if err != nil { + return nil, err + } - sl.udpListeners = append(sl.udpListeners, ul) + err = sockopt.UDPReuseaddr(ul.(*net.UDPConn)) + if err != nil { + log.Warnln("Failed to Reuse UDP Address: %s", err) + } - go func() { - conn := bufio.NewPacketConn(ul) - for { - buff := buf.NewPacket() - remoteAddr, err := conn.ReadPacket(buff) - if err != nil { - buff.Release() - if sl.closed { - break + sl.udpListeners = append(sl.udpListeners, ul) + + go func() { + conn := bufio.NewPacketConn(ul) + for { + buff := buf.NewPacket() + remoteAddr, err := conn.ReadPacket(buff) + if err != nil { + buff.Release() + if sl.closed { + break + } + continue } - continue + _ = sl.service.NewPacket(context.TODO(), conn, buff, metadata.Metadata{ + Protocol: "shadowsocks", + Source: remoteAddr, + }) } - _ = sl.service.NewPacket(context.TODO(), conn, buff, metadata.Metadata{ - Protocol: "shadowsocks", - Source: remoteAddr, - }) - } - }() + }() + } //TCP l, err := inbound.Listen("tcp", addr) diff --git a/listener/sing_tun/server.go b/listener/sing_tun/server.go index 5c387a8d30..ada9d1c217 100644 --- a/listener/sing_tun/server.go +++ b/listener/sing_tun/server.go @@ -8,6 +8,7 @@ import ( "runtime" "strconv" "strings" + "time" "github.com/Dreamacro/clash/adapter/inbound" "github.com/Dreamacro/clash/component/dialer" @@ -67,6 +68,26 @@ func CalculateInterfaceName(name string) (tunName string) { return } +func checkTunName(tunName string) (ok bool) { + defer func() { + if !ok { + log.Warnln("[TUN] Unsupported tunName(%s) in %s, force regenerate by ourselves.", tunName, runtime.GOOS) + } + }() + if runtime.GOOS == "darwin" { + if len(tunName) <= 4 { + return false + } + if tunName[:4] != "utun" { + return false + } + if _, parseErr := strconv.ParseInt(tunName[4:], 10, 16); parseErr != nil { + return false + } + } + return true +} + func New(options LC.Tun, tcpIn chan<- C.ConnContext, udpIn chan<- C.PacketAdapter, additions ...inbound.Addition) (l *Listener, err error) { if len(additions) == 0 { additions = []inbound.Addition{ @@ -75,7 +96,7 @@ func New(options LC.Tun, tcpIn chan<- C.ConnContext, udpIn chan<- C.PacketAdapte } } tunName := options.Device - if tunName == "" { + if tunName == "" || !checkTunName(tunName) { tunName = CalculateInterfaceName(InterfaceName) options.Device = tunName } @@ -131,10 +152,11 @@ func New(options LC.Tun, tcpIn chan<- C.ConnContext, udpIn chan<- C.PacketAdapte handler := &ListenerHandler{ ListenerHandler: sing.ListenerHandler{ - TcpIn: tcpIn, - UdpIn: udpIn, - Type: C.TUN, - Additions: additions, + TcpIn: tcpIn, + UdpIn: udpIn, + Type: C.TUN, + Additions: additions, + UDPTimeout: time.Second * time.Duration(udpTimeout), }, DnsAdds: dnsAdds, } @@ -192,6 +214,7 @@ func New(options LC.Tun, tcpIn chan<- C.ConnContext, udpIn chan<- C.PacketAdapte IncludeAndroidUser: options.IncludeAndroidUser, IncludePackage: options.IncludePackage, ExcludePackage: options.ExcludePackage, + FileDescriptor: options.FileDescriptor, InterfaceMonitor: defaultInterfaceMonitor, TableIndex: 2022, } @@ -201,7 +224,7 @@ func New(options LC.Tun, tcpIn chan<- C.ConnContext, udpIn chan<- C.PacketAdapte err = E.Cause(err, "build android rules") return } - tunIf, err := tunOpen(tunOptions) + tunIf, err := tunNew(tunOptions) if err != nil { err = E.Cause(err, "configure tun interface") return @@ -217,7 +240,7 @@ func New(options LC.Tun, tcpIn chan<- C.ConnContext, udpIn chan<- C.PacketAdapte EndpointIndependentNat: options.EndpointIndependentNat, UDPTimeout: udpTimeout, Handler: handler, - Logger: sing.Logger, + Logger: log.SingLogger, }) if err != nil { return diff --git a/listener/sing_tun/server_notwindows.go b/listener/sing_tun/server_notwindows.go index d3280c5c72..eda79dc093 100644 --- a/listener/sing_tun/server_notwindows.go +++ b/listener/sing_tun/server_notwindows.go @@ -6,6 +6,6 @@ import ( tun "github.com/metacubex/sing-tun" ) -func tunOpen(options tun.Options) (tun.Tun, error) { - return tun.Open(options) +func tunNew(options tun.Options) (tun.Tun, error) { + return tun.New(options) } diff --git a/listener/sing_tun/server_windows.go b/listener/sing_tun/server_windows.go index 7b745cacb6..9584f32fac 100644 --- a/listener/sing_tun/server_windows.go +++ b/listener/sing_tun/server_windows.go @@ -8,11 +8,11 @@ import ( tun "github.com/metacubex/sing-tun" ) -func tunOpen(options tun.Options) (tunIf tun.Tun, err error) { +func tunNew(options tun.Options) (tunIf tun.Tun, err error) { maxRetry := 3 for i := 0; i < maxRetry; i++ { timeBegin := time.Now() - tunIf, err = tun.Open(options) + tunIf, err = tun.New(options) if err == nil { return } diff --git a/listener/tuic/server.go b/listener/tuic/server.go index a7ad69f6e5..92cc0b3798 100644 --- a/listener/tuic/server.go +++ b/listener/tuic/server.go @@ -61,6 +61,16 @@ func New(config LC.TuicServer, tcpIn chan<- C.ConnContext, udpIn chan<- C.Packet quicConfig.InitialConnectionReceiveWindow = tuic.DefaultConnectionReceiveWindow / 10 quicConfig.MaxConnectionReceiveWindow = tuic.DefaultConnectionReceiveWindow + if config.MaxUdpRelayPacketSize == 0 { + config.MaxUdpRelayPacketSize = 1500 + } + maxDatagramFrameSize := config.MaxUdpRelayPacketSize + tuic.PacketOverHead + if maxDatagramFrameSize > 1400 { + maxDatagramFrameSize = 1400 + } + config.MaxUdpRelayPacketSize = maxDatagramFrameSize - tuic.PacketOverHead + quicConfig.MaxDatagramFrameSize = int64(maxDatagramFrameSize) + tokens := make([][32]byte, len(config.Token)) for i, token := range config.Token { tokens[i] = tuic.GenTKN(token) diff --git a/listener/tunnel/tcp.go b/listener/tunnel/tcp.go index bf278c1c51..c1d896adfe 100644 --- a/listener/tunnel/tcp.go +++ b/listener/tunnel/tcp.go @@ -41,7 +41,7 @@ func (l *Listener) handleTCP(conn net.Conn, in chan<- C.ConnContext, additions . } func New(addr, target, proxy string, in chan<- C.ConnContext, additions ...inbound.Addition) (*Listener, error) { - l, err := net.Listen("tcp", addr) + l, err := inbound.Listen("tcp", addr) if err != nil { return nil, err } diff --git a/log/sing.go b/log/sing.go new file mode 100644 index 0000000000..818acc79d4 --- /dev/null +++ b/log/sing.go @@ -0,0 +1,68 @@ +package log + +import ( + "context" + "fmt" + + L "github.com/sagernet/sing/common/logger" +) + +type singLogger struct{} + +func (l singLogger) TraceContext(ctx context.Context, args ...any) { + Debugln(fmt.Sprint(args...)) +} + +func (l singLogger) DebugContext(ctx context.Context, args ...any) { + Debugln(fmt.Sprint(args...)) +} + +func (l singLogger) InfoContext(ctx context.Context, args ...any) { + Infoln(fmt.Sprint(args...)) +} + +func (l singLogger) WarnContext(ctx context.Context, args ...any) { + Warnln(fmt.Sprint(args...)) +} + +func (l singLogger) ErrorContext(ctx context.Context, args ...any) { + Errorln(fmt.Sprint(args...)) +} + +func (l singLogger) FatalContext(ctx context.Context, args ...any) { + Fatalln(fmt.Sprint(args...)) +} + +func (l singLogger) PanicContext(ctx context.Context, args ...any) { + Fatalln(fmt.Sprint(args...)) +} + +func (l singLogger) Trace(args ...any) { + Debugln(fmt.Sprint(args...)) +} + +func (l singLogger) Debug(args ...any) { + Debugln(fmt.Sprint(args...)) +} + +func (l singLogger) Info(args ...any) { + Infoln(fmt.Sprint(args...)) +} + +func (l singLogger) Warn(args ...any) { + Warnln(fmt.Sprint(args...)) +} + +func (l singLogger) Error(args ...any) { + Errorln(fmt.Sprint(args...)) +} + +func (l singLogger) Fatal(args ...any) { + Fatalln(fmt.Sprint(args...)) +} + +func (l singLogger) Panic(args ...any) { + Fatalln(fmt.Sprint(args...)) +} + +var SingLogger L.ContextLogger = singLogger{} diff --git a/transport/gun/gun.go b/transport/gun/gun.go index 920e7adc3a..ae2ea6a46e 100644 --- a/transport/gun/gun.go +++ b/transport/gun/gun.go @@ -20,6 +20,7 @@ import ( "github.com/Dreamacro/clash/common/buf" "github.com/Dreamacro/clash/common/pool" tlsC "github.com/Dreamacro/clash/component/tls" + "go.uber.org/atomic" "golang.org/x/net/http2" ) @@ -189,7 +190,7 @@ func (g *Conn) SetDeadline(t time.Time) error { return nil } -func NewHTTP2Client(dialFn DialFn, tlsConfig *tls.Config, Fingerprint string) *TransportWrap { +func NewHTTP2Client(dialFn DialFn, tlsConfig *tls.Config, Fingerprint string, realityConfig *tlsC.RealityConfig) *TransportWrap { wrap := TransportWrap{} dialFunc := func(ctx context.Context, network, addr string, cfg *tls.Config) (net.Conn, error) { @@ -201,20 +202,37 @@ func NewHTTP2Client(dialFn DialFn, tlsConfig *tls.Config, Fingerprint string) *T wrap.remoteAddr = pconn.RemoteAddr() if len(Fingerprint) != 0 { - if fingerprint, exists := tlsC.GetFingerprint(Fingerprint); exists { - utlsConn := tlsC.UClient(pconn, cfg, fingerprint) - if err := utlsConn.(*tlsC.UConn).HandshakeContext(ctx); err != nil { + if realityConfig == nil { + if fingerprint, exists := tlsC.GetFingerprint(Fingerprint); exists { + utlsConn := tlsC.UClient(pconn, cfg, fingerprint) + if err := utlsConn.(*tlsC.UConn).HandshakeContext(ctx); err != nil { + pconn.Close() + return nil, err + } + state := utlsConn.(*tlsC.UConn).ConnectionState() + if p := state.NegotiatedProtocol; p != http2.NextProtoTLS { + utlsConn.Close() + return nil, fmt.Errorf("http2: unexpected ALPN protocol %s, want %s", p, http2.NextProtoTLS) + } + return utlsConn, nil + } + } else { + realityConn, err := tlsC.GetRealityConn(ctx, pconn, Fingerprint, cfg, realityConfig) + if err != nil { pconn.Close() return nil, err } - state := utlsConn.(*tlsC.UConn).ConnectionState() - if p := state.NegotiatedProtocol; p != http2.NextProtoTLS { - utlsConn.Close() - return nil, fmt.Errorf("http2: unexpected ALPN protocol %s, want %s", p, http2.NextProtoTLS) - } - return utlsConn, nil + //state := realityConn.(*utls.UConn).ConnectionState() + //if p := state.NegotiatedProtocol; p != http2.NextProtoTLS { + // realityConn.Close() + // return nil, fmt.Errorf("http2: unexpected ALPN protocol %s, want %s", p, http2.NextProtoTLS) + //} + return realityConn, nil } } + if realityConfig != nil { + return nil, errors.New("REALITY is based on uTLS, please set a client-fingerprint") + } conn := tls.Client(pconn, cfg) if err := conn.HandshakeContext(ctx); err != nil { @@ -274,11 +292,11 @@ func StreamGunWithTransport(transport *TransportWrap, cfg *Config) (net.Conn, er return conn, nil } -func StreamGunWithConn(conn net.Conn, tlsConfig *tls.Config, cfg *Config) (net.Conn, error) { +func StreamGunWithConn(conn net.Conn, tlsConfig *tls.Config, cfg *Config, realityConfig *tlsC.RealityConfig) (net.Conn, error) { dialFn := func(network, addr string) (net.Conn, error) { return conn, nil } - transport := NewHTTP2Client(dialFn, tlsConfig, cfg.ClientFingerprint) + transport := NewHTTP2Client(dialFn, tlsConfig, cfg.ClientFingerprint, realityConfig) return StreamGunWithTransport(transport, cfg) } diff --git a/transport/hysteria/conns/udp/hop.go b/transport/hysteria/conns/udp/hop.go index 53830ae4db..447a759205 100644 --- a/transport/hysteria/conns/udp/hop.go +++ b/transport/hysteria/conns/udp/hop.go @@ -2,7 +2,6 @@ package udp import ( "errors" - "math/rand" "net" "strconv" "strings" @@ -12,6 +11,8 @@ import ( "github.com/Dreamacro/clash/transport/hysteria/obfs" "github.com/Dreamacro/clash/transport/hysteria/utils" + + "github.com/zhangyunhao116/fastrand" ) const ( @@ -85,7 +86,7 @@ func NewObfsUDPHopClientPacketConn(server string, serverPorts string, hopInterva serverAddrs: serverAddrs, hopInterval: hopInterval, obfs: obfs, - addrIndex: rand.Intn(len(serverAddrs)), + addrIndex: fastrand.Intn(len(serverAddrs)), recvQueue: make(chan *udpPacket, packetQueueSize), closeChan: make(chan struct{}), bufPool: sync.Pool{ @@ -176,7 +177,7 @@ func (c *ObfsUDPHopClientPacketConn) hop(dialer utils.PacketDialer, rAddr net.Ad _ = trySetPacketConnWriteBuffer(c.currentConn, c.writeBufferSize) } go c.recvRoutine(c.currentConn) - c.addrIndex = rand.Intn(len(c.serverAddrs)) + c.addrIndex = fastrand.Intn(len(c.serverAddrs)) } func (c *ObfsUDPHopClientPacketConn) ReadFrom(b []byte) (int, net.Addr, error) { diff --git a/transport/hysteria/conns/wechat/obfs.go b/transport/hysteria/conns/wechat/obfs.go index 815aa52fee..d13cca55a3 100644 --- a/transport/hysteria/conns/wechat/obfs.go +++ b/transport/hysteria/conns/wechat/obfs.go @@ -2,12 +2,14 @@ package wechat import ( "encoding/binary" - "github.com/Dreamacro/clash/log" - "github.com/Dreamacro/clash/transport/hysteria/obfs" - "math/rand" "net" "sync" "time" + + "github.com/Dreamacro/clash/log" + "github.com/Dreamacro/clash/transport/hysteria/obfs" + + "github.com/zhangyunhao116/fastrand" ) const udpBufferSize = 65535 @@ -29,7 +31,7 @@ func NewObfsWeChatUDPConn(orig net.PacketConn, obfs obfs.Obfuscator) *ObfsWeChat obfs: obfs, readBuf: make([]byte, udpBufferSize), writeBuf: make([]byte, udpBufferSize), - sn: rand.Uint32() & 0xFFFF, + sn: fastrand.Uint32() & 0xFFFF, } } diff --git a/transport/hysteria/core/client.go b/transport/hysteria/core/client.go index 1df14242ef..e0f3a0dd1f 100644 --- a/transport/hysteria/core/client.go +++ b/transport/hysteria/core/client.go @@ -6,20 +6,20 @@ import ( "crypto/tls" "errors" "fmt" - "math/rand" "net" "strconv" "sync" "time" - "github.com/lunixbochs/struc" - "github.com/metacubex/quic-go" - "github.com/metacubex/quic-go/congestion" - "github.com/Dreamacro/clash/transport/hysteria/obfs" "github.com/Dreamacro/clash/transport/hysteria/pmtud_fix" "github.com/Dreamacro/clash/transport/hysteria/transport" "github.com/Dreamacro/clash/transport/hysteria/utils" + + "github.com/lunixbochs/struc" + "github.com/metacubex/quic-go" + "github.com/metacubex/quic-go/congestion" + "github.com/zhangyunhao116/fastrand" ) var ( @@ -408,7 +408,7 @@ func (c *quicPktConn) WriteTo(p []byte, addr string) error { if err != nil { if errSize, ok := err.(quic.ErrMessageTooLarge); ok { // need to frag - msg.MsgID = uint16(rand.Intn(0xFFFF)) + 1 // msgID must be > 0 when fragCount > 1 + msg.MsgID = uint16(fastrand.Intn(0xFFFF)) + 1 // msgID must be > 0 when fragCount > 1 fragMsgs := fragUDPMessage(msg, int(errSize)) for _, fragMsg := range fragMsgs { msgBuf.Reset() diff --git a/transport/hysteria/obfs/xplus.go b/transport/hysteria/obfs/xplus.go index dd636452dd..171bf28186 100644 --- a/transport/hysteria/obfs/xplus.go +++ b/transport/hysteria/obfs/xplus.go @@ -2,9 +2,8 @@ package obfs import ( "crypto/sha256" - "math/rand" - "sync" - "time" + + "github.com/zhangyunhao116/fastrand" ) // [salt][obfuscated payload] @@ -12,16 +11,12 @@ import ( const saltLen = 16 type XPlusObfuscator struct { - Key []byte - RandSrc *rand.Rand - - lk sync.Mutex + Key []byte } func NewXPlusObfuscator(key []byte) *XPlusObfuscator { return &XPlusObfuscator{ - Key: key, - RandSrc: rand.New(rand.NewSource(time.Now().UnixNano())), + Key: key, } } @@ -40,9 +35,7 @@ func (x *XPlusObfuscator) Deobfuscate(in []byte, out []byte) int { } func (x *XPlusObfuscator) Obfuscate(in []byte, out []byte) int { - x.lk.Lock() - _, _ = x.RandSrc.Read(out[:saltLen]) // salt - x.lk.Unlock() + _, _ = fastrand.Read(out[:saltLen]) // salt // Obfuscate the payload key := sha256.Sum256(append(x.Key, out[:saltLen]...)) for i, c := range in { diff --git a/transport/restls/restls.go b/transport/restls/restls.go new file mode 100644 index 0000000000..0f3ba8ac77 --- /dev/null +++ b/transport/restls/restls.go @@ -0,0 +1,39 @@ +package restls + +import ( + "context" + "net" + + tls "github.com/3andne/restls-client-go" +) + +const ( + Mode string = "restls" +) + +type Restls struct { + *tls.UConn +} + +func (r *Restls) Upstream() any { + return r.UConn.NetConn() +} + +// NewRestls return a Restls Connection +func NewRestls(ctx context.Context, conn net.Conn, config *tls.Config) (net.Conn, error) { + clientHellowID := tls.HelloChrome_Auto + if config != nil { + clientIDPtr := config.ClientID.Load() + if clientIDPtr != nil { + clientHellowID = *clientIDPtr + } + } + restls := &Restls{ + UConn: tls.UClient(conn, config, clientHellowID), + } + if err := restls.HandshakeContext(ctx); err != nil { + return nil, err + } + + return restls, nil +} diff --git a/transport/simple-obfs/http.go b/transport/simple-obfs/http.go index a06bad2394..80db34ba79 100644 --- a/transport/simple-obfs/http.go +++ b/transport/simple-obfs/http.go @@ -5,11 +5,12 @@ import ( "encoding/base64" "fmt" "io" - "math/rand" "net" "net/http" "github.com/Dreamacro/clash/common/pool" + + "github.com/zhangyunhao116/fastrand" ) // HTTPObfs is shadowsocks http simple-obfs implementation @@ -63,9 +64,9 @@ func (ho *HTTPObfs) Read(b []byte) (int, error) { func (ho *HTTPObfs) Write(b []byte) (int, error) { if ho.firstRequest { randBytes := make([]byte, 16) - rand.Read(randBytes) + fastrand.Read(randBytes) req, _ := http.NewRequest("GET", fmt.Sprintf("http://%s/", ho.host), bytes.NewBuffer(b[:])) - req.Header.Set("User-Agent", fmt.Sprintf("curl/7.%d.%d", rand.Int()%54, rand.Int()%2)) + req.Header.Set("User-Agent", fmt.Sprintf("curl/7.%d.%d", fastrand.Int()%54, fastrand.Int()%2)) req.Header.Set("Upgrade", "websocket") req.Header.Set("Connection", "Upgrade") req.Host = ho.host diff --git a/transport/simple-obfs/tls.go b/transport/simple-obfs/tls.go index fed8a48331..f41e326370 100644 --- a/transport/simple-obfs/tls.go +++ b/transport/simple-obfs/tls.go @@ -4,16 +4,13 @@ import ( "bytes" "encoding/binary" "io" - "math/rand" "net" "time" "github.com/Dreamacro/clash/common/pool" -) -func init() { - rand.Seed(time.Now().Unix()) -} + "github.com/zhangyunhao116/fastrand" +) const ( chunkSize = 1 << 14 // 2 ** 14 == 16 * 1024 @@ -130,8 +127,8 @@ func NewTLSObfs(conn net.Conn, server string) net.Conn { func makeClientHelloMsg(data []byte, server string) []byte { random := make([]byte, 28) sessionID := make([]byte, 32) - rand.Read(random) - rand.Read(sessionID) + fastrand.Read(random) + fastrand.Read(sessionID) buf := &bytes.Buffer{} diff --git a/transport/sing-shadowtls/shadowtls.go b/transport/sing-shadowtls/shadowtls.go new file mode 100644 index 0000000000..0e1e95c0d0 --- /dev/null +++ b/transport/sing-shadowtls/shadowtls.go @@ -0,0 +1,91 @@ +package sing_shadowtls + +import ( + "context" + "crypto/tls" + "net" + + tlsC "github.com/Dreamacro/clash/component/tls" + "github.com/Dreamacro/clash/log" + + "github.com/sagernet/sing-shadowtls" + sing_common "github.com/sagernet/sing/common" + utls "github.com/sagernet/utls" +) + +const ( + Mode string = "shadow-tls" +) + +var ( + DefaultALPN = []string{"h2", "http/1.1"} +) + +type ShadowTLSOption struct { + Password string + Host string + Fingerprint string + ClientFingerprint string + SkipCertVerify bool + Version int +} + +func NewShadowTLS(ctx context.Context, conn net.Conn, option *ShadowTLSOption) (net.Conn, error) { + tlsConfig := &tls.Config{ + NextProtos: DefaultALPN, + MinVersion: tls.VersionTLS12, + InsecureSkipVerify: option.SkipCertVerify, + ServerName: option.Host, + } + + var err error + if len(option.Fingerprint) == 0 { + tlsConfig = tlsC.GetGlobalTLSConfig(tlsConfig) + } else { + if tlsConfig, err = tlsC.GetSpecifiedFingerprintTLSConfig(tlsConfig, option.Fingerprint); err != nil { + return nil, err + } + } + + tlsHandshake := shadowtls.DefaultTLSHandshakeFunc(option.Password, tlsConfig) + if len(option.ClientFingerprint) != 0 { + if fingerprint, exists := tlsC.GetFingerprint(option.ClientFingerprint); exists { + tlsHandshake = uTLSHandshakeFunc(tlsConfig, *fingerprint.ClientHelloID) + } + } + client, err := shadowtls.NewClient(shadowtls.ClientConfig{ + Version: option.Version, + Password: option.Password, + TLSHandshake: tlsHandshake, + Logger: log.SingLogger, + }) + if err != nil { + return nil, err + } + return client.DialContextConn(ctx, conn) +} + +func uTLSHandshakeFunc(config *tls.Config, clientHelloID utls.ClientHelloID) shadowtls.TLSHandshakeFunc { + return func(ctx context.Context, conn net.Conn, sessionIDGenerator shadowtls.TLSSessionIDGeneratorFunc) error { + tlsConfig := &utls.Config{ + Rand: config.Rand, + Time: config.Time, + VerifyPeerCertificate: config.VerifyPeerCertificate, + RootCAs: config.RootCAs, + NextProtos: config.NextProtos, + ServerName: config.ServerName, + InsecureSkipVerify: config.InsecureSkipVerify, + CipherSuites: config.CipherSuites, + MinVersion: config.MinVersion, + MaxVersion: config.MaxVersion, + CurvePreferences: sing_common.Map(config.CurvePreferences, func(it tls.CurveID) utls.CurveID { + return utls.CurveID(it) + }), + SessionTicketsDisabled: config.SessionTicketsDisabled, + Renegotiation: utls.RenegotiationSupport(config.Renegotiation), + SessionIDGenerator: sessionIDGenerator, + } + tlsConn := utls.UClient(conn, tlsConfig, clientHelloID) + return tlsConn.HandshakeContext(ctx) + } +} diff --git a/transport/ssr/obfs/http_simple.go b/transport/ssr/obfs/http_simple.go index c1ea76738f..c91cca49ea 100644 --- a/transport/ssr/obfs/http_simple.go +++ b/transport/ssr/obfs/http_simple.go @@ -4,12 +4,13 @@ import ( "bytes" "encoding/hex" "io" - "math/rand" "net" "strconv" "strings" "github.com/Dreamacro/clash/common/pool" + + "github.com/zhangyunhao116/fastrand" ) func init() { @@ -81,7 +82,7 @@ func (c *httpConn) Write(b []byte) (int, error) { bLength := len(b) headDataLength := bLength if bLength-headLength > 64 { - headDataLength = headLength + rand.Intn(65) + headDataLength = headLength + fastrand.Intn(65) } headData := b[:headDataLength] b = b[headDataLength:] @@ -99,7 +100,7 @@ func (c *httpConn) Write(b []byte) (int, error) { } } hosts := strings.Split(host, ",") - host = hosts[rand.Intn(len(hosts))] + host = hosts[fastrand.Intn(len(hosts))] buf := pool.GetBuffer() defer pool.PutBuffer(buf) @@ -118,7 +119,7 @@ func (c *httpConn) Write(b []byte) (int, error) { buf.WriteString(body + "\r\n\r\n") } else { buf.WriteString("User-Agent: ") - buf.WriteString(userAgent[rand.Intn(len(userAgent))]) + buf.WriteString(userAgent[fastrand.Intn(len(userAgent))]) buf.WriteString("\r\nAccept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8\r\nAccept-Language: en-US,en;q=0.8\r\nAccept-Encoding: gzip, deflate\r\n") if c.post { packBoundary(buf) @@ -146,7 +147,7 @@ func packBoundary(buf *bytes.Buffer) { buf.WriteString("Content-Type: multipart/form-data; boundary=") set := "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789" for i := 0; i < 32; i++ { - buf.WriteByte(set[rand.Intn(62)]) + buf.WriteByte(set[fastrand.Intn(62)]) } buf.WriteString("\r\n") } diff --git a/transport/ssr/obfs/random_head.go b/transport/ssr/obfs/random_head.go index b10b01c56b..4c55d9514f 100644 --- a/transport/ssr/obfs/random_head.go +++ b/transport/ssr/obfs/random_head.go @@ -3,10 +3,11 @@ package obfs import ( "encoding/binary" "hash/crc32" - "math/rand" "net" "github.com/Dreamacro/clash/common/pool" + + "github.com/zhangyunhao116/fastrand" ) func init() { @@ -53,10 +54,10 @@ func (c *randomHeadConn) Write(b []byte) (int, error) { c.buf = append(c.buf, b...) if !c.hasSentHeader { c.hasSentHeader = true - dataLength := rand.Intn(96) + 4 + dataLength := fastrand.Intn(96) + 4 buf := pool.Get(dataLength + 4) defer pool.Put(buf) - rand.Read(buf[:dataLength]) + fastrand.Read(buf[:dataLength]) binary.LittleEndian.PutUint32(buf[dataLength:], 0xffffffff-crc32.ChecksumIEEE(buf[:dataLength])) _, err := c.Conn.Write(buf) return len(b), err diff --git a/transport/ssr/obfs/tls1.2_ticket_auth.go b/transport/ssr/obfs/tls1.2_ticket_auth.go index 10f2786add..af945133bc 100644 --- a/transport/ssr/obfs/tls1.2_ticket_auth.go +++ b/transport/ssr/obfs/tls1.2_ticket_auth.go @@ -4,13 +4,14 @@ import ( "bytes" "crypto/hmac" "encoding/binary" - "math/rand" "net" "strings" "time" "github.com/Dreamacro/clash/common/pool" "github.com/Dreamacro/clash/transport/ssr/tools" + + "github.com/zhangyunhao116/fastrand" ) func init() { @@ -25,7 +26,7 @@ type tls12Ticket struct { func newTLS12Ticket(b *Base) Obfs { r := &tls12Ticket{Base: b, authData: &authData{}} - rand.Read(r.clientID[:]) + fastrand.Read(r.clientID[:]) return r } @@ -90,7 +91,7 @@ func (c *tls12TicketConn) Write(b []byte) (int, error) { buf := pool.GetBuffer() defer pool.PutBuffer(buf) for len(b) > 2048 { - size := rand.Intn(4096) + 100 + size := fastrand.Intn(4096) + 100 if len(b) < size { size = len(b) } @@ -196,7 +197,7 @@ func packSNIData(buf *bytes.Buffer, u string) { } func (c *tls12TicketConn) packTicketBuf(buf *bytes.Buffer, u string) { - length := 16 * (rand.Intn(17) + 8) + length := 16 * (fastrand.Intn(17) + 8) buf.Write([]byte{0, 0x23}) binary.Write(buf, binary.BigEndian, uint16(length)) tools.AppendRandBytes(buf, length) @@ -221,6 +222,6 @@ func (t *tls12Ticket) getHost() string { host = "" } hosts := strings.Split(host, ",") - host = hosts[rand.Intn(len(hosts))] + host = hosts[fastrand.Intn(len(hosts))] return host } diff --git a/transport/ssr/protocol/auth_aes128_sha1.go b/transport/ssr/protocol/auth_aes128_sha1.go index 7b4da962c3..4de481510a 100644 --- a/transport/ssr/protocol/auth_aes128_sha1.go +++ b/transport/ssr/protocol/auth_aes128_sha1.go @@ -4,7 +4,6 @@ import ( "bytes" "encoding/binary" "math" - "math/rand" "net" "strconv" "strings" @@ -12,6 +11,8 @@ import ( "github.com/Dreamacro/clash/common/pool" "github.com/Dreamacro/clash/log" "github.com/Dreamacro/clash/transport/ssr/tools" + + "github.com/zhangyunhao116/fastrand" ) type ( @@ -64,7 +65,7 @@ func (a *authAES128) initUserData() { } if len(a.userKey) == 0 { a.userKey = a.Key - rand.Read(a.userID[:]) + fastrand.Read(a.userID[:]) } } @@ -198,7 +199,7 @@ func (a *authAES128) packData(poolBuf *bytes.Buffer, data []byte, fullDataLength } func trapezoidRandom(max int, d float64) int { - base := rand.Float64() + base := fastrand.Float64() if d-0 > 1e-6 { a := 1 - d base = (math.Sqrt(a*a+4*d*base) - a) / (2 * d) @@ -219,10 +220,10 @@ func (a *authAES128) getRandDataLengthForPackData(dataLength, fullDataLength int if revLength > -1460 { return trapezoidRandom(revLength+1460, -0.3) } - return rand.Intn(32) + return fastrand.Intn(32) } if dataLength > 900 { - return rand.Intn(revLength) + return fastrand.Intn(revLength) } return trapezoidRandom(revLength, -0.3) } @@ -247,7 +248,7 @@ func (a *authAES128) packAuthData(poolBuf *bytes.Buffer, data []byte) { copy(macKey, a.iv) copy(macKey[len(a.iv):], a.Key) - poolBuf.WriteByte(byte(rand.Intn(256))) + poolBuf.WriteByte(byte(fastrand.Intn(256))) poolBuf.Write(a.hmac(macKey, poolBuf.Bytes())[:6]) poolBuf.Write(a.userID[:]) err := a.authData.putEncryptedData(poolBuf, a.userKey, [2]int{packedAuthDataLength, randDataLength}, a.salt) @@ -263,9 +264,9 @@ func (a *authAES128) packAuthData(poolBuf *bytes.Buffer, data []byte) { func (a *authAES128) getRandDataLengthForPackAuthData(size int) int { if size > 400 { - return rand.Intn(512) + return fastrand.Intn(512) } - return rand.Intn(1024) + return fastrand.Intn(1024) } func (a *authAES128) packRandData(poolBuf *bytes.Buffer, size int) { diff --git a/transport/ssr/protocol/auth_sha1_v4.go b/transport/ssr/protocol/auth_sha1_v4.go index 30392c9e77..9e814ac209 100644 --- a/transport/ssr/protocol/auth_sha1_v4.go +++ b/transport/ssr/protocol/auth_sha1_v4.go @@ -5,11 +5,12 @@ import ( "encoding/binary" "hash/adler32" "hash/crc32" - "math/rand" "net" "github.com/Dreamacro/clash/common/pool" "github.com/Dreamacro/clash/transport/ssr/tools" + + "github.com/zhangyunhao116/fastrand" ) func init() { @@ -176,7 +177,7 @@ func (a *authSHA1V4) getRandDataLength(size int) int { return 0 } if size > 400 { - return rand.Intn(256) + return fastrand.Intn(256) } - return rand.Intn(512) + return fastrand.Intn(512) } diff --git a/transport/ssr/protocol/base.go b/transport/ssr/protocol/base.go index 4bf799b352..a826bec8b0 100644 --- a/transport/ssr/protocol/base.go +++ b/transport/ssr/protocol/base.go @@ -6,13 +6,14 @@ import ( "crypto/cipher" "encoding/base64" "encoding/binary" - "math/rand" "sync" "time" "github.com/Dreamacro/clash/common/pool" "github.com/Dreamacro/clash/log" "github.com/Dreamacro/clash/transport/shadowsocks/core" + + "github.com/zhangyunhao116/fastrand" ) type Base struct { @@ -37,8 +38,8 @@ func (a *authData) next() *authData { a.mutex.Lock() defer a.mutex.Unlock() if a.connectionID > 0xff000000 || a.connectionID == 0 { - rand.Read(a.clientID[:]) - a.connectionID = rand.Uint32() & 0xffffff + fastrand.Read(a.clientID[:]) + a.connectionID = fastrand.Uint32() & 0xffffff } a.connectionID++ copy(r.clientID[:], a.clientID[:]) diff --git a/transport/ssr/protocol/protocol.go b/transport/ssr/protocol/protocol.go index 41bd984c8b..5b86ecb926 100644 --- a/transport/ssr/protocol/protocol.go +++ b/transport/ssr/protocol/protocol.go @@ -4,8 +4,9 @@ import ( "bytes" "errors" "fmt" - "math/rand" "net" + + "github.com/zhangyunhao116/fastrand" ) var ( @@ -68,7 +69,7 @@ func getHeadSize(b []byte, defaultValue int) int { func getDataLength(b []byte) int { bLength := len(b) - dataLength := getHeadSize(b, 30) + rand.Intn(32) + dataLength := getHeadSize(b, 30) + fastrand.Intn(32) if bLength < dataLength { return bLength } diff --git a/transport/trojan/trojan.go b/transport/trojan/trojan.go index e336d9db95..8eae8237b5 100644 --- a/transport/trojan/trojan.go +++ b/transport/trojan/trojan.go @@ -19,6 +19,7 @@ import ( "github.com/Dreamacro/clash/transport/socks5" "github.com/Dreamacro/clash/transport/vless" "github.com/Dreamacro/clash/transport/vmess" + xtls "github.com/xtls/go" ) @@ -54,6 +55,7 @@ type Option struct { Flow string FlowShow bool ClientFingerprint string + Reality *tlsC.RealityConfig } type WebsocketOption struct { @@ -117,16 +119,24 @@ func (t *Trojan) StreamConn(conn net.Conn) (net.Conn, error) { } if len(t.option.ClientFingerprint) != 0 { - utlsConn, valid := vmess.GetUtlsConnWithClientFingerprint(conn, t.option.ClientFingerprint, tlsConfig) - if valid { + if t.option.Reality == nil { + utlsConn, valid := vmess.GetUTLSConn(conn, t.option.ClientFingerprint, tlsConfig) + if valid { + ctx, cancel := context.WithTimeout(context.Background(), C.DefaultTLSTimeout) + defer cancel() + + err := utlsConn.(*tlsC.UConn).HandshakeContext(ctx) + return utlsConn, err + } + } else { ctx, cancel := context.WithTimeout(context.Background(), C.DefaultTLSTimeout) defer cancel() - - err := utlsConn.(*tlsC.UConn).HandshakeContext(ctx) - return utlsConn, err - + return tlsC.GetRealityConn(ctx, conn, t.option.ClientFingerprint, tlsConfig, t.option.Reality) } } + if t.option.Reality != nil { + return nil, errors.New("REALITY is based on uTLS, please set a client-fingerprint") + } tlsConn := tls.Client(conn, tlsConfig) diff --git a/transport/tuic/client.go b/transport/tuic/client.go index d3f511dff0..4932dc9b58 100644 --- a/transport/tuic/client.go +++ b/transport/tuic/client.go @@ -6,19 +6,19 @@ import ( "context" "crypto/tls" "errors" - "math/rand" "net" "runtime" "sync" "sync/atomic" "time" - "github.com/metacubex/quic-go" - N "github.com/Dreamacro/clash/common/net" "github.com/Dreamacro/clash/common/pool" C "github.com/Dreamacro/clash/constant" "github.com/Dreamacro/clash/log" + + "github.com/metacubex/quic-go" + "github.com/zhangyunhao116/fastrand" ) var ( @@ -352,7 +352,7 @@ func (t *clientImpl) ListenPacketWithDialer(ctx context.Context, metadata *C.Met pipe1, pipe2 := net.Pipe() var connId uint32 for { - connId = rand.Uint32() + connId = fastrand.Uint32() _, loaded := t.udpInputMap.LoadOrStore(connId, pipe1) if !loaded { break diff --git a/transport/tuic/congestion/bbr_sender.go b/transport/tuic/congestion/bbr_sender.go index 5adbd0b762..d848a9a820 100644 --- a/transport/tuic/congestion/bbr_sender.go +++ b/transport/tuic/congestion/bbr_sender.go @@ -5,11 +5,11 @@ package congestion import ( "fmt" "math" - "math/rand" "net" "time" "github.com/metacubex/quic-go/congestion" + "github.com/zhangyunhao116/fastrand" ) const ( @@ -780,7 +780,7 @@ func (b *bbrSender) EnterProbeBandwidthMode(now time.Time) { // Pick a random offset for the gain cycle out of {0, 2..7} range. 1 is // excluded because in that case increased gain and decreased gain would not // follow each other. - b.cycleCurrentOffset = rand.Int() % (GainCycleLength - 1) + b.cycleCurrentOffset = fastrand.Int() % (GainCycleLength - 1) if b.cycleCurrentOffset >= 1 { b.cycleCurrentOffset += 1 } diff --git a/transport/tuic/conn.go b/transport/tuic/conn.go index 7ecc3f0d97..d5955e1386 100644 --- a/transport/tuic/conn.go +++ b/transport/tuic/conn.go @@ -1,7 +1,6 @@ package tuic import ( - "fmt" "net" "net/netip" "sync" @@ -200,8 +199,8 @@ func (q *quicStreamPacketConn) ReadFrom(p []byte) (n int, addr net.Addr, err err } func (q *quicStreamPacketConn) WriteTo(p []byte, addr net.Addr) (n int, err error) { - if len(p) > q.maxUdpRelayPacketSize { - return 0, fmt.Errorf("udp packet too large(%d > %d)", len(p), q.maxUdpRelayPacketSize) + if q.udpRelayMode != "quic" && len(p) > q.maxUdpRelayPacketSize { + return 0, quic.ErrMessageTooLarge(q.maxUdpRelayPacketSize) } if q.closed { return 0, net.ErrClosed @@ -215,7 +214,6 @@ func (q *quicStreamPacketConn) WriteTo(p []byte, addr net.Addr) (n int, err erro q.deferQuicConnFn(q.quicConn, err) }() } - addr.String() buf := pool.GetBuffer() defer pool.PutBuffer(buf) addrPort, err := netip.ParseAddrPort(addr.String()) @@ -239,7 +237,8 @@ func (q *quicStreamPacketConn) WriteTo(p []byte, addr net.Addr) (n int, err erro return } default: // native - err = q.quicConn.SendMessage(buf.Bytes()) + data := buf.Bytes() + err = q.quicConn.SendMessage(data) if err != nil { return } diff --git a/transport/tuic/protocol.go b/transport/tuic/protocol.go index a54c8e5499..570b6e543c 100644 --- a/transport/tuic/protocol.go +++ b/transport/tuic/protocol.go @@ -114,9 +114,6 @@ func NewAuthenticate(TKN [32]byte) Authenticate { func ReadAuthenticateWithHead(head CommandHead, reader BufferedReader) (c Authenticate, err error) { c.CommandHead = head - if err != nil { - return - } if c.CommandHead.TYPE != AuthenticateType { err = fmt.Errorf("error command type: %s", c.CommandHead.TYPE) return @@ -170,9 +167,6 @@ func NewConnect(ADDR Address) Connect { func ReadConnectWithHead(head CommandHead, reader BufferedReader) (c Connect, err error) { c.CommandHead = head - if err != nil { - return - } if c.CommandHead.TYPE != ConnectType { err = fmt.Errorf("error command type: %s", c.CommandHead.TYPE) return @@ -228,9 +222,6 @@ func NewPacket(ASSOC_ID uint32, LEN uint16, ADDR Address, DATA []byte) Packet { func ReadPacketWithHead(head CommandHead, reader BufferedReader) (c Packet, err error) { c.CommandHead = head - if err != nil { - return - } if c.CommandHead.TYPE != PacketType { err = fmt.Errorf("error command type: %s", c.CommandHead.TYPE) return @@ -291,6 +282,8 @@ func (c Packet) BytesLen() int { return c.CommandHead.BytesLen() + 4 + 2 + c.ADDR.BytesLen() + len(c.DATA) } +var PacketOverHead = NewPacket(0, 0, NewAddressAddrPort(netip.AddrPortFrom(netip.IPv6Unspecified(), 0)), nil).BytesLen() + type Dissociate struct { CommandHead ASSOC_ID uint32 @@ -305,9 +298,6 @@ func NewDissociate(ASSOC_ID uint32) Dissociate { func ReadDissociateWithHead(head CommandHead, reader BufferedReader) (c Dissociate, err error) { c.CommandHead = head - if err != nil { - return - } if c.CommandHead.TYPE != DissociateType { err = fmt.Errorf("error command type: %s", c.CommandHead.TYPE) return @@ -476,15 +466,17 @@ func NewAddress(metadata *C.Metadata) Address { func NewAddressAddrPort(addrPort netip.AddrPort) Address { var addrType byte - if addrPort.Addr().Is4() { + port := addrPort.Port() + addr := addrPort.Addr().Unmap() + if addr.Is4() { addrType = AtypIPv4 } else { addrType = AtypIPv6 } return Address{ TYPE: addrType, - ADDR: addrPort.Addr().AsSlice(), - PORT: addrPort.Port(), + ADDR: addr.AsSlice(), + PORT: port, } } diff --git a/transport/tuic/server.go b/transport/tuic/server.go index 2830b32424..5eb6e6116c 100644 --- a/transport/tuic/server.go +++ b/transport/tuic/server.go @@ -11,13 +11,14 @@ import ( "sync/atomic" "time" - "github.com/gofrs/uuid" - "github.com/metacubex/quic-go" - N "github.com/Dreamacro/clash/common/net" "github.com/Dreamacro/clash/common/pool" + "github.com/Dreamacro/clash/common/utils" C "github.com/Dreamacro/clash/constant" "github.com/Dreamacro/clash/transport/socks5" + + "github.com/gofrs/uuid" + "github.com/metacubex/quic-go" ) type ServerOption struct { @@ -55,14 +56,10 @@ func (s *Server) Serve() error { return err } SetCongestionController(conn, s.CongestionController) - uuid, err := uuid.NewV4() - if err != nil { - return err - } h := &serverHandler{ Server: s, quicConn: conn, - uuid: uuid, + uuid: utils.NewUUIDV4(), authCh: make(chan struct{}), } go h.handle() @@ -75,7 +72,7 @@ func (s *Server) Close() error { type serverHandler struct { *Server - quicConn quic.Connection + quicConn quic.EarlyConnection uuid uuid.UUID authCh chan struct{} @@ -86,13 +83,6 @@ type serverHandler struct { } func (s *serverHandler) handle() { - time.AfterFunc(s.AuthenticationTimeout, func() { - s.authOnce.Do(func() { - _ = s.quicConn.CloseWithError(AuthenticationTimeout, "") - s.authOk = false - close(s.authCh) - }) - }) go func() { _ = s.handleUniStream() }() @@ -102,6 +92,15 @@ func (s *serverHandler) handle() { go func() { _ = s.handleMessage() }() + + <-s.quicConn.HandshakeComplete().Done() + time.AfterFunc(s.AuthenticationTimeout, func() { + s.authOnce.Do(func() { + _ = s.quicConn.CloseWithError(AuthenticationTimeout, "AuthenticationTimeout") + s.authOk = false + close(s.authCh) + }) + }) } func (s *serverHandler) handleMessage() (err error) { @@ -151,14 +150,10 @@ func (s *serverHandler) parsePacket(packet Packet, udpRelayMode string) (err err return s.HandleUdpFn(packet.ADDR.SocksAddr(), &serverUDPPacket{ pc: pc, packet: &packet, - rAddr: s.genServerAssocIdAddr(assocId, s.quicConn.RemoteAddr()), + rAddr: N.NewCustomAddr("tuic", fmt.Sprintf("tuic-%s-%d", s.uuid, assocId), s.quicConn.RemoteAddr()), // for tunnel's handleUDPConn }) } -func (s *serverHandler) genServerAssocIdAddr(assocId uint32, addr net.Addr) net.Addr { - return &ServerAssocIdAddr{assocId: fmt.Sprintf("tuic-%s-%d", s.uuid.String(), assocId), addr: addr} -} - func (s *serverHandler) handleStream() (err error) { for { var quicStream quic.Stream @@ -238,7 +233,7 @@ func (s *serverHandler) handleUniStream() (err error) { } s.authOnce.Do(func() { if !ok { - _ = s.quicConn.CloseWithError(AuthenticationFailed, "") + _ = s.quicConn.CloseWithError(AuthenticationFailed, "AuthenticationFailed") } s.authOk = ok close(s.authCh) @@ -273,23 +268,6 @@ func (s *serverHandler) handleUniStream() (err error) { } } -type ServerAssocIdAddr struct { - assocId string - addr net.Addr -} - -func (a ServerAssocIdAddr) Network() string { - return "ServerAssocIdAddr" -} - -func (a ServerAssocIdAddr) String() string { - return a.assocId -} - -func (a ServerAssocIdAddr) RawAddr() net.Addr { - return a.addr -} - type serverUDPPacket struct { pc *quicStreamPacketConn packet *Packet diff --git a/transport/vless/conn.go b/transport/vless/conn.go index aceda463ef..1f7d2cb345 100644 --- a/transport/vless/conn.go +++ b/transport/vless/conn.go @@ -1,105 +1,298 @@ package vless import ( + "bytes" + "crypto/subtle" + gotls "crypto/tls" "encoding/binary" "errors" "fmt" "io" "net" + "reflect" "sync" - "time" + "unsafe" "github.com/Dreamacro/clash/common/buf" N "github.com/Dreamacro/clash/common/net" + tlsC "github.com/Dreamacro/clash/component/tls" + "github.com/Dreamacro/clash/log" "github.com/gofrs/uuid" + utls "github.com/sagernet/utls" xtls "github.com/xtls/go" "google.golang.org/protobuf/proto" ) type Conn struct { - N.ExtendedConn + N.ExtendedWriter + N.ExtendedReader + net.Conn dst *DstAddr id *uuid.UUID addons *Addons received bool - handshake chan struct{} handshakeMutex sync.Mutex + needHandshake bool err error + + tlsConn net.Conn + input *bytes.Reader + rawInput *bytes.Buffer + + packetsToFilter int + isTLS bool + isTLS12orAbove bool + enableXTLS bool + cipher uint16 + remainingServerHello uint16 + readRemainingContent int + readRemainingPadding int + readProcess bool + readFilterUUID bool + readLastCommand byte + writeFilterApplicationData bool + writeDirect bool } func (vc *Conn) Read(b []byte) (int, error) { if vc.received { - return vc.ExtendedConn.Read(b) + if vc.readProcess { + buffer := buf.With(b) + err := vc.ReadBuffer(buffer) + return buffer.Len(), err + } + return vc.ExtendedReader.Read(b) } if err := vc.recvResponse(); err != nil { return 0, err } vc.received = true - return vc.ExtendedConn.Read(b) + return vc.Read(b) } func (vc *Conn) ReadBuffer(buffer *buf.Buffer) error { if vc.received { - return vc.ExtendedConn.ReadBuffer(buffer) + toRead := buffer.FreeBytes() + if vc.readRemainingContent > 0 { + if vc.readRemainingContent < buffer.FreeLen() { + toRead = toRead[:vc.readRemainingContent] + } + n, err := vc.ExtendedReader.Read(toRead) + buffer.Truncate(n) + vc.readRemainingContent -= n + vc.FilterTLS(toRead) + return err + } + if vc.readRemainingPadding > 0 { + _, err := io.CopyN(io.Discard, vc.ExtendedReader, int64(vc.readRemainingPadding)) + if err != nil { + return err + } + vc.readRemainingPadding = 0 + } + if vc.readProcess { + switch vc.readLastCommand { + case commandPaddingContinue: + //if vc.isTLS || vc.packetsToFilter > 0 { + headerUUIDLen := 0 + if vc.readFilterUUID { + headerUUIDLen = uuid.Size + } + var header []byte + if need := headerUUIDLen + paddingHeaderLen; buffer.FreeLen() < need { + header = make([]byte, need) + } else { + header = buffer.FreeBytes()[:need] + } + _, err := io.ReadFull(vc.ExtendedReader, header) + if err != nil { + return err + } + pos := 0 + if vc.readFilterUUID { + vc.readFilterUUID = false + pos = uuid.Size + if subtle.ConstantTimeCompare(vc.id.Bytes(), header[:uuid.Size]) != 1 { + err = fmt.Errorf("XTLS Vision server responded unknown UUID: %s", + uuid.FromBytesOrNil(header[:uuid.Size]).String()) + log.Errorln(err.Error()) + return err + } + } + vc.readLastCommand = header[pos] + vc.readRemainingContent = int(binary.BigEndian.Uint16(header[pos+1:])) + vc.readRemainingPadding = int(binary.BigEndian.Uint16(header[pos+3:])) + log.Debugln("XTLS Vision read padding: command=%d, payloadLen=%d, paddingLen=%d", + vc.readLastCommand, vc.readRemainingContent, vc.readRemainingPadding) + return vc.ReadBuffer(buffer) + //} + case commandPaddingEnd: + vc.readProcess = false + return vc.ReadBuffer(buffer) + case commandPaddingDirect: + needReturn := false + if vc.input != nil { + _, err := buffer.ReadFrom(vc.input) + if err != nil { + return err + } + if vc.input.Len() == 0 { + needReturn = true + vc.input = nil + } else { // buffer is full + return nil + } + } + if vc.rawInput != nil { + _, err := buffer.ReadFrom(vc.rawInput) + if err != nil { + return err + } + needReturn = true + if vc.rawInput.Len() == 0 { + vc.rawInput = nil + } + } + if vc.input == nil && vc.rawInput == nil { + vc.readProcess = false + vc.ExtendedReader = N.NewExtendedReader(vc.Conn) + log.Debugln("XTLS Vision direct read start") + } + if needReturn { + return nil + } + default: + err := fmt.Errorf("XTLS Vision read unknown command: %d", vc.readLastCommand) + log.Debugln(err.Error()) + return err + } + } + return vc.ExtendedReader.ReadBuffer(buffer) } if err := vc.recvResponse(); err != nil { return err } vc.received = true - return vc.ExtendedConn.ReadBuffer(buffer) + return vc.ReadBuffer(buffer) } func (vc *Conn) Write(p []byte) (int, error) { - select { - case <-vc.handshake: - default: - if vc.sendRequest(p) { + if vc.needHandshake { + vc.handshakeMutex.Lock() + if vc.needHandshake { + vc.needHandshake = false + if vc.sendRequest(p) { + vc.handshakeMutex.Unlock() + if vc.err != nil { + return 0, vc.err + } + return len(p), vc.err + } if vc.err != nil { + vc.handshakeMutex.Unlock() return 0, vc.err } - return len(p), vc.err } - if vc.err != nil { - return 0, vc.err + vc.handshakeMutex.Unlock() + } + + if vc.writeFilterApplicationData { + _buffer := buf.StackNew() + defer buf.KeepAlive(_buffer) + buffer := buf.Dup(_buffer) + defer buffer.Release() + buffer.Write(p) + err := vc.WriteBuffer(buffer) + if err != nil { + return 0, err } + return len(p), nil } - return vc.ExtendedConn.Write(p) + return vc.ExtendedWriter.Write(p) } func (vc *Conn) WriteBuffer(buffer *buf.Buffer) error { - select { - case <-vc.handshake: - default: - if vc.sendRequest(buffer.Bytes()) { - return vc.err - } - if vc.err != nil { - return vc.err + if vc.needHandshake { + vc.handshakeMutex.Lock() + if vc.needHandshake { + vc.needHandshake = false + if vc.sendRequest(buffer.Bytes()) { + vc.handshakeMutex.Unlock() + return vc.err + } + if vc.err != nil { + vc.handshakeMutex.Unlock() + return vc.err + } } + vc.handshakeMutex.Unlock() } - return vc.ExtendedConn.WriteBuffer(buffer) -} -func (vc *Conn) sendRequest(p []byte) bool { - vc.handshakeMutex.Lock() - defer vc.handshakeMutex.Unlock() + if vc.writeFilterApplicationData { + buffer2 := ReshapeBuffer(buffer) + defer buffer2.Release() + vc.FilterTLS(buffer.Bytes()) + command := commandPaddingContinue + if !vc.isTLS { + command = commandPaddingEnd - select { - case <-vc.handshake: - // The handshake has been completed before. - // So return false to remind the caller. - return false - default: + // disable XTLS + //vc.readProcess = false + vc.writeFilterApplicationData = false + vc.packetsToFilter = 0 + } else if buffer.Len() > 6 && bytes.Equal(buffer.To(3), tlsApplicationDataStart) || vc.packetsToFilter <= 0 { + command = commandPaddingEnd + if vc.enableXTLS { + command = commandPaddingDirect + vc.writeDirect = true + } + vc.writeFilterApplicationData = false + } + ApplyPadding(buffer, command, nil, vc.isTLS) + err := vc.ExtendedWriter.WriteBuffer(buffer) + if err != nil { + return err + } + if vc.writeDirect { + vc.ExtendedWriter = N.NewExtendedWriter(vc.Conn) + log.Debugln("XTLS Vision direct write start") + //time.Sleep(5 * time.Millisecond) + } + if buffer2 != nil { + if vc.writeDirect || !vc.isTLS { + return vc.ExtendedWriter.WriteBuffer(buffer2) + } + vc.FilterTLS(buffer2.Bytes()) + command = commandPaddingContinue + if buffer2.Len() > 6 && bytes.Equal(buffer2.To(3), tlsApplicationDataStart) || vc.packetsToFilter <= 0 { + command = commandPaddingEnd + if vc.enableXTLS { + command = commandPaddingDirect + vc.writeDirect = true + } + vc.writeFilterApplicationData = false + } + ApplyPadding(buffer2, command, nil, vc.isTLS) + err = vc.ExtendedWriter.WriteBuffer(buffer2) + if vc.writeDirect { + vc.ExtendedWriter = N.NewExtendedWriter(vc.Conn) + log.Debugln("XTLS Vision direct write start") + //time.Sleep(10 * time.Millisecond) + } + } + return err } - defer close(vc.handshake) + /*if vc.writeDirect { + log.Debugln("XTLS Vision Direct write, payloadLen=%d", buffer.Len()) + }*/ + return vc.ExtendedWriter.WriteBuffer(buffer) +} - requestLen := 1 // protocol version - requestLen += 16 // UUID - requestLen += 1 // addons length +func (vc *Conn) sendRequest(p []byte) bool { var addonsBytes []byte if vc.addons != nil { addonsBytes, vc.err = proto.Marshal(vc.addons) @@ -107,19 +300,32 @@ func (vc *Conn) sendRequest(p []byte) bool { return true } } - requestLen += len(addonsBytes) - requestLen += 1 // command - if !vc.dst.Mux { - requestLen += 2 // port - requestLen += 1 // addr type - requestLen += len(vc.dst.Addr) - } - requestLen += len(p) + isVision := vc.IsXTLSVisionEnabled() - _buffer := buf.StackNewSize(requestLen) - defer buf.KeepAlive(_buffer) - buffer := buf.Dup(_buffer) - defer buffer.Release() + var buffer *buf.Buffer + if isVision { + _buffer := buf.StackNew() + defer buf.KeepAlive(_buffer) + buffer = buf.Dup(_buffer) + defer buffer.Release() + } else { + requestLen := 1 // protocol version + requestLen += 16 // UUID + requestLen += 1 // addons length + requestLen += len(addonsBytes) + requestLen += 1 // command + if !vc.dst.Mux { + requestLen += 2 // port + requestLen += 1 // addr type + requestLen += len(vc.dst.Addr) + } + requestLen += len(p) + + _buffer := buf.StackNewSize(requestLen) + defer buf.KeepAlive(_buffer) + buffer = buf.Dup(_buffer) + defer buffer.Release() + } buf.Must( buffer.WriteByte(Version), // protocol version @@ -144,47 +350,103 @@ func (vc *Conn) sendRequest(p []byte) bool { ) } - buf.Must(buf.Error(buffer.Write(p))) + if isVision && !vc.dst.UDP && !vc.dst.Mux { + if len(p) == 0 { + WriteWithPadding(buffer, nil, commandPaddingContinue, vc.id, vc.isTLS) + } else { + vc.FilterTLS(p) + //if vc.isTLS { + WriteWithPadding(buffer, p, commandPaddingContinue, vc.id, vc.isTLS) + //} else { + // buf.Must(buf.Error(buffer.Write(p))) + // + // // disable XTLS + // vc.readProcess = false + // vc.writeFilterApplicationData = false + // vc.packetsToFilter = 0 + //} + } + } else { + buf.Must(buf.Error(buffer.Write(p))) + } - _, vc.err = vc.ExtendedConn.Write(buffer.Bytes()) + _, vc.err = vc.ExtendedWriter.Write(buffer.Bytes()) + if vc.err != nil { + return true + } + if isVision { + switch underlying := vc.tlsConn.(type) { + case *gotls.Conn: + if underlying.ConnectionState().Version != gotls.VersionTLS13 { + vc.err = ErrNotTLS13 + } + case *utls.UConn: + if underlying.ConnectionState().Version != utls.VersionTLS13 { + vc.err = ErrNotTLS13 + } + default: + vc.err = fmt.Errorf(`failed to use %s, maybe "security" is not "tls" or "utls"`, vc.addons.Flow) + } + vc.tlsConn = nil + } return true } func (vc *Conn) recvResponse() error { - var buf [1]byte - _, vc.err = io.ReadFull(vc.ExtendedConn, buf[:]) + var buffer [1]byte + _, vc.err = io.ReadFull(vc.ExtendedReader, buffer[:]) if vc.err != nil { return vc.err } - if buf[0] != Version { + if buffer[0] != Version { return errors.New("unexpected response version") } - _, vc.err = io.ReadFull(vc.ExtendedConn, buf[:]) + _, vc.err = io.ReadFull(vc.ExtendedReader, buffer[:]) if vc.err != nil { return vc.err } - length := int64(buf[0]) + length := int64(buffer[0]) if length != 0 { // addon data length > 0 - io.CopyN(io.Discard, vc.ExtendedConn, length) // just discard + io.CopyN(io.Discard, vc.ExtendedReader, length) // just discard } return nil } +func (vc *Conn) FrontHeadroom() int { + if vc.IsXTLSVisionEnabled() { + return paddingHeaderLen + } + return 0 +} + func (vc *Conn) Upstream() any { - return vc.ExtendedConn + if vc.tlsConn == nil { + return vc.Conn + } + return vc.tlsConn +} + +func (vc *Conn) NeedHandshake() bool { + return vc.needHandshake +} + +func (vc *Conn) IsXTLSVisionEnabled() bool { + return vc.addons != nil && vc.addons.Flow == XRV } // newConn return a Conn instance func newConn(conn net.Conn, client *Client, dst *DstAddr) (*Conn, error) { c := &Conn{ - ExtendedConn: N.NewExtendedConn(conn), - id: client.uuid, - dst: dst, - handshake: make(chan struct{}), + ExtendedReader: N.NewExtendedReader(conn), + ExtendedWriter: N.NewExtendedWriter(conn), + Conn: conn, + id: client.uuid, + dst: dst, + needHandshake: true, } if !dst.UDP && client.Addons != nil { @@ -205,15 +467,46 @@ func newConn(conn net.Conn, client *Client, dst *DstAddr) (*Conn, error) { } else { return nil, fmt.Errorf("failed to use %s, maybe \"security\" is not \"xtls\"", client.Addons.Flow) } + case XRV: + c.packetsToFilter = 6 + c.readProcess = true + c.readFilterUUID = true + c.writeFilterApplicationData = true + c.addons = client.Addons + var t reflect.Type + var p uintptr + switch underlying := conn.(type) { + case *gotls.Conn: + //log.Debugln("type tls") + c.Conn = underlying.NetConn() + c.tlsConn = underlying + t = reflect.TypeOf(underlying).Elem() + p = uintptr(unsafe.Pointer(underlying)) + case *utls.UConn: + //log.Debugln("type *utls.UConn") + c.Conn = underlying.NetConn() + c.tlsConn = underlying + t = reflect.TypeOf(underlying.Conn).Elem() + p = uintptr(unsafe.Pointer(underlying.Conn)) + case *tlsC.UConn: + //log.Debugln("type *tlsC.UConn") + c.Conn = underlying.NetConn() + c.tlsConn = underlying.UConn + t = reflect.TypeOf(underlying.Conn).Elem() + //log.Debugln("t:%v", t) + p = uintptr(unsafe.Pointer(underlying.Conn)) + default: + return nil, fmt.Errorf(`failed to use %s, maybe "security" is not "tls" or "utls"`, client.Addons.Flow) + } + i, _ := t.FieldByName("input") + r, _ := t.FieldByName("rawInput") + c.input = (*bytes.Reader)(unsafe.Pointer(p + i.Offset)) + c.rawInput = (*bytes.Buffer)(unsafe.Pointer(p + r.Offset)) + //if _, ok := c.Conn.(*net.TCPConn); !ok { + // log.Debugln("XTLS underlying conn is not *net.TCPConn, got %T", c.Conn) + //} } } - go func() { - select { - case <-c.handshake: - case <-time.After(200 * time.Millisecond): - c.sendRequest(nil) - } - }() return c, nil } diff --git a/transport/vless/filter.go b/transport/vless/filter.go new file mode 100644 index 0000000000..3ddfb8b99d --- /dev/null +++ b/transport/vless/filter.go @@ -0,0 +1,90 @@ +package vless + +import ( + "bytes" + "encoding/binary" + + "github.com/Dreamacro/clash/log" +) + +var ( + tls13SupportedVersions = []byte{0x00, 0x2b, 0x00, 0x02, 0x03, 0x04} + tlsClientHandshakeStart = []byte{0x16, 0x03} + tlsServerHandshakeStart = []byte{0x16, 0x03, 0x03} + tlsApplicationDataStart = []byte{0x17, 0x03, 0x03} + + tls13CipherSuiteMap = map[uint16]string{ + 0x1301: "TLS_AES_128_GCM_SHA256", + 0x1302: "TLS_AES_256_GCM_SHA384", + 0x1303: "TLS_CHACHA20_POLY1305_SHA256", + 0x1304: "TLS_AES_128_CCM_SHA256", + 0x1305: "TLS_AES_128_CCM_8_SHA256", + } +) + +const ( + tlsHandshakeTypeClientHello byte = 0x01 + tlsHandshakeTypeServerHello byte = 0x02 +) + +func (vc *Conn) FilterTLS(buffer []byte) (index int) { + if vc.packetsToFilter <= 0 { + return 0 + } + lenP := len(buffer) + vc.packetsToFilter-- + if index = bytes.Index(buffer, tlsServerHandshakeStart); index != -1 { + if lenP >= index+5 { + if buffer[0] == 22 && buffer[1] == 3 && buffer[2] == 3 { + vc.isTLS = true + if buffer[5] == tlsHandshakeTypeServerHello { + //log.Debugln("isTLS12orAbove") + vc.remainingServerHello = binary.BigEndian.Uint16(buffer[index+3:]) + 5 + vc.isTLS12orAbove = true + if lenP-index >= 79 && vc.remainingServerHello >= 79 { + sessionIDLen := int(buffer[index+43]) + vc.cipher = binary.BigEndian.Uint16(buffer[index+43+sessionIDLen+1:]) + } + } + } + } + } else if index = bytes.Index(buffer, tlsClientHandshakeStart); index != -1 { + if lenP >= index+5 && buffer[index+5] == tlsHandshakeTypeClientHello { + vc.isTLS = true + } + } + + if vc.remainingServerHello > 0 { + end := int(vc.remainingServerHello) + i := index + if i < 0 { + i = 0 + } + if i+end > lenP { + end = lenP + vc.remainingServerHello -= uint16(end - i) + } else { + vc.remainingServerHello -= uint16(end) + end += i + } + if bytes.Contains(buffer[i:end], tls13SupportedVersions) { + // TLS 1.3 Client Hello + cs, ok := tls13CipherSuiteMap[vc.cipher] + if ok && cs != "TLS_AES_128_CCM_8_SHA256" { + vc.enableXTLS = true + } + log.Debugln("XTLS Vision found TLS 1.3, packetLength=%d, CipherSuite=%s", lenP, cs) + vc.packetsToFilter = 0 + return + } else if vc.remainingServerHello <= 0 { + log.Debugln("XTLS Vision found TLS 1.2, packetLength=%d", lenP) + vc.packetsToFilter = 0 + return + } + log.Debugln("XTLS Vision found inconclusive server hello, packetLength=%d, remainingServerHelloBytes=%d", lenP, vc.remainingServerHello) + } + if vc.packetsToFilter <= 0 { + log.Debugln("XTLS Vision stop filtering") + } + return +} diff --git a/transport/vless/vision.go b/transport/vless/vision.go new file mode 100644 index 0000000000..8dc84e40f9 --- /dev/null +++ b/transport/vless/vision.go @@ -0,0 +1,81 @@ +package vless + +import ( + "bytes" + "encoding/binary" + + "github.com/Dreamacro/clash/common/buf" + "github.com/Dreamacro/clash/log" + + "github.com/gofrs/uuid" + "github.com/zhangyunhao116/fastrand" +) + +const ( + paddingHeaderLen = 1 + 2 + 2 // =5 + + commandPaddingContinue byte = 0x00 + commandPaddingEnd byte = 0x01 + commandPaddingDirect byte = 0x02 +) + +func WriteWithPadding(buffer *buf.Buffer, p []byte, command byte, userUUID *uuid.UUID, paddingTLS bool) { + contentLen := int32(len(p)) + var paddingLen int32 + if contentLen < 900 { + if paddingTLS { + //log.Debugln("long padding") + paddingLen = fastrand.Int31n(500) + 900 - contentLen + } else { + paddingLen = fastrand.Int31n(256) + } + } + if userUUID != nil { + buffer.Write(userUUID.Bytes()) + } + + buffer.WriteByte(command) + binary.BigEndian.PutUint16(buffer.Extend(2), uint16(contentLen)) + binary.BigEndian.PutUint16(buffer.Extend(2), uint16(paddingLen)) + buffer.Write(p) + + buffer.Extend(int(paddingLen)) + log.Debugln("XTLS Vision write padding1: command=%v, payloadLen=%v, paddingLen=%v", command, contentLen, paddingLen) +} + +func ApplyPadding(buffer *buf.Buffer, command byte, userUUID *uuid.UUID, paddingTLS bool) { + contentLen := int32(buffer.Len()) + var paddingLen int32 + if contentLen < 900 { + if paddingTLS { + //log.Debugln("long padding") + paddingLen = fastrand.Int31n(500) + 900 - contentLen + } else { + paddingLen = fastrand.Int31n(256) + } + } + + binary.BigEndian.PutUint16(buffer.ExtendHeader(2), uint16(paddingLen)) + binary.BigEndian.PutUint16(buffer.ExtendHeader(2), uint16(contentLen)) + buffer.ExtendHeader(1)[0] = command + if userUUID != nil { + copy(buffer.ExtendHeader(uuid.Size), userUUID.Bytes()) + } + + buffer.Extend(int(paddingLen)) + log.Debugln("XTLS Vision write padding2: command=%d, payloadLen=%d, paddingLen=%d", command, contentLen, paddingLen) +} + +func ReshapeBuffer(buffer *buf.Buffer) *buf.Buffer { + if buffer.Len() <= buf.BufferSize-paddingHeaderLen { + return nil + } + cutAt := bytes.LastIndex(buffer.Bytes(), tlsApplicationDataStart) + if cutAt == -1 { + cutAt = buf.BufferSize / 2 + } + buffer2 := buf.New() + buffer2.Write(buffer.From(cutAt)) + buffer.Truncate(cutAt) + return buffer2 +} diff --git a/transport/vless/vless.go b/transport/vless/vless.go index 4b101703ce..6989374c07 100644 --- a/transport/vless/vless.go +++ b/transport/vless/vless.go @@ -12,6 +12,7 @@ const ( XRO = "xtls-rprx-origin" XRD = "xtls-rprx-direct" XRS = "xtls-rprx-splice" + XRV = "xtls-rprx-vision" Version byte = 0 // protocol version. preview version is 0 ) diff --git a/transport/vless/xtls.go b/transport/vless/xtls.go index a1aea44fa9..3a31956878 100644 --- a/transport/vless/xtls.go +++ b/transport/vless/xtls.go @@ -2,6 +2,7 @@ package vless import ( "context" + "errors" "net" tlsC "github.com/Dreamacro/clash/component/tls" @@ -9,6 +10,10 @@ import ( xtls "github.com/xtls/go" ) +var ( + ErrNotTLS13 = errors.New("XTLS Vision based on TLS 1.3 outer connection") +) + type XTLSConfig struct { Host string SkipCertVerify bool diff --git a/transport/vmess/conn.go b/transport/vmess/conn.go index cc3155ee42..292137abc3 100644 --- a/transport/vmess/conn.go +++ b/transport/vmess/conn.go @@ -11,17 +11,13 @@ import ( "errors" "hash/fnv" "io" - "math/rand" "net" "time" + "github.com/zhangyunhao116/fastrand" "golang.org/x/crypto/chacha20poly1305" ) -func init() { - rand.Seed(time.Now().UnixNano()) -} - // Conn wrapper a net.Conn with vmess protocol type Conn struct { net.Conn @@ -76,7 +72,7 @@ func (vc *Conn) sendRequest() error { buf.WriteByte(vc.respV) buf.WriteByte(OptionChunkStream) - p := rand.Intn(16) + p := fastrand.Intn(16) // P Sec Reserve Cmd buf.WriteByte(byte(p<<4) | byte(vc.security)) buf.WriteByte(0) @@ -94,7 +90,7 @@ func (vc *Conn) sendRequest() error { // padding if p > 0 { padding := make([]byte, p) - rand.Read(padding) + fastrand.Read(padding) buf.Write(padding) } @@ -200,7 +196,7 @@ func hashTimestamp(t time.Time) []byte { // newConn return a Conn instance func newConn(conn net.Conn, id *ID, dst *DstAddr, security Security, isAead bool) (*Conn, error) { randBytes := make([]byte, 33) - rand.Read(randBytes) + fastrand.Read(randBytes) reqBodyIV := make([]byte, 16) reqBodyKey := make([]byte, 16) copy(reqBodyIV[:], randBytes[:16]) diff --git a/transport/vmess/h2.go b/transport/vmess/h2.go index d4e81607c7..6901f61e16 100644 --- a/transport/vmess/h2.go +++ b/transport/vmess/h2.go @@ -2,11 +2,11 @@ package vmess import ( "io" - "math/rand" "net" "net/http" "net/url" + "github.com/zhangyunhao116/fastrand" "golang.org/x/net/http2" ) @@ -26,7 +26,7 @@ type H2Config struct { func (hc *h2Conn) establishConn() error { preader, pwriter := io.Pipe() - host := hc.cfg.Hosts[rand.Intn(len(hc.cfg.Hosts))] + host := hc.cfg.Hosts[fastrand.Intn(len(hc.cfg.Hosts))] path := hc.cfg.Path // TODO: connect use VMess Host instead of H2 Host req := http.Request{ diff --git a/transport/vmess/http.go b/transport/vmess/http.go index 1c09e215a8..c4f27c4cfa 100644 --- a/transport/vmess/http.go +++ b/transport/vmess/http.go @@ -4,10 +4,11 @@ import ( "bufio" "bytes" "fmt" - "math/rand" "net" "net/http" "net/textproto" + + "github.com/zhangyunhao116/fastrand" ) type httpConn struct { @@ -51,16 +52,16 @@ func (hc *httpConn) Write(b []byte) (int, error) { return hc.Conn.Write(b) } - path := hc.cfg.Path[rand.Intn(len(hc.cfg.Path))] + path := hc.cfg.Path[fastrand.Intn(len(hc.cfg.Path))] host := hc.cfg.Host if header := hc.cfg.Headers["Host"]; len(header) != 0 { - host = header[rand.Intn(len(header))] + host = header[fastrand.Intn(len(header))] } u := fmt.Sprintf("http://%s%s", host, path) req, _ := http.NewRequest("GET", u, bytes.NewBuffer(b)) for key, list := range hc.cfg.Headers { - req.Header.Set(key, list[rand.Intn(len(list))]) + req.Header.Set(key, list[fastrand.Intn(len(list))]) } req.ContentLength = int64(len(b)) if err := req.Write(hc.Conn); err != nil { diff --git a/transport/vmess/tls.go b/transport/vmess/tls.go index 711c342d0e..f020d2732a 100644 --- a/transport/vmess/tls.go +++ b/transport/vmess/tls.go @@ -3,6 +3,7 @@ package vmess import ( "context" "crypto/tls" + "errors" "net" tlsC "github.com/Dreamacro/clash/component/tls" @@ -15,6 +16,7 @@ type TLSConfig struct { FingerPrint string ClientFingerprint string NextProtos []string + Reality *tlsC.RealityConfig } func StreamTLSConn(conn net.Conn, cfg *TLSConfig) (net.Conn, error) { @@ -34,15 +36,25 @@ func StreamTLSConn(conn net.Conn, cfg *TLSConfig) (net.Conn, error) { } if len(cfg.ClientFingerprint) != 0 { - utlsConn, valid := GetUtlsConnWithClientFingerprint(conn, cfg.ClientFingerprint, tlsConfig) - if valid { + if cfg.Reality == nil { + utlsConn, valid := GetUTLSConn(conn, cfg.ClientFingerprint, tlsConfig) + if valid { + ctx, cancel := context.WithTimeout(context.Background(), C.DefaultTLSTimeout) + defer cancel() + + err := utlsConn.(*tlsC.UConn).HandshakeContext(ctx) + return utlsConn, err + } + } else { ctx, cancel := context.WithTimeout(context.Background(), C.DefaultTLSTimeout) defer cancel() - - err := utlsConn.(*tlsC.UConn).HandshakeContext(ctx) - return utlsConn, err + return tlsC.GetRealityConn(ctx, conn, cfg.ClientFingerprint, tlsConfig, cfg.Reality) } } + if cfg.Reality != nil { + return nil, errors.New("REALITY is based on uTLS, please set a client-fingerprint") + } + tlsConn := tls.Client(conn, tlsConfig) ctx, cancel := context.WithTimeout(context.Background(), C.DefaultTLSTimeout) @@ -52,7 +64,7 @@ func StreamTLSConn(conn net.Conn, cfg *TLSConfig) (net.Conn, error) { return tlsConn, err } -func GetUtlsConnWithClientFingerprint(conn net.Conn, ClientFingerprint string, tlsConfig *tls.Config) (net.Conn, bool) { +func GetUTLSConn(conn net.Conn, ClientFingerprint string, tlsConfig *tls.Config) (net.Conn, bool) { if fingerprint, exists := tlsC.GetFingerprint(ClientFingerprint); exists { utlsConn := tlsC.UClient(conn, tlsConfig, fingerprint) diff --git a/transport/vmess/vmess.go b/transport/vmess/vmess.go index d7c8edb467..ee7ce12137 100644 --- a/transport/vmess/vmess.go +++ b/transport/vmess/vmess.go @@ -2,12 +2,13 @@ package vmess import ( "fmt" - "github.com/Dreamacro/clash/common/utils" - "math/rand" "net" "runtime" + "github.com/Dreamacro/clash/common/utils" + "github.com/gofrs/uuid" + "github.com/zhangyunhao116/fastrand" ) // Version of vmess @@ -77,7 +78,7 @@ type Config struct { // StreamConn return a Conn with net.Conn and DstAddr func (c *Client) StreamConn(conn net.Conn, dst *DstAddr) (net.Conn, error) { - r := rand.Intn(len(c.user)) + r := fastrand.Intn(len(c.user)) return newConn(conn, c.user[r], dst, c.security, c.isAead) } diff --git a/transport/vmess/websocket.go b/transport/vmess/websocket.go index 71dabbdd6d..5fcaa0b89d 100644 --- a/transport/vmess/websocket.go +++ b/transport/vmess/websocket.go @@ -8,9 +8,7 @@ import ( "encoding/binary" "errors" "fmt" - "io" - "math/rand" "net" "net/http" "net/url" @@ -22,7 +20,9 @@ import ( "github.com/Dreamacro/clash/common/buf" N "github.com/Dreamacro/clash/common/net" tlsC "github.com/Dreamacro/clash/component/tls" + "github.com/gorilla/websocket" + "github.com/zhangyunhao116/fastrand" ) type websocketConn struct { @@ -120,7 +120,7 @@ func (wsc *websocketConn) WriteBuffer(buffer *buf.Buffer) error { binary.BigEndian.PutUint64(header[2:], uint64(dataLen)) } - maskKey := rand.Uint32() + maskKey := fastrand.Uint32() binary.LittleEndian.PutUint32(header[1+payloadBitLength:], maskKey) N.MaskWebSocket(maskKey, data) @@ -301,15 +301,27 @@ func (wsedc *websocketWithEarlyDataConn) SetWriteDeadline(t time.Time) error { return wsedc.Conn.SetWriteDeadline(t) } -func (wsedc *websocketWithEarlyDataConn) LazyHeadroom() bool { - return wsedc.Conn == nil +func (wsedc *websocketWithEarlyDataConn) FrontHeadroom() int { + return 14 } func (wsedc *websocketWithEarlyDataConn) Upstream() any { - if wsedc.Conn == nil { // ensure return a nil interface not an interface with nil value - return nil - } - return wsedc.Conn + return wsedc.underlay +} + +//func (wsedc *websocketWithEarlyDataConn) LazyHeadroom() bool { +// return wsedc.Conn == nil +//} +// +//func (wsedc *websocketWithEarlyDataConn) Upstream() any { +// if wsedc.Conn == nil { // ensure return a nil interface not an interface with nil value +// return nil +// } +// return wsedc.Conn +//} + +func (wsedc *websocketWithEarlyDataConn) NeedHandshake() bool { + return wsedc.Conn == nil } func streamWebsocketWithEarlyDataConn(conn net.Conn, c *WebsocketConfig) (net.Conn, error) { diff --git a/tunnel/connection.go b/tunnel/connection.go index bd8d1b6348..e21bbdbf0f 100644 --- a/tunnel/connection.go +++ b/tunnel/connection.go @@ -46,7 +46,8 @@ func handleUDPToLocal(packet C.UDPPacket, pc net.PacketConn, key string, oAddr, } fromUDPAddr := from.(*net.UDPAddr) - fromUDPAddr = &(*fromUDPAddr) // make a copy + _fromUDPAddr := *fromUDPAddr + fromUDPAddr = &_fromUDPAddr // make a copy if fromAddr, ok := netip.AddrFromSlice(fromUDPAddr.IP); ok { if fAddr.IsValid() && (oAddr.Unmap() == fromAddr.Unmap()) { fromUDPAddr.IP = fAddr.Unmap().AsSlice() diff --git a/tunnel/statistic/tracker.go b/tunnel/statistic/tracker.go index 97dd731630..f7e9d97176 100644 --- a/tunnel/statistic/tracker.go +++ b/tunnel/statistic/tracker.go @@ -6,6 +6,7 @@ import ( "github.com/Dreamacro/clash/common/buf" N "github.com/Dreamacro/clash/common/net" + "github.com/Dreamacro/clash/common/utils" C "github.com/Dreamacro/clash/constant" "github.com/gofrs/uuid" @@ -81,8 +82,7 @@ func (tt *tcpTracker) Upstream() any { return tt.Conn } -func NewTCPTracker(conn C.Conn, manager *Manager, metadata *C.Metadata, rule C.Rule) *tcpTracker { - uuid, _ := uuid.NewV4() +func NewTCPTracker(conn C.Conn, manager *Manager, metadata *C.Metadata, rule C.Rule, uploadTotal int64, downloadTotal int64) *tcpTracker { if conn != nil { if tcpAddr, ok := conn.RemoteAddr().(*net.TCPAddr); ok { metadata.RemoteDst = tcpAddr.IP.String() @@ -95,13 +95,13 @@ func NewTCPTracker(conn C.Conn, manager *Manager, metadata *C.Metadata, rule C.R Conn: conn, manager: manager, trackerInfo: &trackerInfo{ - UUID: uuid, + UUID: utils.NewUUIDV4(), Start: time.Now(), Metadata: metadata, Chain: conn.Chains(), Rule: "", - UploadTotal: atomic.NewInt64(0), - DownloadTotal: atomic.NewInt64(0), + UploadTotal: atomic.NewInt64(uploadTotal), + DownloadTotal: atomic.NewInt64(downloadTotal), }, extendedReader: N.NewExtendedReader(conn), extendedWriter: N.NewExtendedWriter(conn), @@ -147,21 +147,20 @@ func (ut *udpTracker) Close() error { return ut.PacketConn.Close() } -func NewUDPTracker(conn C.PacketConn, manager *Manager, metadata *C.Metadata, rule C.Rule) *udpTracker { - uuid, _ := uuid.NewV4() +func NewUDPTracker(conn C.PacketConn, manager *Manager, metadata *C.Metadata, rule C.Rule, uploadTotal int64, downloadTotal int64) *udpTracker { metadata.RemoteDst = conn.RemoteDestination() ut := &udpTracker{ PacketConn: conn, manager: manager, trackerInfo: &trackerInfo{ - UUID: uuid, + UUID: utils.NewUUIDV4(), Start: time.Now(), Metadata: metadata, Chain: conn.Chains(), Rule: "", - UploadTotal: atomic.NewInt64(0), - DownloadTotal: atomic.NewInt64(0), + UploadTotal: atomic.NewInt64(uploadTotal), + DownloadTotal: atomic.NewInt64(downloadTotal), }, } diff --git a/tunnel/status.go b/tunnel/status.go new file mode 100644 index 0000000000..d81dd45e8f --- /dev/null +++ b/tunnel/status.go @@ -0,0 +1,92 @@ +package tunnel + +import ( + "encoding/json" + "errors" + "strings" + "sync/atomic" +) + +type TunnelStatus int + +// StatusMapping is a mapping for Status enum +var StatusMapping = map[string]TunnelStatus{ + Suspend.String(): Suspend, + Inner.String(): Inner, + Running.String(): Running, +} + +const ( + Suspend TunnelStatus = iota + Inner + Running +) + +// UnmarshalJSON unserialize Status +func (s *TunnelStatus) UnmarshalJSON(data []byte) error { + var tp string + json.Unmarshal(data, &tp) + status, exist := StatusMapping[strings.ToLower(tp)] + if !exist { + return errors.New("invalid mode") + } + *s = status + return nil +} + +// UnmarshalYAML unserialize Status with yaml +func (s *TunnelStatus) UnmarshalYAML(unmarshal func(any) error) error { + var tp string + unmarshal(&tp) + status, exist := StatusMapping[strings.ToLower(tp)] + if !exist { + return errors.New("invalid status") + } + *s = status + return nil +} + +// MarshalJSON serialize Status +func (s TunnelStatus) MarshalJSON() ([]byte, error) { + return json.Marshal(s.String()) +} + +// MarshalYAML serialize TunnelMode with yaml +func (s TunnelStatus) MarshalYAML() (any, error) { + return s.String(), nil +} + +func (s TunnelStatus) String() string { + switch s { + case Suspend: + return "suspend" + case Inner: + return "inner" + case Running: + return "running" + default: + return "Unknown" + } +} + +type AtomicStatus struct { + value atomic.Int32 +} + +func (a *AtomicStatus) Store(s TunnelStatus) { + a.value.Store(int32(s)) +} + +func (a *AtomicStatus) Load() TunnelStatus { + return TunnelStatus(a.value.Load()) +} + +func (a *AtomicStatus) String() string { + return a.Load().String() +} + +func newAtomicStatus(s TunnelStatus) *AtomicStatus { + a := &AtomicStatus{} + a.Store(s) + return a +} diff --git a/tunnel/tunnel.go b/tunnel/tunnel.go index 695f294505..c4f55fbdad 100644 --- a/tunnel/tunnel.go +++ b/tunnel/tunnel.go @@ -13,6 +13,7 @@ import ( "github.com/jpillora/backoff" + N "github.com/Dreamacro/clash/common/net" "github.com/Dreamacro/clash/component/nat" P "github.com/Dreamacro/clash/component/process" "github.com/Dreamacro/clash/component/resolver" @@ -25,6 +26,7 @@ import ( ) var ( + status = newAtomicStatus(Suspend) tcpQueue = make(chan C.ConnContext, 200) udpQueue = make(chan C.PacketAdapter, 200) natTable = nat.New() @@ -48,6 +50,22 @@ var ( fakeIPRange netip.Prefix ) +func OnSuspend() { + status.Store(Suspend) +} + +func OnInnerLoading() { + status.Store(Inner) +} + +func OnRunning() { + status.Store(Running) +} + +func Status() TunnelStatus { + return status.Load() +} + func SetFakeIPRange(p netip.Prefix) { fakeIPRange = p } @@ -157,10 +175,19 @@ func SetFindProcessMode(mode P.FindProcessMode) { findProcessMode = mode } +func isHandle(t C.Type) bool { + status := status.Load() + return status == Running || (status == Inner && t == C.INNER) +} + // processUDP starts a loop to handle udp packet func processUDP() { queue := udpQueue for conn := range queue { + if !isHandle(conn.Metadata().Type) { + conn.Drop() + continue + } handleUDPConn(conn) } } @@ -176,6 +203,10 @@ func process() { queue := tcpQueue for conn := range queue { + if !isHandle(conn.Metadata().Type) { + _ = conn.Conn().Close() + continue + } go handleTCPConn(conn) } } @@ -200,13 +231,18 @@ func preHandleMetadata(metadata *C.Metadata) error { if resolver.FakeIPEnabled() { metadata.DstIP = netip.Addr{} metadata.DNSMode = C.DNSFakeIP - } else if node := resolver.DefaultHosts.Search(host); node != nil { + } else if node, ok := resolver.DefaultHosts.Search(host, false); ok { // redir-host should lookup the hosts - metadata.DstIP = node.Data() + metadata.DstIP, _ = node.RandIP() + } else if node != nil && node.IsDomain { + metadata.Host = node.Domain } } else if resolver.IsFakeIP(metadata.DstIP) { return fmt.Errorf("fake DNS record %s missing", metadata.DstIP) } + } else if node, ok := resolver.DefaultHosts.Search(metadata.Host, true); ok { + // try use domain mapping + metadata.Host = node.Domain } return nil @@ -322,7 +358,7 @@ func handleUDPConn(packet C.PacketAdapter) { } pCtx.InjectPacketConn(rawPc) - pc := statistic.NewUDPTracker(rawPc, statistic.DefaultManager, metadata, rule) + pc := statistic.NewUDPTracker(rawPc, statistic.DefaultManager, metadata, rule, 0, 0) switch true { case metadata.SpecialProxy != "": @@ -366,8 +402,21 @@ func handleTCPConn(connCtx C.ConnContext) { return } + conn := connCtx.Conn() + conn.ResetPeeked() // reset before sniffer if sniffer.Dispatcher.Enable() && sniffingEnable { - sniffer.Dispatcher.TCPSniff(connCtx.Conn(), metadata) + sniffer.Dispatcher.TCPSniff(conn, metadata) + } + + peekMutex := sync.Mutex{} + if !conn.Peeked() { + peekMutex.Lock() + go func() { + defer peekMutex.Unlock() + _ = conn.SetReadDeadline(time.Now().Add(200 * time.Millisecond)) + _, _ = conn.Peek(1) + _ = conn.SetReadDeadline(time.Time{}) + }() } proxy, rule, err := resolveMetadata(connCtx, metadata) @@ -378,8 +427,8 @@ func handleTCPConn(connCtx C.ConnContext) { dialMetadata := metadata if len(metadata.Host) > 0 { - if node := resolver.DefaultHosts.Search(metadata.Host); node != nil { - if dstIp := node.Data(); !FakeIPRange().Contains(dstIp) { + if node, ok := resolver.DefaultHosts.Search(metadata.Host, false); ok { + if dstIp, _ := node.RandIP(); !FakeIPRange().Contains(dstIp) { dialMetadata.DstIP = dstIp dialMetadata.DNSMode = C.DNSHosts dialMetadata = dialMetadata.Pure() @@ -387,10 +436,41 @@ func handleTCPConn(connCtx C.ConnContext) { } } + var peekBytes []byte + var peekLen int + ctx, cancel := context.WithTimeout(context.Background(), C.DefaultTCPTimeout) defer cancel() - remoteConn, err := retry(ctx, func(ctx context.Context) (C.Conn, error) { - return proxy.DialContext(ctx, dialMetadata) + remoteConn, err := retry(ctx, func(ctx context.Context) (remoteConn C.Conn, err error) { + remoteConn, err = proxy.DialContext(ctx, dialMetadata) + if err != nil { + return + } + + if N.NeedHandshake(remoteConn) { + defer func() { + for _, chain := range remoteConn.Chains() { + if chain == "REJECT" { + err = nil + return + } + } + if err != nil { + remoteConn = nil + } + }() + peekMutex.Lock() + defer peekMutex.Unlock() + peekBytes, _ = conn.Peek(conn.Buffered()) + _, err = remoteConn.Write(peekBytes) + if err != nil { + return + } + if peekLen = len(peekBytes); peekLen > 0 { + _, _ = conn.Discard(peekLen) + } + } + return }, func(err error) { if rule == nil { log.Warnln( @@ -408,7 +488,7 @@ func handleTCPConn(connCtx C.ConnContext) { return } - remoteConn = statistic.NewTCPTracker(remoteConn, statistic.DefaultManager, metadata, rule) + remoteConn = statistic.NewTCPTracker(remoteConn, statistic.DefaultManager, metadata, rule, 0, int64(peekLen)) defer func(remoteConn C.Conn) { _ = remoteConn.Close() }(remoteConn) @@ -434,6 +514,10 @@ func handleTCPConn(connCtx C.ConnContext) { ) } + _ = conn.SetReadDeadline(time.Now()) // stop unfinished peek + peekMutex.Lock() + defer peekMutex.Unlock() + _ = conn.SetReadDeadline(time.Time{}) // reset handleSocket(connCtx, remoteConn) } @@ -449,8 +533,8 @@ func match(metadata *C.Metadata) (C.Proxy, C.Rule, error) { processFound bool ) - if node := resolver.DefaultHosts.Search(metadata.Host); node != nil { - metadata.DstIP = node.Data() + if node, ok := resolver.DefaultHosts.Search(metadata.Host, false); ok { + metadata.DstIP, _ = node.RandIP() resolved = true }