Skip to content

Commit

Permalink
Feature: support relay (proxy chains) (#539)
Browse files Browse the repository at this point in the history
  • Loading branch information
duament authored Mar 21, 2020
1 parent 70a19b9 commit c0a2473
Show file tree
Hide file tree
Showing 18 changed files with 290 additions and 83 deletions.
10 changes: 10 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
15 changes: 12 additions & 3 deletions adapters/outbound/base.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ var (

type Base struct {
name string
addr string
tp C.AdapterType
udp bool
}
Expand All @@ -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")
}
Expand All @@ -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 {
Expand All @@ -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()}}
}

Expand Down
2 changes: 1 addition & 1 deletion adapters/outbound/direct.go
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
27 changes: 19 additions & 8 deletions adapters/outbound/http.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,6 @@ import (

type Http struct {
*Base
addr string
user string
pass string
tlsConfig *tls.Config
Expand All @@ -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 {
Expand Down Expand Up @@ -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,
Expand Down
2 changes: 1 addition & 1 deletion adapters/outbound/reject.go
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
43 changes: 24 additions & 19 deletions adapters/outbound/shadowsocks.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,6 @@ import (

type ShadowSocks struct {
*Base
server string
cipher core.Cipher

// obfs
Expand Down Expand Up @@ -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) {
Expand All @@ -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
}
Expand All @@ -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
Expand All @@ -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

Expand All @@ -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,
Expand Down
33 changes: 19 additions & 14 deletions adapters/outbound/snell.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,6 @@ import (

type Snell struct {
*Base
server string
psk []byte
obfsOption *simpleObfsOption
}
Expand All @@ -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
Expand Down
35 changes: 23 additions & 12 deletions adapters/outbound/socks5.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,6 @@ import (

type Socks5 struct {
*Base
addr string
user string
pass string
tls bool
Expand All @@ -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{
Expand All @@ -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) {
Expand Down Expand Up @@ -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,
Expand Down
Loading

0 comments on commit c0a2473

Please sign in to comment.