diff --git a/adapter/outbound/reality.go b/adapter/outbound/reality.go new file mode 100644 index 00000000..23314e5f --- /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/trojan.go b/adapter/outbound/trojan.go index beedd614..030e74a9 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) } @@ -259,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()...) @@ -285,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/vless.go b/adapter/outbound/vless.go index 010af23c..757661cc 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 @@ -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 { @@ -479,7 +483,10 @@ func NewVless(option VlessOption) (*Vless, error) { if option.Network != "ws" && len(option.Flow) >= 16 { option.Flow = option.Flow[:16] switch option.Flow { - case vless.XRO, vless.XRD, vless.XRS, vless.XRV: + 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, } @@ -519,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 { @@ -553,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 25ffebf3..5bb46dad 100644 --- a/adapter/outbound/vmess.go +++ b/adapter/outbound/vmess.go @@ -35,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 { @@ -95,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, @@ -144,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 @@ -172,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 != "" { @@ -190,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 { @@ -199,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 != "" { @@ -451,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 e5d7cf3f..7eae30fc 100644 --- a/adapter/outbound/wireguard.go +++ b/adapter/outbound/wireguard.go @@ -77,7 +77,7 @@ type wgNetDialer struct { 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)) + return d.tunDevice.DialContext(ctx, network, M.ParseSocksaddr(address).Unwrap()) } func NewWireGuard(option WireGuardOption) (*WireGuard, error) { @@ -221,11 +221,12 @@ func (w *WireGuard) DialContext(ctx context.Context, metadata *C.Metadata, opts return nil, w.startErr } if !metadata.Resolved() { + 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 @@ -257,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/loadbalance.go b/adapter/outboundgroup/loadbalance.go index 7c7edd36..1ed80496 100644 --- a/adapter/outboundgroup/loadbalance.go +++ b/adapter/outboundgroup/loadbalance.go @@ -131,21 +131,23 @@ func strategyRoundRobin() strategyFn { idx := 0 idxMutex := sync.Mutex{} return func(proxies []C.Proxy, metadata *C.Metadata, touch bool) C.Proxy { - id := idx // value could be wrong due to no lock, but don't care if we don't touch + idxMutex.Lock() + defer idxMutex.Unlock() + + i := 0 + length := len(proxies) + if touch { - idxMutex.Lock() - defer idxMutex.Unlock() - id = idx // get again by lock's protect, so it must be right defer func() { - idx = id + idx = (idx + i) % length }() } - length := len(proxies) - for i := 0; i < length; i++ { - id = (id + 1) % length + for ; i < length; i++ { + id := (idx + i) % length proxy := proxies[id] if proxy.Alive() { + i++ return proxy } } @@ -216,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, true) + return lb.strategyFn(proxies, metadata, touch) } // MarshalJSON implements C.ProxyAdapter diff --git a/common/convert/converter.go b/common/convert/converter.go index 7d896d53..abd07a94 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/utils/must.go b/common/utils/must.go new file mode 100644 index 00000000..2dd5ff5e --- /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 00000000..1b0fa494 --- /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/component/dialer/dialer.go b/component/dialer/dialer.go index 025f7034..d70e9173 100644 --- a/component/dialer/dialer.go +++ b/component/dialer/dialer.go @@ -116,7 +116,8 @@ func dialContext(ctx context.Context, network string, destination netip.Addr, po case nil: netDialer = &net.Dialer{} case *net.Dialer: - netDialer = &*netDialer.(*net.Dialer) // make a copy + _netDialer := *netDialer.(*net.Dialer) + netDialer = &_netDialer // make a copy default: return netDialer.DialContext(ctx, network, address) } @@ -170,7 +171,7 @@ func dualStackDialContext(ctx context.Context, dialFn dialFunc, network string, select { case results <- result: case <-returned: - if result.Conn != nil { + if result.Conn != nil && result.error == nil { _ = result.Conn.Close() } } @@ -180,29 +181,33 @@ func dualStackDialContext(ctx context.Context, dialFn dialFunc, network string, go racer(ipv4s, preferIPVersion != 6) go racer(ipv6s, preferIPVersion != 4) var fallback dialResult - var err error - for { + var errs []error + for i := 0; i < 2; { select { - case <-ctx.Done(): - if fallback.error == nil && fallback.Conn != nil { - return fallback.Conn, nil - } - return nil, fmt.Errorf("dual stack connect failed: %w", err) case <-fallbackTicker.C: if fallback.error == nil && fallback.Conn != nil { return fallback.Conn, nil } case res := <-results: + i++ if res.error == nil { if res.isPrimary { return res.Conn, nil } fallback = res } else { - err = res.error + if res.isPrimary { + errs = append([]error{fmt.Errorf("connect failed: %w", res.error)}, errs...) + } else { + errs = append(errs, fmt.Errorf("connect failed: %w", res.error)) + } } } } + if fallback.error == nil && fallback.Conn != nil { + return fallback.Conn, nil + } + return nil, errorsJoin(errs...) } func parallelDialContext(ctx context.Context, network string, ips []netip.Addr, port string, opt *option) (net.Conn, error) { @@ -213,41 +218,35 @@ func parallelDialContext(ctx context.Context, network string, ips []netip.Addr, returned := make(chan struct{}) defer close(returned) racer := func(ctx context.Context, ip netip.Addr) { - result := dialResult{isPrimary: true} + 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() } } }() - result.ip = ip result.Conn, result.error = dialContext(ctx, network, ip, port, opt) } for _, ip := range ips { go racer(ctx, ip) } - var err error - for { - select { - case <-ctx.Done(): - if err != nil { - return nil, err - } - if ctx.Err() == context.DeadlineExceeded { - return nil, os.ErrDeadlineExceeded - } - return nil, ctx.Err() - case res := <-results: - if res.error == nil { - return res.Conn, nil - } - err = res.error + 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 len(errs) > 0 { + return nil, errorsJoin(errs...) + } + return nil, os.ErrDeadlineExceeded } func serialDialContext(ctx context.Context, network string, ips []netip.Addr, port string, opt *option) (net.Conn, error) { @@ -302,13 +301,18 @@ func parseAddr(ctx context.Context, network, address string, preferResolver reso if err != nil { return nil, "-1", fmt.Errorf("dns resolve failed: %w", err) } + for i, ip := range ips { + if ip.Is4In6() { + ips[i] = ip.Unmap() + } + } return ips, port, nil } func sortationAddr(ips []netip.Addr) (ipv4s, ipv6s []netip.Addr) { for _, v := range ips { - if v.Is4() || v.Is4In6() { - ipv4s = append(ipv4s, v.Unmap()) + if v.Is4() { // 4in6 parse was in parseAddr + ipv4s = append(ipv4s, v) } else { ipv6s = append(ipv6s, v) } diff --git a/component/resolver/host.go b/component/resolver/host.go new file mode 100644 index 00000000..3b7e9a37 --- /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 6ae2d7c2..f5872ad7 100644 --- a/component/resolver/resolver.go +++ b/component/resolver/resolver.go @@ -9,6 +9,7 @@ import ( "strings" "time" + "github.com/Dreamacro/clash/common/utils" "github.com/Dreamacro/clash/component/trie" "github.com/miekg/dns" @@ -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 } } @@ -106,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 } } @@ -155,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 { diff --git a/component/tls/reality.go b/component/tls/reality.go new file mode 100644 index 00000000..a060de92 --- /dev/null +++ b/component/tls/reality.go @@ -0,0 +1,169 @@ +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, + NextProtos: tlsConfig.NextProtos, + 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 + hello.SessionId = make([]byte, 32) + copy(hello.Raw[39:], hello.SessionId) + + var nowTime time.Time + if uConfig.Time != nil { + nowTime = uConfig.Time() + } else { + nowTime = time.Now() + } + binary.BigEndian.PutUint64(hello.SessionId, uint64(nowTime.Unix())) + + hello.SessionId[0] = 1 + hello.SessionId[1] = 7 + hello.SessionId[2] = 5 + 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))) + 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 a7189aa8..e08ca7ee 100644 --- a/component/tls/utls.go +++ b/component/tls/utls.go @@ -89,6 +89,7 @@ func copyConfig(c *tls.Config) *utls.Config { return &utls.Config{ RootCAs: c.RootCAs, ServerName: c.ServerName, + NextProtos: c.NextProtos, InsecureSkipVerify: c.InsecureSkipVerify, VerifyPeerCertificate: c.VerifyPeerCertificate, } diff --git a/config/config.go b/config/config.go index 76e5491b..dfcb772e 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 } @@ -153,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 @@ -171,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"` @@ -263,7 +265,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"` @@ -337,7 +339,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{}, @@ -377,6 +379,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{ @@ -415,9 +418,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", }, } @@ -443,6 +446,11 @@ func ParseRawConfig(rawCfg *RawConfig) (*Config, error) { } config.General = general + if len(config.General.GlobalClientFingerprint) != 0 { + log.Debugln("GlobalClientFingerprint:%s", config.General.GlobalClientFingerprint) + tlsC.SetGlobalUtlsClient(config.General.GlobalClientFingerprint) + } + dialer.DefaultInterface.Store(config.General.Interface) proxies, providers, err := parseProxies(rawCfg) if err != nil { @@ -518,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 } @@ -824,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) - if err != nil { - return nil, fmt.Errorf("%s is not a valid IP", 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 + } } - _ = tree.Insert(domain, ip) + value, err := resolver.NewHostValue(anyValue) + if err != nil { + return nil, fmt.Errorf("%s is not a valid value", anyValue) + } + 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() @@ -956,26 +985,39 @@ func parsePureDNSServer(server string) string { } func parseNameServerPolicy(nsPolicy map[string]any, preferH3 bool) (map[string][]dns.NameServer, error) { policy := map[string][]dns.NameServer{} + updatedPolicy := make(map[string]interface{}) + re := regexp.MustCompile(`[a-zA-Z0-9\-]+\.[a-zA-Z]{2,}(\.[a-zA-Z]{2,})?`) - 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))) + for k, v := range nsPolicy { + if strings.Contains(k, "geosite:") { + subkeys := strings.Split(k, ":") + subkeys = subkeys[1:] + subkeys = strings.Split(subkeys[0], ",") + //log.Infoln("subkeys:%+v", subkeys) + for _, subkey := range subkeys { + newKey := "geosite:" + subkey + //log.Infoln("newKey:%+v", newKey) + updatedPolicy[newKey] = 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 if re.MatchString(k) { + subkeys := strings.Split(k, ",") + //log.Infoln("subkeys:%+v", subkeys) + for _, subkey := range subkeys { + updatedPolicy[subkey] = v + } + } else { + updatedPolicy[k] = v } + } + //log.Infoln("updatedPolicy:%+v", updatedPolicy) + + 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 } @@ -1038,7 +1080,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") @@ -1048,6 +1090,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{ @@ -1255,8 +1298,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/dns/middleware.go b/dns/middleware.go index 7dc9622d..f2dd9c96 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 59b1ee06..57f581a5 100644 --- a/dns/resolver.go +++ b/dns/resolver.go @@ -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 } @@ -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: old.policy, + ipv6Timeout: old.ipv6Timeout, } return r } diff --git a/dns/util.go b/dns/util.go index 203ab615..4821195d 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/docs/config.yaml b/docs/config.yaml index da01725e..2ae4c02f 100644 --- a/docs/config.yaml +++ b/docs/config.yaml @@ -58,11 +58,13 @@ 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 选择记录 +profile: # 存储 select 选择记录 store-selected: false - + # 持久化 fake-ip store-fake-ip: true @@ -167,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: @@ -229,12 +231,13 @@ dns: # - '+.youtube.com' # 配置查询域名使用的 DNS 服务器 - nameserver-policy: # 'www.baidu.com': '114.114.114.114' + nameserver-policy: + # 'www.baidu.com': '114.114.114.114' # '+.internal.crop.com': '10.0.0.1' - "geosite:cn": + "geosite:cn,private,apple": - https://doh.pub/dns-query - https://dns.alidns.com/dns-query - "www.baidu.com": [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" @@ -273,7 +276,7 @@ proxies: # socks5 # obfs-opts: # mode: http # or tls # host: bing.com - + # Shadowsocks # cipher支持: # aes-128-gcm aes-192-gcm aes-256-gcm @@ -296,7 +299,7 @@ proxies: # socks5 # UDP 则为双栈解析,获取结果中的第一个 IPv4 # ipv6-prefer 同 ipv4-prefer # 现有协议都支持此参数,TCP 效果仅在开启 tcp-concurrent 生效 - + - name: "ss2" type: ss server: server @@ -472,12 +475,44 @@ proxies: # socks5 network: tcp tls: true udp: true - xudp: true - flow: xtls-rprx-vision # xtls-rprx-origin # enable XTLS + flow: xtls-rprx-vision client-fingerprint: chrome # fingerprint: xxxx # skip-cert-verify: true + - name: "vless-reality-vision" + type: vless + server: server + port: 443 + 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 + 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 @@ -549,7 +584,7 @@ proxies: # socks5 # sni: example.com # aka server name # skip-cert-verify: true # fingerprint: xxxx - + #hysteria - name: "hysteria" type: hysteria @@ -609,7 +644,7 @@ proxies: # socks5 # 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: @@ -738,7 +773,7 @@ rules: - IP-CIDR,1.1.1.1/32,ss1 - IP-CIDR6,2409::/64,DIRECT # 当满足条件是 TCP 或 UDP 流量时,使用名为 sub-rule-name1 的规则集 - - SUB-RULE,(OR,((NETWORK,TCP),(NETWORK,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) @@ -911,4 +946,4 @@ listeners: # authentication-timeout: 1000 # alpn: # - h3 -# max-udp-relay-packet-size: 1500 \ No newline at end of file +# max-udp-relay-packet-size: 1500 diff --git a/go.mod b/go.mod index b1e7d2f9..28760b44 100644 --- a/go.mod +++ b/go.mod @@ -18,19 +18,19 @@ 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/quic-go v0.33.1 github.com/metacubex/sing-shadowsocks v0.1.1-0.20230226153717-4e80da7e6947 github.com/metacubex/sing-tun v0.1.1-0.20230304153753-5058534177f3 - github.com/metacubex/sing-wireguard v0.0.0-20230213124601-d04406a109b4 + github.com/metacubex/sing-wireguard v0.0.0-20230310035749-f7595fcae5cb github.com/miekg/dns v1.1.50 github.com/mroth/weightedrand/v2 v2.0.0 github.com/oschwald/geoip2-golang v1.8.0 github.com/sagernet/netlink v0.0.0-20220905062125-8043b4a9aa97 - github.com/sagernet/sing v0.1.8-0.20230303052048-c875a4ffab1a + github.com/sagernet/sing v0.1.8 github.com/sagernet/sing-shadowtls v0.1.0 - github.com/sagernet/sing-vmess v0.1.3-0.20230303082804-627cc46ae68b + github.com/sagernet/sing-vmess v0.1.3-0.20230307060529-d110e81a50bc github.com/sagernet/tfo-go v0.0.0-20230207095944-549363a7327d - github.com/sagernet/utls v0.0.0-20230220130002-c08891932056 + 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 @@ -70,9 +70,8 @@ require ( 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/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 diff --git a/go.sum b/go.sum index ff456cb4..f9e584a2 100644 --- a/go.sum +++ b/go.sum @@ -91,14 +91,14 @@ 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-20230304153416-e2bb9c726005 h1:0TEvReK/D6YLszjGj/bdx4d7amQSjQ2X/98r4ZiUbxU= github.com/metacubex/gvisor v0.0.0-20230304153416-e2bb9c726005/go.mod h1:wqEuzdImyqD2MCGE8CYRJXbB77oSEJeoSSXXdwKjnsE= -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/quic-go v0.33.1 h1:ZIxZFGivpSLOEZuuNkLy+aPvo1RP4uRBjNg3SAkXwIg= +github.com/metacubex/quic-go v0.33.1/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.1-0.20230304153753-5058534177f3 h1:oQLThm1a8E7hHmoM9XF2cO0FZPsHVynC4YXW4b3liUI= github.com/metacubex/sing-tun v0.1.1-0.20230304153753-5058534177f3/go.mod h1:b/19bRRhwampNPV+1gVDyDsQHmuGDaplxPQkwJh1kj4= -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/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= @@ -117,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/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/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.8-0.20230303052048-c875a4ffab1a h1:NvhI/8DMFt2yV3eoYhw6P/XyWzzIKkMiGvFglJbWHWg= -github.com/sagernet/sing v0.1.8-0.20230303052048-c875a4ffab1a/go.mod h1:jt1w2u7lJQFFSGLiRrRIs5YWmx4kAPfWuOejuDW9qMk= +github.com/sagernet/sing v0.1.8 h1:6DKo2FkSHn0nUcjO7bAext/ai7y7pCusK/+fScBJ5Jk= +github.com/sagernet/sing v0.1.8/go.mod h1:jt1w2u7lJQFFSGLiRrRIs5YWmx4kAPfWuOejuDW9qMk= 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-0.20230303082804-627cc46ae68b h1:NZeF0ATeJwe4W3gTJNeIfTB6yBxai665q1HvDOaWmmU= -github.com/sagernet/sing-vmess v0.1.3-0.20230303082804-627cc46ae68b/go.mod h1:9NSj8mZTx1JIY/HF9LaYRppUsVkysIN5tEFpNZujXxY= +github.com/sagernet/sing-vmess v0.1.3-0.20230307060529-d110e81a50bc h1:vqlYWupvVDRpvv2F+RtECJN+VbuKjLtmQculQvOecls= +github.com/sagernet/sing-vmess v0.1.3-0.20230307060529-d110e81a50bc/go.mod h1:V14iffGwhZPU2S7wgIiPlLWXygSjAXazYzD1w0ejBl4= 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/utls v0.0.0-20230220130002-c08891932056 h1:gDXi/0uYe8dA48UyUI1LM2la5QYN0IvsDvR2H2+kFnA= -github.com/sagernet/utls v0.0.0-20230220130002-c08891932056/go.mod h1:JKQMZq/O2qnZjdrt+B57olmfgEmLtY9iiSIEYtWvoSM= +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= diff --git a/hub/executor/executor.go b/hub/executor/executor.go index d6ff7851..21a25ecd 100644 --- a/hub/executor/executor.go +++ b/hub/executor/executor.go @@ -190,6 +190,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, @@ -225,8 +226,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) { diff --git a/hub/hub.go b/hub/hub.go index e4c414fa..bd228fad 100644 --- a/hub/hub.go +++ b/hub/hub.go @@ -44,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.LogLevel==log.DEBUG) + cfg.General.Secret, cfg.TLS.Certificate, cfg.TLS.PrivateKey, cfg.General.LogLevel == log.DEBUG) } executor.ApplyConfig(cfg, true) diff --git a/transport/gun/gun.go b/transport/gun/gun.go index 920e7adc..ae2ea6a4 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/trojan/trojan.go b/transport/trojan/trojan.go index e336d9db..8eae8237 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/conn.go b/transport/tuic/conn.go index 7ecc3f0d..ee687425 100644 --- a/transport/tuic/conn.go +++ b/transport/tuic/conn.go @@ -200,7 +200,7 @@ 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 { + if q.udpRelayMode != "quic" && len(p) > q.maxUdpRelayPacketSize { return 0, fmt.Errorf("udp packet too large(%d > %d)", len(p), q.maxUdpRelayPacketSize) } if q.closed { @@ -215,7 +215,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 +238,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 } @@ -250,7 +250,29 @@ func (q *quicStreamPacketConn) WriteTo(p []byte, addr net.Addr) (n int, err erro } func (q *quicStreamPacketConn) LocalAddr() net.Addr { - return q.quicConn.LocalAddr() + addr := q.quicConn.LocalAddr() + if q.inputConn != nil { // client + return &packetAddr{addrStr: q.quicConn.LocalAddr().String(), connId: q.connId, rawAddr: addr} + } + return addr // server } var _ net.PacketConn = &quicStreamPacketConn{} + +type packetAddr struct { + addrStr string + connId uint32 + rawAddr net.Addr +} + +func (a packetAddr) Network() string { + return "tuic" +} + +func (a packetAddr) String() string { + return fmt.Sprintf("%s-%d", a.addrStr, a.connId) +} + +func (a packetAddr) RawAddr() net.Addr { + return a.rawAddr +} diff --git a/transport/tuic/protocol.go b/transport/tuic/protocol.go index a54c8e54..b14074a7 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 @@ -305,9 +296,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 +464,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 e8dee8d6..770e1869 100644 --- a/transport/tuic/server.go +++ b/transport/tuic/server.go @@ -5,7 +5,6 @@ import ( "bytes" "context" "crypto/tls" - "fmt" "net" "sync" "sync/atomic" @@ -76,7 +75,7 @@ func (s *Server) Close() error { type serverHandler struct { *Server - quicConn quic.Connection + quicConn quic.EarlyConnection uuid uuid.UUID authCh chan struct{} @@ -87,13 +86,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() }() @@ -103,6 +95,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) { @@ -152,14 +153,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: &packetAddr{addrStr: "tuic-" + s.uuid.String(), connId: assocId, rawAddr: s.quicConn.RemoteAddr()}, }) } -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 @@ -239,7 +236,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) @@ -274,23 +271,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/vision.go b/transport/vless/vision.go index d817c912..8dc84e40 100644 --- a/transport/vless/vision.go +++ b/transport/vless/vision.go @@ -3,6 +3,7 @@ package vless import ( "bytes" "encoding/binary" + "github.com/Dreamacro/clash/common/buf" "github.com/Dreamacro/clash/log" @@ -21,16 +22,15 @@ const ( func WriteWithPadding(buffer *buf.Buffer, p []byte, command byte, userUUID *uuid.UUID, paddingTLS bool) { contentLen := int32(len(p)) var paddingLen int32 - if contentLen < 900 && paddingTLS { - log.Debugln("long padding") - paddingLen = fastrand.Int31n(500) + 900 - contentLen - } else { - paddingLen = fastrand.Int31n(256) + if contentLen < 900 { + if paddingTLS { + //log.Debugln("long padding") + paddingLen = fastrand.Int31n(500) + 900 - contentLen + } else { + paddingLen = fastrand.Int31n(256) + } } - if paddingLen > buf.BufferSize-21-contentLen { - paddingLen = buf.BufferSize - 21 - contentLen - } - if userUUID != nil { // unnecessary, but keep the same with Xray + if userUUID != nil { buffer.Write(userUUID.Bytes()) } @@ -38,6 +38,7 @@ func WriteWithPadding(buffer *buf.Buffer, p []byte, command byte, userUUID *uuid 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) } @@ -45,21 +46,22 @@ func WriteWithPadding(buffer *buf.Buffer, p []byte, command byte, userUUID *uuid func ApplyPadding(buffer *buf.Buffer, command byte, userUUID *uuid.UUID, paddingTLS bool) { contentLen := int32(buffer.Len()) var paddingLen int32 - if contentLen < 900 && paddingTLS { - log.Debugln("long padding") - paddingLen = fastrand.Int31n(500) + 900 - contentLen - } else { - paddingLen = fastrand.Int31n(256) - } - if paddingLen > buf.BufferSize-21-contentLen { - paddingLen = buf.BufferSize - 21 - contentLen + 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 { // unnecessary, but keep the same with Xray + 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) } diff --git a/transport/vmess/tls.go b/transport/vmess/tls.go index 711c342d..f020d273 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/tunnel/connection.go b/tunnel/connection.go index bd8d1b63..e21bbdbf 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/tunnel.go b/tunnel/tunnel.go index d80893c9..b686eae6 100644 --- a/tunnel/tunnel.go +++ b/tunnel/tunnel.go @@ -201,13 +201,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 @@ -392,8 +397,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() @@ -498,8 +503,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 }