package adapter import ( "context" "encoding/json" "fmt" "github.com/Dreamacro/clash/common/queue" "github.com/Dreamacro/clash/component/dialer" C "github.com/Dreamacro/clash/constant" "net" "net/http" "net/netip" "net/url" "strings" "time" "go.uber.org/atomic" ) var UnifiedDelay = atomic.NewBool(false) type Proxy struct { C.ProxyAdapter history *queue.Queue[C.DelayHistory] alive *atomic.Bool } // Alive implements C.Proxy func (p *Proxy) Alive() bool { return p.alive.Load() } // Dial implements C.Proxy func (p *Proxy) Dial(metadata *C.Metadata) (C.Conn, error) { ctx, cancel := context.WithTimeout(context.Background(), C.DefaultTCPTimeout) defer cancel() return p.DialContext(ctx, metadata) } // DialContext implements C.ProxyAdapter func (p *Proxy) DialContext(ctx context.Context, metadata *C.Metadata, opts ...dialer.Option) (C.Conn, error) { conn, err := p.ProxyAdapter.DialContext(ctx, metadata, opts...) wasCancel := false if err != nil { wasCancel = strings.Contains(err.Error(), "operation was canceled") } p.alive.Store(err == nil || wasCancel) return conn, err } // DialUDP implements C.ProxyAdapter func (p *Proxy) DialUDP(metadata *C.Metadata) (C.PacketConn, error) { ctx, cancel := context.WithTimeout(context.Background(), C.DefaultUDPTimeout) defer cancel() return p.ListenPacketContext(ctx, metadata) } // ListenPacketContext implements C.ProxyAdapter func (p *Proxy) ListenPacketContext(ctx context.Context, metadata *C.Metadata, opts ...dialer.Option) (C.PacketConn, error) { pc, err := p.ProxyAdapter.ListenPacketContext(ctx, metadata, opts...) p.alive.Store(err == nil) return pc, err } // DelayHistory implements C.Proxy func (p *Proxy) DelayHistory() []C.DelayHistory { queueM := p.history.Copy() histories := []C.DelayHistory{} for _, item := range queueM { histories = append(histories, item) } return histories } // LastDelay return last history record. if proxy is not alive, return the max value of uint16. // implements C.Proxy func (p *Proxy) LastDelay() (delay uint16) { var max uint16 = 0xffff if !p.alive.Load() { return max } history := p.history.Last() if history.Delay == 0 { return max } return history.Delay } // MarshalJSON implements C.ProxyAdapter func (p *Proxy) MarshalJSON() ([]byte, error) { inner, err := p.ProxyAdapter.MarshalJSON() if err != nil { return inner, err } mapping := map[string]any{} _ = json.Unmarshal(inner, &mapping) mapping["history"] = p.DelayHistory() mapping["name"] = p.Name() mapping["udp"] = p.SupportUDP() return json.Marshal(mapping) } // URLTest get the delay for the specified URL // implements C.Proxy func (p *Proxy) URLTest(ctx context.Context, url string) (t uint16, err error) { defer func() { p.alive.Store(err == nil) record := C.DelayHistory{Time: time.Now()} if err == nil { record.Delay = t } p.history.Put(record) if p.history.Len() > 10 { p.history.Pop() } }() unifiedDelay := UnifiedDelay.Load() addr, err := urlToMetadata(url) if err != nil { return } start := time.Now() instance, err := p.DialContext(ctx, &addr) if err != nil { return } defer func() { _ = instance.Close() }() req, err := http.NewRequest(http.MethodHead, url, nil) if err != nil { return } req = req.WithContext(ctx) transport := &http.Transport{ DialContext: func(context.Context, string, string) (net.Conn, error) { return instance, nil }, // from http.DefaultTransport MaxIdleConns: 100, IdleConnTimeout: 90 * time.Second, TLSHandshakeTimeout: 10 * time.Second, ExpectContinueTimeout: 1 * time.Second, } client := http.Client{ Transport: transport, CheckRedirect: func(req *http.Request, via []*http.Request) error { return http.ErrUseLastResponse }, } defer client.CloseIdleConnections() resp, err := client.Do(req) if err != nil { return } _ = resp.Body.Close() if unifiedDelay { start = time.Now() respRepeat, err := client.Do(req) if err != nil { return 0, err } _ = respRepeat.Body.Close() } t = uint16(time.Since(start) / time.Millisecond) return } func NewProxy(adapter C.ProxyAdapter) *Proxy { return &Proxy{adapter, queue.New[C.DelayHistory](10), atomic.NewBool(true)} } func urlToMetadata(rawURL string) (addr C.Metadata, err error) { u, err := url.Parse(rawURL) if err != nil { return } port := u.Port() if port == "" { switch u.Scheme { case "https": port = "443" case "http": port = "80" default: err = fmt.Errorf("%s scheme not Support", rawURL) return } } addr = C.Metadata{ AddrType: C.AtypDomainName, Host: u.Hostname(), DstIP: netip.Addr{}, DstPort: port, } return }