From e8dfbf513561a8b9f86424b2895664283084e466 Mon Sep 17 00:00:00 2001 From: gVisor bot Date: Sat, 21 Mar 2020 23:46:49 +0800 Subject: [PATCH] Feature: support relay (proxy chains) (#539) --- README.md | 10 ++++ adapters/outbound/base.go | 15 ++++- adapters/outbound/direct.go | 2 +- adapters/outbound/http.go | 27 ++++++--- adapters/outbound/reject.go | 2 +- adapters/outbound/shadowsocks.go | 43 ++++++++------ adapters/outbound/snell.go | 33 ++++++----- adapters/outbound/socks5.go | 35 +++++++---- adapters/outbound/trojan.go | 38 +++++++----- adapters/outbound/vmess.go | 20 ++++--- adapters/outboundgroup/fallback.go | 2 +- adapters/outboundgroup/loadbalance.go | 2 +- adapters/outboundgroup/parser.go | 4 +- adapters/outboundgroup/relay.go | 84 +++++++++++++++++++++++++++ adapters/outboundgroup/selector.go | 2 +- adapters/outboundgroup/urltest.go | 2 +- adapters/outboundgroup/util.go | 53 +++++++++++++++++ constant/adapters.go | 5 ++ 18 files changed, 293 insertions(+), 86 deletions(-) create mode 100644 adapters/outboundgroup/relay.go create mode 100644 adapters/outboundgroup/util.go diff --git a/README.md b/README.md index 4727d293..a7d6a766 100644 --- a/README.md +++ b/README.md @@ -255,6 +255,16 @@ proxies: # skip-cert-verify: true proxy-groups: + # relay chains the proxies. proxies shall not contain a proxy-group. No UDP support. + # Traffic: clash <-> http <-> vmess <-> ss1 <-> ss2 <-> Internet + - name: "relay" + type: relay + proxies: + - http + - vmess + - ss1 + - ss2 + # url-test select which proxy will be used by benchmarking speed to a URL. - name: "auto" type: url-test diff --git a/adapters/outbound/base.go b/adapters/outbound/base.go index 8ef50670..48576647 100644 --- a/adapters/outbound/base.go +++ b/adapters/outbound/base.go @@ -18,6 +18,7 @@ var ( type Base struct { name string + addr string tp C.AdapterType udp bool } @@ -30,6 +31,10 @@ func (b *Base) Type() C.AdapterType { return b.tp } +func (b *Base) StreamConn(c net.Conn, metadata *C.Metadata) (net.Conn, error) { + return c, errors.New("no support") +} + func (b *Base) DialUDP(metadata *C.Metadata) (C.PacketConn, error) { return nil, errors.New("no support") } @@ -44,8 +49,12 @@ func (b *Base) MarshalJSON() ([]byte, error) { }) } -func NewBase(name string, tp C.AdapterType, udp bool) *Base { - return &Base{name, tp, udp} +func (b *Base) Addr() string { + return b.addr +} + +func NewBase(name string, addr string, tp C.AdapterType, udp bool) *Base { + return &Base{name, addr, tp, udp} } type conn struct { @@ -61,7 +70,7 @@ func (c *conn) AppendToChains(a C.ProxyAdapter) { c.chain = append(c.chain, a.Name()) } -func newConn(c net.Conn, a C.ProxyAdapter) C.Conn { +func NewConn(c net.Conn, a C.ProxyAdapter) C.Conn { return &conn{c, []string{a.Name()}} } diff --git a/adapters/outbound/direct.go b/adapters/outbound/direct.go index 1d6d381e..f9413e6e 100644 --- a/adapters/outbound/direct.go +++ b/adapters/outbound/direct.go @@ -24,7 +24,7 @@ func (d *Direct) DialContext(ctx context.Context, metadata *C.Metadata) (C.Conn, return nil, err } tcpKeepAlive(c) - return newConn(c, d), nil + return NewConn(c, d), nil } func (d *Direct) DialUDP(metadata *C.Metadata) (C.PacketConn, error) { diff --git a/adapters/outbound/http.go b/adapters/outbound/http.go index e0ae0c41..e005b329 100644 --- a/adapters/outbound/http.go +++ b/adapters/outbound/http.go @@ -19,7 +19,6 @@ import ( type Http struct { *Base - addr string user string pass string tlsConfig *tls.Config @@ -35,23 +34,35 @@ type HttpOption struct { SkipCertVerify bool `proxy:"skip-cert-verify,omitempty"` } -func (h *Http) DialContext(ctx context.Context, metadata *C.Metadata) (C.Conn, error) { - c, err := dialer.DialContext(ctx, "tcp", h.addr) - if err == nil && h.tlsConfig != nil { +func (h *Http) StreamConn(c net.Conn, metadata *C.Metadata) (net.Conn, error) { + if h.tlsConfig != nil { cc := tls.Client(c, h.tlsConfig) - err = cc.Handshake() + err := cc.Handshake() c = cc + if err != nil { + return nil, fmt.Errorf("%s connect error: %w", h.addr, err) + } } + if err := h.shakeHand(metadata, c); err != nil { + return nil, err + } + return c, nil +} + +func (h *Http) DialContext(ctx context.Context, metadata *C.Metadata) (C.Conn, error) { + c, err := dialer.DialContext(ctx, "tcp", h.addr) if err != nil { return nil, fmt.Errorf("%s connect error: %w", h.addr, err) } tcpKeepAlive(c) - if err := h.shakeHand(metadata, c); err != nil { + + c, err = h.StreamConn(c, metadata) + if err != nil { return nil, err } - return newConn(c, h), nil + return NewConn(c, h), nil } func (h *Http) shakeHand(metadata *C.Metadata, rw io.ReadWriter) error { @@ -113,9 +124,9 @@ func NewHttp(option HttpOption) *Http { return &Http{ Base: &Base{ name: option.Name, + addr: net.JoinHostPort(option.Server, strconv.Itoa(option.Port)), tp: C.Http, }, - addr: net.JoinHostPort(option.Server, strconv.Itoa(option.Port)), user: option.UserName, pass: option.Password, tlsConfig: tlsConfig, diff --git a/adapters/outbound/reject.go b/adapters/outbound/reject.go index 7daba1a2..a403ce94 100644 --- a/adapters/outbound/reject.go +++ b/adapters/outbound/reject.go @@ -15,7 +15,7 @@ type Reject struct { } func (r *Reject) DialContext(ctx context.Context, metadata *C.Metadata) (C.Conn, error) { - return newConn(&NopConn{}, r), nil + return NewConn(&NopConn{}, r), nil } func (r *Reject) DialUDP(metadata *C.Metadata) (C.PacketConn, error) { diff --git a/adapters/outbound/shadowsocks.go b/adapters/outbound/shadowsocks.go index 5ad18df9..fca0e606 100644 --- a/adapters/outbound/shadowsocks.go +++ b/adapters/outbound/shadowsocks.go @@ -21,7 +21,6 @@ import ( type ShadowSocks struct { *Base - server string cipher core.Cipher // obfs @@ -60,28 +59,34 @@ type v2rayObfsOption struct { Mux bool `obfs:"mux,omitempty"` } -func (ss *ShadowSocks) DialContext(ctx context.Context, metadata *C.Metadata) (C.Conn, error) { - c, err := dialer.DialContext(ctx, "tcp", ss.server) - if err != nil { - return nil, fmt.Errorf("%s connect error: %w", ss.server, err) - } - tcpKeepAlive(c) +func (ss *ShadowSocks) StreamConn(c net.Conn, metadata *C.Metadata) (net.Conn, error) { switch ss.obfsMode { case "tls": c = obfs.NewTLSObfs(c, ss.obfsOption.Host) case "http": - _, port, _ := net.SplitHostPort(ss.server) + _, port, _ := net.SplitHostPort(ss.addr) c = obfs.NewHTTPObfs(c, ss.obfsOption.Host, port) case "websocket": var err error c, err = v2rayObfs.NewV2rayObfs(c, ss.v2rayOption) if err != nil { - return nil, fmt.Errorf("%s connect error: %w", ss.server, err) + return nil, fmt.Errorf("%s connect error: %w", ss.addr, err) } } c = ss.cipher.StreamConn(c) - _, err = c.Write(serializesSocksAddr(metadata)) - return newConn(c, ss), err + _, err := c.Write(serializesSocksAddr(metadata)) + return c, err +} + +func (ss *ShadowSocks) DialContext(ctx context.Context, metadata *C.Metadata) (C.Conn, error) { + c, err := dialer.DialContext(ctx, "tcp", ss.addr) + if err != nil { + return nil, fmt.Errorf("%s connect error: %w", ss.addr, err) + } + tcpKeepAlive(c) + + c, err = ss.StreamConn(c, metadata) + return NewConn(c, ss), err } func (ss *ShadowSocks) DialUDP(metadata *C.Metadata) (C.PacketConn, error) { @@ -90,7 +95,7 @@ func (ss *ShadowSocks) DialUDP(metadata *C.Metadata) (C.PacketConn, error) { return nil, err } - addr, err := resolveUDPAddr("udp", ss.server) + addr, err := resolveUDPAddr("udp", ss.addr) if err != nil { return nil, err } @@ -106,12 +111,12 @@ func (ss *ShadowSocks) MarshalJSON() ([]byte, error) { } func NewShadowSocks(option ShadowSocksOption) (*ShadowSocks, error) { - server := net.JoinHostPort(option.Server, strconv.Itoa(option.Port)) + addr := net.JoinHostPort(option.Server, strconv.Itoa(option.Port)) cipher := option.Cipher password := option.Password ciph, err := core.PickCipher(cipher, nil, password) if err != nil { - return nil, fmt.Errorf("ss %s initialize error: %w", server, err) + return nil, fmt.Errorf("ss %s initialize error: %w", addr, err) } var v2rayOption *v2rayObfs.Option @@ -133,22 +138,22 @@ func NewShadowSocks(option ShadowSocksOption) (*ShadowSocks, error) { if option.Plugin == "obfs" { opts := simpleObfsOption{Host: "bing.com"} if err := decoder.Decode(option.PluginOpts, &opts); err != nil { - return nil, fmt.Errorf("ss %s initialize obfs error: %w", server, err) + return nil, fmt.Errorf("ss %s initialize obfs error: %w", addr, err) } if opts.Mode != "tls" && opts.Mode != "http" { - return nil, fmt.Errorf("ss %s obfs mode error: %s", server, opts.Mode) + return nil, fmt.Errorf("ss %s obfs mode error: %s", addr, opts.Mode) } obfsMode = opts.Mode obfsOption = &opts } else if option.Plugin == "v2ray-plugin" { opts := v2rayObfsOption{Host: "bing.com", Mux: true} if err := decoder.Decode(option.PluginOpts, &opts); err != nil { - return nil, fmt.Errorf("ss %s initialize v2ray-plugin error: %w", server, err) + return nil, fmt.Errorf("ss %s initialize v2ray-plugin error: %w", addr, err) } if opts.Mode != "websocket" { - return nil, fmt.Errorf("ss %s obfs mode error: %s", server, opts.Mode) + return nil, fmt.Errorf("ss %s obfs mode error: %s", addr, opts.Mode) } obfsMode = opts.Mode @@ -172,10 +177,10 @@ func NewShadowSocks(option ShadowSocksOption) (*ShadowSocks, error) { return &ShadowSocks{ Base: &Base{ name: option.Name, + addr: addr, tp: C.Shadowsocks, udp: option.UDP, }, - server: server, cipher: ciph, obfsMode: obfsMode, diff --git a/adapters/outbound/snell.go b/adapters/outbound/snell.go index f96d8fff..61c103b8 100644 --- a/adapters/outbound/snell.go +++ b/adapters/outbound/snell.go @@ -15,7 +15,6 @@ import ( type Snell struct { *Base - server string psk []byte obfsOption *simpleObfsOption } @@ -28,45 +27,51 @@ type SnellOption struct { ObfsOpts map[string]interface{} `proxy:"obfs-opts,omitempty"` } -func (s *Snell) DialContext(ctx context.Context, metadata *C.Metadata) (C.Conn, error) { - c, err := dialer.DialContext(ctx, "tcp", s.server) - if err != nil { - return nil, fmt.Errorf("%s connect error: %w", s.server, err) - } - tcpKeepAlive(c) +func (s *Snell) StreamConn(c net.Conn, metadata *C.Metadata) (net.Conn, error) { switch s.obfsOption.Mode { case "tls": c = obfs.NewTLSObfs(c, s.obfsOption.Host) case "http": - _, port, _ := net.SplitHostPort(s.server) + _, port, _ := net.SplitHostPort(s.addr) c = obfs.NewHTTPObfs(c, s.obfsOption.Host, port) } c = snell.StreamConn(c, s.psk) port, _ := strconv.Atoi(metadata.DstPort) - err = snell.WriteHeader(c, metadata.String(), uint(port)) - return newConn(c, s), err + err := snell.WriteHeader(c, metadata.String(), uint(port)) + return c, err +} + +func (s *Snell) DialContext(ctx context.Context, metadata *C.Metadata) (C.Conn, error) { + c, err := dialer.DialContext(ctx, "tcp", s.addr) + if err != nil { + return nil, fmt.Errorf("%s connect error: %w", s.addr, err) + } + tcpKeepAlive(c) + + c, err = s.StreamConn(c, metadata) + return NewConn(c, s), err } func NewSnell(option SnellOption) (*Snell, error) { - server := net.JoinHostPort(option.Server, strconv.Itoa(option.Port)) + addr := net.JoinHostPort(option.Server, strconv.Itoa(option.Port)) psk := []byte(option.Psk) decoder := structure.NewDecoder(structure.Option{TagName: "obfs", WeaklyTypedInput: true}) obfsOption := &simpleObfsOption{Host: "bing.com"} if err := decoder.Decode(option.ObfsOpts, obfsOption); err != nil { - return nil, fmt.Errorf("snell %s initialize obfs error: %w", server, err) + return nil, fmt.Errorf("snell %s initialize obfs error: %w", addr, err) } if obfsOption.Mode != "tls" && obfsOption.Mode != "http" { - return nil, fmt.Errorf("snell %s obfs mode error: %s", server, obfsOption.Mode) + return nil, fmt.Errorf("snell %s obfs mode error: %s", addr, obfsOption.Mode) } return &Snell{ Base: &Base{ name: option.Name, + addr: addr, tp: C.Snell, }, - server: server, psk: psk, obfsOption: obfsOption, }, nil diff --git a/adapters/outbound/socks5.go b/adapters/outbound/socks5.go index c8bd1bf5..315b05b2 100644 --- a/adapters/outbound/socks5.go +++ b/adapters/outbound/socks5.go @@ -17,7 +17,6 @@ import ( type Socks5 struct { *Base - addr string user string pass string tls bool @@ -36,19 +35,16 @@ type Socks5Option struct { SkipCertVerify bool `proxy:"skip-cert-verify,omitempty"` } -func (ss *Socks5) DialContext(ctx context.Context, metadata *C.Metadata) (C.Conn, error) { - c, err := dialer.DialContext(ctx, "tcp", ss.addr) - - if err == nil && ss.tls { +func (ss *Socks5) StreamConn(c net.Conn, metadata *C.Metadata) (net.Conn, error) { + if ss.tls { cc := tls.Client(c, ss.tlsConfig) - err = cc.Handshake() + err := cc.Handshake() c = cc + if err != nil { + return nil, fmt.Errorf("%s connect error: %w", ss.addr, err) + } } - if err != nil { - return nil, fmt.Errorf("%s connect error: %w", ss.addr, err) - } - tcpKeepAlive(c) var user *socks5.User if ss.user != "" { user = &socks5.User{ @@ -59,7 +55,22 @@ func (ss *Socks5) DialContext(ctx context.Context, metadata *C.Metadata) (C.Conn if _, err := socks5.ClientHandshake(c, serializesSocksAddr(metadata), socks5.CmdConnect, user); err != nil { return nil, err } - return newConn(c, ss), nil + return c, nil +} + +func (ss *Socks5) DialContext(ctx context.Context, metadata *C.Metadata) (C.Conn, error) { + c, err := dialer.DialContext(ctx, "tcp", ss.addr) + if err != nil { + return nil, fmt.Errorf("%s connect error: %w", ss.addr, err) + } + tcpKeepAlive(c) + + c, err = ss.StreamConn(c, metadata) + if err != nil { + return nil, err + } + + return NewConn(c, ss), nil } func (ss *Socks5) DialUDP(metadata *C.Metadata) (_ C.PacketConn, err error) { @@ -127,10 +138,10 @@ func NewSocks5(option Socks5Option) *Socks5 { return &Socks5{ Base: &Base{ name: option.Name, + addr: net.JoinHostPort(option.Server, strconv.Itoa(option.Port)), tp: C.Socks5, udp: option.UDP, }, - addr: net.JoinHostPort(option.Server, strconv.Itoa(option.Port)), user: option.UserName, pass: option.Password, tls: option.TLS, diff --git a/adapters/outbound/trojan.go b/adapters/outbound/trojan.go index a7a3ae93..ab154b32 100644 --- a/adapters/outbound/trojan.go +++ b/adapters/outbound/trojan.go @@ -14,7 +14,6 @@ import ( type Trojan struct { *Base - server string instance *trojan.Trojan } @@ -29,32 +28,41 @@ type TrojanOption struct { UDP bool `proxy:"udp,omitempty"` } -func (t *Trojan) DialContext(ctx context.Context, metadata *C.Metadata) (C.Conn, error) { - c, err := dialer.DialContext(ctx, "tcp", t.server) +func (t *Trojan) StreamConn(c net.Conn, metadata *C.Metadata) (net.Conn, error) { + c, err := t.instance.StreamConn(c) if err != nil { - return nil, fmt.Errorf("%s connect error: %w", t.server, err) - } - tcpKeepAlive(c) - c, err = t.instance.StreamConn(c) - if err != nil { - return nil, fmt.Errorf("%s connect error: %w", t.server, err) + return nil, fmt.Errorf("%s connect error: %w", t.addr, err) } err = t.instance.WriteHeader(c, trojan.CommandTCP, serializesSocksAddr(metadata)) - return newConn(c, t), err + return c, err +} + +func (t *Trojan) DialContext(ctx context.Context, metadata *C.Metadata) (C.Conn, error) { + c, err := dialer.DialContext(ctx, "tcp", t.addr) + if err != nil { + return nil, fmt.Errorf("%s connect error: %w", t.addr, err) + } + tcpKeepAlive(c) + c, err = t.StreamConn(c, metadata) + if err != nil { + return nil, err + } + + return NewConn(c, t), err } func (t *Trojan) DialUDP(metadata *C.Metadata) (C.PacketConn, error) { ctx, cancel := context.WithTimeout(context.Background(), tcpTimeout) defer cancel() - c, err := dialer.DialContext(ctx, "tcp", t.server) + c, err := dialer.DialContext(ctx, "tcp", t.addr) if err != nil { - return nil, fmt.Errorf("%s connect error: %w", t.server, err) + return nil, fmt.Errorf("%s connect error: %w", t.addr, err) } tcpKeepAlive(c) c, err = t.instance.StreamConn(c) if err != nil { - return nil, fmt.Errorf("%s connect error: %w", t.server, err) + return nil, fmt.Errorf("%s connect error: %w", t.addr, err) } err = t.instance.WriteHeader(c, trojan.CommandUDP, serializesSocksAddr(metadata)) @@ -73,7 +81,7 @@ func (t *Trojan) MarshalJSON() ([]byte, error) { } func NewTrojan(option TrojanOption) (*Trojan, error) { - server := net.JoinHostPort(option.Server, strconv.Itoa(option.Port)) + addr := net.JoinHostPort(option.Server, strconv.Itoa(option.Port)) tOption := &trojan.Option{ Password: option.Password, @@ -90,10 +98,10 @@ func NewTrojan(option TrojanOption) (*Trojan, error) { return &Trojan{ Base: &Base{ name: option.Name, + addr: addr, tp: C.Trojan, udp: option.UDP, }, - server: server, instance: trojan.New(tOption), }, nil } diff --git a/adapters/outbound/vmess.go b/adapters/outbound/vmess.go index 73a8f4a9..4420514f 100644 --- a/adapters/outbound/vmess.go +++ b/adapters/outbound/vmess.go @@ -16,7 +16,6 @@ import ( type Vmess struct { *Base - server string client *vmess.Client } @@ -35,14 +34,19 @@ type VmessOption struct { SkipCertVerify bool `proxy:"skip-cert-verify,omitempty"` } +func (v *Vmess) StreamConn(c net.Conn, metadata *C.Metadata) (net.Conn, error) { + return v.client.New(c, parseVmessAddr(metadata)) +} + func (v *Vmess) DialContext(ctx context.Context, metadata *C.Metadata) (C.Conn, error) { - c, err := dialer.DialContext(ctx, "tcp", v.server) + c, err := dialer.DialContext(ctx, "tcp", v.addr) if err != nil { - return nil, fmt.Errorf("%s connect error", v.server) + return nil, fmt.Errorf("%s connect error", v.addr) } tcpKeepAlive(c) - c, err = v.client.New(c, parseVmessAddr(metadata)) - return newConn(c, v), err + + c, err = v.StreamConn(c, metadata) + return NewConn(c, v), err } func (v *Vmess) DialUDP(metadata *C.Metadata) (C.PacketConn, error) { @@ -57,9 +61,9 @@ func (v *Vmess) DialUDP(metadata *C.Metadata) (C.PacketConn, error) { ctx, cancel := context.WithTimeout(context.Background(), tcpTimeout) defer cancel() - c, err := dialer.DialContext(ctx, "tcp", v.server) + c, err := dialer.DialContext(ctx, "tcp", v.addr) if err != nil { - return nil, fmt.Errorf("%s connect error", v.server) + return nil, fmt.Errorf("%s connect error", v.addr) } tcpKeepAlive(c) c, err = v.client.New(c, parseVmessAddr(metadata)) @@ -91,10 +95,10 @@ func NewVmess(option VmessOption) (*Vmess, error) { return &Vmess{ Base: &Base{ name: option.Name, + addr: net.JoinHostPort(option.Server, strconv.Itoa(option.Port)), tp: C.Vmess, udp: true, }, - server: net.JoinHostPort(option.Server, strconv.Itoa(option.Port)), client: client, }, nil } diff --git a/adapters/outboundgroup/fallback.go b/adapters/outboundgroup/fallback.go index 7a344d09..2fb48649 100644 --- a/adapters/outboundgroup/fallback.go +++ b/adapters/outboundgroup/fallback.go @@ -77,7 +77,7 @@ func (f *Fallback) findAliveProxy() C.Proxy { func NewFallback(name string, providers []provider.ProxyProvider) *Fallback { return &Fallback{ - Base: outbound.NewBase(name, C.Fallback, false), + Base: outbound.NewBase(name, "", C.Fallback, false), single: singledo.NewSingle(defaultGetProxiesDuration), providers: providers, } diff --git a/adapters/outboundgroup/loadbalance.go b/adapters/outboundgroup/loadbalance.go index 1495cda2..9e070105 100644 --- a/adapters/outboundgroup/loadbalance.go +++ b/adapters/outboundgroup/loadbalance.go @@ -120,7 +120,7 @@ func (lb *LoadBalance) MarshalJSON() ([]byte, error) { func NewLoadBalance(name string, providers []provider.ProxyProvider) *LoadBalance { return &LoadBalance{ - Base: outbound.NewBase(name, C.LoadBalance, false), + Base: outbound.NewBase(name, "", C.LoadBalance, false), single: singledo.NewSingle(defaultGetProxiesDuration), maxRetry: 3, providers: providers, diff --git a/adapters/outboundgroup/parser.go b/adapters/outboundgroup/parser.go index f0c01258..7eac7e9a 100644 --- a/adapters/outboundgroup/parser.go +++ b/adapters/outboundgroup/parser.go @@ -64,7 +64,7 @@ func ParseProxyGroup(config map[string]interface{}, proxyMap map[string]C.Proxy, providers = append(providers, pd) } else { // select don't need health check - if groupOption.Type == "select" { + if groupOption.Type == "select" || groupOption.Type == "relay" { hc := provider.NewHealthCheck(ps, "", 0) pd, err := provider.NewCompatibleProvider(groupName, ps, hc) if err != nil { @@ -108,6 +108,8 @@ func ParseProxyGroup(config map[string]interface{}, proxyMap map[string]C.Proxy, group = NewFallback(groupName, providers) case "load-balance": group = NewLoadBalance(groupName, providers) + case "relay": + group = NewRelay(groupName, providers) default: return nil, fmt.Errorf("%w: %s", errType, groupOption.Type) } diff --git a/adapters/outboundgroup/relay.go b/adapters/outboundgroup/relay.go new file mode 100644 index 00000000..37a57e00 --- /dev/null +++ b/adapters/outboundgroup/relay.go @@ -0,0 +1,84 @@ +package outboundgroup + +import ( + "context" + "encoding/json" + "errors" + "fmt" + + "github.com/Dreamacro/clash/adapters/outbound" + "github.com/Dreamacro/clash/adapters/provider" + "github.com/Dreamacro/clash/common/singledo" + "github.com/Dreamacro/clash/component/dialer" + C "github.com/Dreamacro/clash/constant" +) + +type Relay struct { + *outbound.Base + single *singledo.Single + providers []provider.ProxyProvider +} + +func (r *Relay) DialContext(ctx context.Context, metadata *C.Metadata) (C.Conn, error) { + proxies := r.proxies() + if len(proxies) == 0 { + return nil, errors.New("Proxy does not exist") + } + first := proxies[0] + last := proxies[len(proxies)-1] + + c, err := dialer.DialContext(ctx, "tcp", first.Addr()) + if err != nil { + return nil, fmt.Errorf("%s connect error: %w", first.Addr(), err) + } + tcpKeepAlive(c) + + var currentMeta *C.Metadata + for _, proxy := range proxies[1:] { + currentMeta, err = addrToMetadata(proxy.Addr()) + if err != nil { + return nil, err + } + + c, err = first.StreamConn(c, currentMeta) + if err != nil { + return nil, fmt.Errorf("%s connect error: %w", first.Addr(), err) + } + + first = proxy + } + + c, err = last.StreamConn(c, metadata) + if err != nil { + return nil, fmt.Errorf("%s connect error: %w", last.Addr(), err) + } + + return outbound.NewConn(c, r), nil +} + +func (r *Relay) MarshalJSON() ([]byte, error) { + var all []string + for _, proxy := range r.proxies() { + all = append(all, proxy.Name()) + } + return json.Marshal(map[string]interface{}{ + "type": r.Type().String(), + "all": all, + }) +} + +func (r *Relay) proxies() []C.Proxy { + elm, _, _ := r.single.Do(func() (interface{}, error) { + return getProvidersProxies(r.providers), nil + }) + + return elm.([]C.Proxy) +} + +func NewRelay(name string, providers []provider.ProxyProvider) *Relay { + return &Relay{ + Base: outbound.NewBase(name, "", C.Relay, false), + single: singledo.NewSingle(defaultGetProxiesDuration), + providers: providers, + } +} diff --git a/adapters/outboundgroup/selector.go b/adapters/outboundgroup/selector.go index 533a6126..6ad78f20 100644 --- a/adapters/outboundgroup/selector.go +++ b/adapters/outboundgroup/selector.go @@ -77,7 +77,7 @@ func (s *Selector) proxies() []C.Proxy { func NewSelector(name string, providers []provider.ProxyProvider) *Selector { selected := providers[0].Proxies()[0] return &Selector{ - Base: outbound.NewBase(name, C.Selector, false), + Base: outbound.NewBase(name, "", C.Selector, false), single: singledo.NewSingle(defaultGetProxiesDuration), providers: providers, selected: selected, diff --git a/adapters/outboundgroup/urltest.go b/adapters/outboundgroup/urltest.go index c985eea8..bfdb20e9 100644 --- a/adapters/outboundgroup/urltest.go +++ b/adapters/outboundgroup/urltest.go @@ -86,7 +86,7 @@ func (u *URLTest) MarshalJSON() ([]byte, error) { func NewURLTest(name string, providers []provider.ProxyProvider) *URLTest { return &URLTest{ - Base: outbound.NewBase(name, C.URLTest, false), + Base: outbound.NewBase(name, "", C.URLTest, false), single: singledo.NewSingle(defaultGetProxiesDuration), fastSingle: singledo.NewSingle(time.Second * 10), providers: providers, diff --git a/adapters/outboundgroup/util.go b/adapters/outboundgroup/util.go new file mode 100644 index 00000000..c8a6a858 --- /dev/null +++ b/adapters/outboundgroup/util.go @@ -0,0 +1,53 @@ +package outboundgroup + +import ( + "fmt" + "net" + "time" + + C "github.com/Dreamacro/clash/constant" +) + +func addrToMetadata(rawAddress string) (addr *C.Metadata, err error) { + host, port, err := net.SplitHostPort(rawAddress) + if err != nil { + err = fmt.Errorf("addrToMetadata failed: %w", err) + return + } + + ip := net.ParseIP(host) + if ip != nil { + if ip.To4() != nil { + addr = &C.Metadata{ + AddrType: C.AtypIPv4, + Host: "", + DstIP: ip, + DstPort: port, + } + return + } else { + addr = &C.Metadata{ + AddrType: C.AtypIPv6, + Host: "", + DstIP: ip, + DstPort: port, + } + return + } + } else { + addr = &C.Metadata{ + AddrType: C.AtypDomainName, + Host: host, + DstIP: nil, + DstPort: port, + } + return + } +} + +func tcpKeepAlive(c net.Conn) { + if tcp, ok := c.(*net.TCPConn); ok { + tcp.SetKeepAlive(true) + tcp.SetKeepAlivePeriod(30 * time.Second) + } +} diff --git a/constant/adapters.go b/constant/adapters.go index 3bc6d278..ac19de36 100644 --- a/constant/adapters.go +++ b/constant/adapters.go @@ -19,6 +19,7 @@ const ( Vmess Trojan + Relay Selector Fallback URLTest @@ -62,10 +63,12 @@ type PacketConn interface { type ProxyAdapter interface { Name() string Type() AdapterType + StreamConn(c net.Conn, metadata *Metadata) (net.Conn, error) DialContext(ctx context.Context, metadata *Metadata) (Conn, error) DialUDP(metadata *Metadata) (PacketConn, error) SupportUDP() bool MarshalJSON() ([]byte, error) + Addr() string } type DelayHistory struct { @@ -105,6 +108,8 @@ func (at AdapterType) String() string { case Trojan: return "Trojan" + case Relay: + return "Relay" case Selector: return "Selector" case Fallback: