From 8ef439b8d8fb233eeec690264625b36819232e7f Mon Sep 17 00:00:00 2001 From: xjasonlyu Date: Mon, 22 Jul 2024 18:04:57 -0400 Subject: [PATCH] Refactor(proxy): make proxies registerable --- engine/engine.go | 4 +- engine/parse.go | 111 +------------------------ engine/proxy.go | 11 +++ proxy/base.go | 33 -------- proxy/{ => direct}/direct.go | 30 +++---- proxy/{ => http}/http.go | 36 +++++--- proxy/internal/base.go | 44 ++++++++++ proxy/internal/util.go | 32 +++++++ proxy/proto/proto.go | 36 -------- proxy/protocol.go | 72 ++++++++++++++++ proxy/proxy.go | 36 ++++---- proxy/{ => reject}/reject.go | 36 ++++---- proxy/{ => relay}/relay.go | 48 +++++++---- proxy/{ => shadowsocks}/shadowsocks.go | 84 +++++++++++++++---- proxy/socks4.go | 45 ---------- proxy/socks4/socks4.go | 55 ++++++++++++ proxy/{ => socks5}/socks5.go | 58 ++++++++----- proxy/util.go | 25 ------ 18 files changed, 423 insertions(+), 373 deletions(-) create mode 100644 engine/proxy.go delete mode 100644 proxy/base.go rename proxy/{ => direct}/direct.go (67%) rename proxy/{ => http}/http.go (73%) create mode 100644 proxy/internal/base.go create mode 100644 proxy/internal/util.go delete mode 100644 proxy/proto/proto.go create mode 100644 proxy/protocol.go rename proxy/{ => reject}/reject.go (73%) rename proxy/{ => relay}/relay.go (81%) rename proxy/{ => shadowsocks}/shadowsocks.go (52%) delete mode 100644 proxy/socks4.go create mode 100644 proxy/socks4/socks4.go rename proxy/{ => socks5}/socks5.go (72%) delete mode 100644 proxy/util.go diff --git a/engine/engine.go b/engine/engine.go index f513fac3..bd752aa9 100644 --- a/engine/engine.go +++ b/engine/engine.go @@ -192,7 +192,7 @@ func netstack(k *Key) (err error) { if _defaultProxy, err = parseProxy(k.Proxy); err != nil { return } - proxy.SetDialer(_defaultProxy) + proxy.DefaultProxy = _defaultProxy if _defaultDevice, err = parseDevice(k.Device, uint32(k.MTU)); err != nil { return @@ -236,7 +236,7 @@ func netstack(k *Key) (err error) { log.Infof( "[STACK] %s://%s <-> %s://%s", _defaultDevice.Type(), _defaultDevice.Name(), - _defaultProxy.Proto(), _defaultProxy.Addr(), + _defaultProxy.Protocol(), _defaultProxy.Address(), ) return nil } diff --git a/engine/parse.go b/engine/parse.go index c4cba68e..cd6cf3d3 100644 --- a/engine/parse.go +++ b/engine/parse.go @@ -1,19 +1,15 @@ package engine import ( - "encoding/base64" "fmt" "net" "net/url" "strings" - "github.com/gorilla/schema" - "github.com/xjasonlyu/tun2socks/v2/core/device" "github.com/xjasonlyu/tun2socks/v2/core/device/fdbased" "github.com/xjasonlyu/tun2socks/v2/core/device/tun" "github.com/xjasonlyu/tun2socks/v2/proxy" - "github.com/xjasonlyu/tun2socks/v2/proxy/proto" ) func parseRestAPI(s string) (*url.URL, error) { @@ -70,112 +66,7 @@ func parseFD(u *url.URL, mtu uint32) (device.Device, error) { } func parseProxy(s string) (proxy.Proxy, error) { - if !strings.Contains(s, "://") { - s = fmt.Sprintf("%s://%s", proto.Socks5 /* default protocol */, s) - } - - u, err := url.Parse(s) - if err != nil { - return nil, err - } - - protocol := strings.ToLower(u.Scheme) - - switch protocol { - case proto.Direct.String(): - return proxy.NewDirect(), nil - case proto.Reject.String(): - return proxy.NewReject(), nil - case proto.HTTP.String(): - return parseHTTP(u) - case proto.Socks4.String(): - return parseSocks4(u) - case proto.Socks5.String(): - return parseSocks5(u) - case proto.Shadowsocks.String(): - return parseShadowsocks(u) - case proto.Relay.String(): - return parseRelay(u) - default: - return nil, fmt.Errorf("unsupported protocol: %s", protocol) - } -} - -func parseHTTP(u *url.URL) (proxy.Proxy, error) { - address, username := u.Host, u.User.Username() - password, _ := u.User.Password() - return proxy.NewHTTP(address, username, password) -} - -func parseSocks4(u *url.URL) (proxy.Proxy, error) { - address, userID := u.Host, u.User.Username() - return proxy.NewSocks4(address, userID) -} - -func parseSocks5(u *url.URL) (proxy.Proxy, error) { - address, username := u.Host, u.User.Username() - password, _ := u.User.Password() - - // Socks5 over UDS - if address == "" { - address = u.Path - } - return proxy.NewSocks5(address, username, password) -} - -func parseShadowsocks(u *url.URL) (proxy.Proxy, error) { - var ( - address = u.Host - method, password string - obfsMode, obfsHost string - ) - - if ss := u.User.String(); ss == "" { - method = "dummy" // none cipher mode - } else if pass, set := u.User.Password(); set { - method = u.User.Username() - password = pass - } else { - data, _ := base64.RawURLEncoding.DecodeString(ss) - userInfo := strings.SplitN(string(data), ":", 2) - if len(userInfo) == 2 { - method = userInfo[0] - password = userInfo[1] - } - } - - rawQuery, _ := url.QueryUnescape(u.RawQuery) - for _, s := range strings.Split(rawQuery, ";") { - data := strings.SplitN(s, "=", 2) - if len(data) != 2 { - continue - } - key := data[0] - value := data[1] - - switch key { - case "obfs": - obfsMode = value - case "obfs-host": - obfsHost = value - } - } - - return proxy.NewShadowsocks(address, method, password, obfsMode, obfsHost) -} - -func parseRelay(u *url.URL) (proxy.Proxy, error) { - address, username := u.Host, u.User.Username() - password, _ := u.User.Password() - - opts := struct { - NoDelay bool - }{} - if err := schema.NewDecoder().Decode(&opts, u.Query()); err != nil { - return nil, err - } - - return proxy.NewRelay(address, username, password, opts.NoDelay) + return proxy.ParseFromURL(s) } func parseMulticastGroups(s string) (multicastGroups []net.IP, _ error) { diff --git a/engine/proxy.go b/engine/proxy.go new file mode 100644 index 00000000..040b5331 --- /dev/null +++ b/engine/proxy.go @@ -0,0 +1,11 @@ +package engine + +import ( + _ "github.com/xjasonlyu/tun2socks/v2/proxy/direct" + _ "github.com/xjasonlyu/tun2socks/v2/proxy/http" + _ "github.com/xjasonlyu/tun2socks/v2/proxy/reject" + _ "github.com/xjasonlyu/tun2socks/v2/proxy/relay" + _ "github.com/xjasonlyu/tun2socks/v2/proxy/shadowsocks" + _ "github.com/xjasonlyu/tun2socks/v2/proxy/socks4" + _ "github.com/xjasonlyu/tun2socks/v2/proxy/socks5" +) diff --git a/proxy/base.go b/proxy/base.go deleted file mode 100644 index e187fbf8..00000000 --- a/proxy/base.go +++ /dev/null @@ -1,33 +0,0 @@ -package proxy - -import ( - "context" - "errors" - "net" - - M "github.com/xjasonlyu/tun2socks/v2/metadata" - "github.com/xjasonlyu/tun2socks/v2/proxy/proto" -) - -var _ Proxy = (*Base)(nil) - -type Base struct { - addr string - proto proto.Proto -} - -func (b *Base) Addr() string { - return b.addr -} - -func (b *Base) Proto() proto.Proto { - return b.proto -} - -func (b *Base) DialContext(context.Context, *M.Metadata) (net.Conn, error) { - return nil, errors.ErrUnsupported -} - -func (b *Base) DialUDP(*M.Metadata) (net.PacketConn, error) { - return nil, errors.ErrUnsupported -} diff --git a/proxy/direct.go b/proxy/direct/direct.go similarity index 67% rename from proxy/direct.go rename to proxy/direct/direct.go index 70a5ad8e..16768ed8 100644 --- a/proxy/direct.go +++ b/proxy/direct/direct.go @@ -1,34 +1,32 @@ -package proxy +package direct import ( "context" "net" + "net/url" "github.com/xjasonlyu/tun2socks/v2/dialer" M "github.com/xjasonlyu/tun2socks/v2/metadata" - "github.com/xjasonlyu/tun2socks/v2/proxy/proto" + "github.com/xjasonlyu/tun2socks/v2/proxy" + "github.com/xjasonlyu/tun2socks/v2/proxy/internal" ) -var _ Proxy = (*Direct)(nil) +var _ proxy.Proxy = (*Direct)(nil) -type Direct struct { - *Base -} +const Protocol = "direct" -func NewDirect() *Direct { - return &Direct{ - Base: &Base{ - proto: proto.Direct, - }, - } -} +type Direct struct{ *internal.Base } + +func New() *Direct { return &Direct{internal.New(Protocol, "")} } + +func Parse(*url.URL) (proxy.Proxy, error) { return New(), nil } func (d *Direct) DialContext(ctx context.Context, metadata *M.Metadata) (net.Conn, error) { c, err := dialer.DialContext(ctx, "tcp", metadata.DestinationAddress()) if err != nil { return nil, err } - setKeepAlive(c) + internal.SetKeepAlive(c) return c, nil } @@ -55,3 +53,7 @@ func (pc *directPacketConn) WriteTo(b []byte, addr net.Addr) (int, error) { } return pc.PacketConn.WriteTo(b, udpAddr) } + +func init() { + proxy.RegisterProtocol(Protocol, Parse) +} diff --git a/proxy/http.go b/proxy/http/http.go similarity index 73% rename from proxy/http.go rename to proxy/http/http.go index b0dec31d..3914494b 100644 --- a/proxy/http.go +++ b/proxy/http/http.go @@ -1,4 +1,4 @@ -package proxy +package http import ( "bufio" @@ -13,36 +13,44 @@ import ( "github.com/xjasonlyu/tun2socks/v2/dialer" M "github.com/xjasonlyu/tun2socks/v2/metadata" - "github.com/xjasonlyu/tun2socks/v2/proxy/proto" + "github.com/xjasonlyu/tun2socks/v2/proxy" + "github.com/xjasonlyu/tun2socks/v2/proxy/internal" ) +var _ proxy.Proxy = (*HTTP)(nil) + +const Protocol = "http" + type HTTP struct { - *Base + *internal.Base user string pass string } -func NewHTTP(addr, user, pass string) (*HTTP, error) { +func New(addr, user, pass string) (*HTTP, error) { return &HTTP{ - Base: &Base{ - addr: addr, - proto: proto.HTTP, - }, + Base: internal.New(Protocol, addr), user: user, pass: pass, }, nil } +func Parse(proxyURL *url.URL) (proxy.Proxy, error) { + address, username := proxyURL.Host, proxyURL.User.Username() + password, _ := proxyURL.User.Password() + return New(address, username, password) +} + func (h *HTTP) DialContext(ctx context.Context, metadata *M.Metadata) (c net.Conn, err error) { - c, err = dialer.DialContext(ctx, "tcp", h.Addr()) + c, err = dialer.DialContext(ctx, "tcp", h.Address()) if err != nil { - return nil, fmt.Errorf("connect to %s: %w", h.Addr(), err) + return nil, fmt.Errorf("connect to %s: %w", h.Address(), err) } - setKeepAlive(c) + internal.SetKeepAlive(c) defer func(c net.Conn) { - safeConnClose(c, err) + internal.SafeConnClose(c, err) }(c) err = h.shakeHand(metadata, c) @@ -98,3 +106,7 @@ func basicAuth(username, password string) string { auth := username + ":" + password return base64.StdEncoding.EncodeToString([]byte(auth)) } + +func init() { + proxy.RegisterProtocol(Protocol, Parse) +} diff --git a/proxy/internal/base.go b/proxy/internal/base.go new file mode 100644 index 00000000..1055d6dd --- /dev/null +++ b/proxy/internal/base.go @@ -0,0 +1,44 @@ +package internal + +import ( + "context" + "errors" + "fmt" + "net" + + M "github.com/xjasonlyu/tun2socks/v2/metadata" + "github.com/xjasonlyu/tun2socks/v2/proxy" +) + +var _ proxy.Proxy = (*Base)(nil) + +type Base struct { + protocol, address string +} + +func New(protocol, address string) *Base { + return &Base{ + protocol: protocol, + address: address, + } +} + +func (b *Base) Address() string { + return b.address +} + +func (b *Base) Protocol() string { + return b.protocol +} + +func (b *Base) String() string { + return fmt.Sprintf("%s://%s", b.protocol, b.address) +} + +func (b *Base) DialContext(context.Context, *M.Metadata) (net.Conn, error) { + return nil, errors.ErrUnsupported +} + +func (b *Base) DialUDP(*M.Metadata) (net.PacketConn, error) { + return nil, errors.ErrUnsupported +} diff --git a/proxy/internal/util.go b/proxy/internal/util.go new file mode 100644 index 00000000..7036f519 --- /dev/null +++ b/proxy/internal/util.go @@ -0,0 +1,32 @@ +package internal + +import ( + "net" + "time" + + M "github.com/xjasonlyu/tun2socks/v2/metadata" + "github.com/xjasonlyu/tun2socks/v2/transport/socks5" +) + +const ( + tcpKeepAlivePeriod = 30 * time.Second +) + +// SetKeepAlive sets tcp keepalive option for tcp connection. +func SetKeepAlive(c net.Conn) { + if tcp, ok := c.(*net.TCPConn); ok { + tcp.SetKeepAlive(true) + tcp.SetKeepAlivePeriod(tcpKeepAlivePeriod) + } +} + +// SafeConnClose closes tcp connection safely. +func SafeConnClose(c net.Conn, err error) { + if c != nil && err != nil { + c.Close() + } +} + +func SerializeSocksAddr(m *M.Metadata) socks5.Addr { + return socks5.SerializeAddr("", m.DstIP, m.DstPort) +} diff --git a/proxy/proto/proto.go b/proxy/proto/proto.go deleted file mode 100644 index ef77d053..00000000 --- a/proxy/proto/proto.go +++ /dev/null @@ -1,36 +0,0 @@ -package proto - -import "fmt" - -const ( - Direct Proto = iota - Reject - HTTP - Socks4 - Socks5 - Shadowsocks - Relay -) - -type Proto uint8 - -func (proto Proto) String() string { - switch proto { - case Direct: - return "direct" - case Reject: - return "reject" - case HTTP: - return "http" - case Socks4: - return "socks4" - case Socks5: - return "socks5" - case Shadowsocks: - return "ss" - case Relay: - return "relay" - default: - return fmt.Sprintf("proto(%d)", proto) - } -} diff --git a/proxy/protocol.go b/proxy/protocol.go new file mode 100644 index 00000000..f6edad47 --- /dev/null +++ b/proxy/protocol.go @@ -0,0 +1,72 @@ +package proxy + +import ( + "errors" + "net/url" + "sync" + + "go.uber.org/atomic" +) + +// ErrProtocol indicates that parsing encountered an unknown protocol. +var ErrProtocol = errors.New("proxy: unknown protocol") + +// A proxy holds a proxy's protocol and how to parse it. +type protocol struct { + name string + parse func(*url.URL) (Proxy, error) +} + +// Protocols is the list of registered proxy protocols. +var ( + protocolsMu sync.Mutex + atomicProtocols atomic.Value +) + +// RegisterProtocol registers a proxy protocol for use by [Parse]. +// Name is the name of the proxy protocol, like "http" or "socks5". +// [Parse] is the function that parses the proxy url. +func RegisterProtocol(name string, parse func(*url.URL) (Proxy, error)) { + protocolsMu.Lock() + formats, _ := atomicProtocols.Load().([]protocol) + atomicProtocols.Store(append(formats, protocol{name, parse})) + protocolsMu.Unlock() +} + +// pick determines the protocol by the given name. +func pick(name string) protocol { + protocols, _ := atomicProtocols.Load().([]protocol) + for _, p := range protocols { + if p.name == name { + return p + } + } + return protocol{} +} + +// Parse parses a proxy url that has been encoded in a registered format. +// The string returned is the format name used during format registration. +// Format registration is typically done by an init function in the codec- +// specific package. +func Parse(proxyURL *url.URL) (Proxy, error) { + if proxyURL == nil { + return nil, errors.New("proxy: nil url") + } + if proxyURL.Scheme == "" { + return nil, errors.New("proxy: protocol not specified") + } + p := pick(proxyURL.Scheme) + if p.parse == nil { + return nil, ErrProtocol + } + return p.parse(proxyURL) +} + +// ParseFromURL parses a +func ParseFromURL(proxy string) (Proxy, error) { + proxyURL, err := url.Parse(proxy) + if err != nil { + return nil, err + } + return Parse(proxyURL) +} diff --git a/proxy/proxy.go b/proxy/proxy.go index 27cafc7b..a114d654 100644 --- a/proxy/proxy.go +++ b/proxy/proxy.go @@ -7,44 +7,36 @@ import ( "time" M "github.com/xjasonlyu/tun2socks/v2/metadata" - "github.com/xjasonlyu/tun2socks/v2/proxy/proto" ) const ( - tcpConnectTimeout = 5 * time.Second + TCPConnectTimeout = 5 * time.Second ) -var _defaultDialer Dialer = &Base{} +// DefaultProxy is the default [Proxy] and is used by [Dial], [DialContext], and [DialUDP]. +var DefaultProxy Proxy = nil -type Dialer interface { +type Proxy interface { + Address() string + Protocol() string + String() string DialContext(context.Context, *M.Metadata) (net.Conn, error) DialUDP(*M.Metadata) (net.PacketConn, error) } -type Proxy interface { - Dialer - Addr() string - Proto() proto.Proto -} - -// SetDialer sets default Dialer. -func SetDialer(d Dialer) { - _defaultDialer = d -} - -// Dial uses default Dialer to dial TCP. +// Dial uses the DefaultProxy to dial TCP. func Dial(metadata *M.Metadata) (net.Conn, error) { - ctx, cancel := context.WithTimeout(context.Background(), tcpConnectTimeout) + ctx, cancel := context.WithTimeout(context.Background(), TCPConnectTimeout) defer cancel() - return _defaultDialer.DialContext(ctx, metadata) + return DialContext(ctx, metadata) } -// DialContext uses default Dialer to dial TCP with context. +// DialContext uses the DefaultProxy to dial TCP with context. func DialContext(ctx context.Context, metadata *M.Metadata) (net.Conn, error) { - return _defaultDialer.DialContext(ctx, metadata) + return DefaultProxy.DialContext(ctx, metadata) } -// DialUDP uses default Dialer to dial UDP. +// DialUDP uses the DefaultProxy to dial UDP. func DialUDP(metadata *M.Metadata) (net.PacketConn, error) { - return _defaultDialer.DialUDP(metadata) + return DefaultProxy.DialUDP(metadata) } diff --git a/proxy/reject.go b/proxy/reject/reject.go similarity index 73% rename from proxy/reject.go rename to proxy/reject/reject.go index 3509a716..9f1b4ba8 100644 --- a/proxy/reject.go +++ b/proxy/reject/reject.go @@ -1,36 +1,30 @@ -package proxy +package reject import ( "context" "io" "net" + "net/url" "time" M "github.com/xjasonlyu/tun2socks/v2/metadata" - "github.com/xjasonlyu/tun2socks/v2/proxy/proto" + "github.com/xjasonlyu/tun2socks/v2/proxy" + "github.com/xjasonlyu/tun2socks/v2/proxy/internal" ) -var _ Proxy = (*Reject)(nil) +var _ proxy.Proxy = (*Reject)(nil) -type Reject struct { - *Base -} +const Protocol = "reject" -func NewReject() *Reject { - return &Reject{ - Base: &Base{ - proto: proto.Reject, - }, - } -} +type Reject struct{ *internal.Base } -func (r *Reject) DialContext(context.Context, *M.Metadata) (net.Conn, error) { - return &nopConn{}, nil -} +func New() *Reject { return &Reject{internal.New(Protocol, "")} } -func (r *Reject) DialUDP(*M.Metadata) (net.PacketConn, error) { - return &nopPacketConn{}, nil -} +func Parse(*url.URL) (proxy.Proxy, error) { return New(), nil } + +func (r *Reject) DialContext(context.Context, *M.Metadata) (net.Conn, error) { return &nopConn{}, nil } + +func (r *Reject) DialUDP(*M.Metadata) (net.PacketConn, error) { return &nopPacketConn{}, nil } type nopConn struct{} @@ -52,3 +46,7 @@ func (npc *nopPacketConn) LocalAddr() net.Addr { ret func (npc *nopPacketConn) SetDeadline(time.Time) error { return nil } func (npc *nopPacketConn) SetReadDeadline(time.Time) error { return nil } func (npc *nopPacketConn) SetWriteDeadline(time.Time) error { return nil } + +func init() { + proxy.RegisterProtocol(Protocol, Parse) +} diff --git a/proxy/relay.go b/proxy/relay/relay.go similarity index 81% rename from proxy/relay.go rename to proxy/relay/relay.go index b9858d16..22a6dea7 100644 --- a/proxy/relay.go +++ b/proxy/relay/relay.go @@ -1,4 +1,4 @@ -package proxy +package relay import ( "bytes" @@ -9,20 +9,25 @@ import ( "io" "math" "net" + "net/url" "sync" "github.com/go-gost/relay" + "github.com/gorilla/schema" "github.com/xjasonlyu/tun2socks/v2/common/pool" "github.com/xjasonlyu/tun2socks/v2/dialer" M "github.com/xjasonlyu/tun2socks/v2/metadata" - "github.com/xjasonlyu/tun2socks/v2/proxy/proto" + "github.com/xjasonlyu/tun2socks/v2/proxy" + "github.com/xjasonlyu/tun2socks/v2/proxy/internal" ) -var _ Proxy = (*Relay)(nil) +var _ proxy.Proxy = (*Relay)(nil) + +const Protocol = "relay" type Relay struct { - *Base + *internal.Base user string pass string @@ -30,24 +35,35 @@ type Relay struct { noDelay bool } -func NewRelay(addr, user, pass string, noDelay bool) (*Relay, error) { +func New(addr, user, pass string, noDelay bool) (*Relay, error) { return &Relay{ - Base: &Base{ - addr: addr, - proto: proto.Relay, - }, + Base: internal.New(Protocol, addr), user: user, pass: pass, noDelay: noDelay, }, nil } +func Parse(proxyURL *url.URL) (proxy.Proxy, error) { + address, username := proxyURL.Host, proxyURL.User.Username() + password, _ := proxyURL.User.Password() + + opts := struct { + NoDelay bool + }{} + if err := schema.NewDecoder().Decode(&opts, proxyURL.Query()); err != nil { + return nil, err + } + + return New(address, username, password, opts.NoDelay) +} + func (rl *Relay) DialContext(ctx context.Context, metadata *M.Metadata) (c net.Conn, err error) { return rl.dialContext(ctx, metadata) } func (rl *Relay) DialUDP(metadata *M.Metadata) (net.PacketConn, error) { - ctx, cancel := context.WithTimeout(context.Background(), tcpConnectTimeout) + ctx, cancel := context.WithTimeout(context.Background(), proxy.TCPConnectTimeout) defer cancel() return rl.dialContext(ctx, metadata) @@ -56,14 +72,14 @@ func (rl *Relay) DialUDP(metadata *M.Metadata) (net.PacketConn, error) { func (rl *Relay) dialContext(ctx context.Context, metadata *M.Metadata) (rc *relayConn, err error) { var c net.Conn - c, err = dialer.DialContext(ctx, "tcp", rl.Addr()) + c, err = dialer.DialContext(ctx, "tcp", rl.Address()) if err != nil { - return nil, fmt.Errorf("connect to %s: %w", rl.Addr(), err) + return nil, fmt.Errorf("connect to %s: %w", rl.Address(), err) } - setKeepAlive(c) + internal.SetKeepAlive(c) defer func(c net.Conn) { - safeConnClose(c, err) + internal.SafeConnClose(c, err) }(c) req := relay.Request{ @@ -250,3 +266,7 @@ func serializeRelayAddr(m *M.Metadata) *relay.AddrFeature { } return af } + +func init() { + proxy.RegisterProtocol(Protocol, Parse) +} diff --git a/proxy/shadowsocks.go b/proxy/shadowsocks/shadowsocks.go similarity index 52% rename from proxy/shadowsocks.go rename to proxy/shadowsocks/shadowsocks.go index a64b2769..f0939782 100644 --- a/proxy/shadowsocks.go +++ b/proxy/shadowsocks/shadowsocks.go @@ -1,23 +1,29 @@ -package proxy +package shadowsocks import ( "context" + "encoding/base64" "errors" "fmt" "net" + "net/url" + "strings" "github.com/xjasonlyu/tun2socks/v2/dialer" M "github.com/xjasonlyu/tun2socks/v2/metadata" - "github.com/xjasonlyu/tun2socks/v2/proxy/proto" + "github.com/xjasonlyu/tun2socks/v2/proxy" + "github.com/xjasonlyu/tun2socks/v2/proxy/internal" "github.com/xjasonlyu/tun2socks/v2/transport/shadowsocks/core" obfs "github.com/xjasonlyu/tun2socks/v2/transport/simple-obfs" "github.com/xjasonlyu/tun2socks/v2/transport/socks5" ) -var _ Proxy = (*Shadowsocks)(nil) +var _ proxy.Proxy = (*Shadowsocks)(nil) + +const Protocol = "ss" type Shadowsocks struct { - *Base + *internal.Base cipher core.Cipher @@ -25,44 +31,82 @@ type Shadowsocks struct { obfsMode, obfsHost string } -func NewShadowsocks(addr, method, password, obfsMode, obfsHost string) (*Shadowsocks, error) { +func New(addr, method, password, obfsMode, obfsHost string) (*Shadowsocks, error) { cipher, err := core.PickCipher(method, nil, password) if err != nil { return nil, fmt.Errorf("ss initialize: %w", err) } return &Shadowsocks{ - Base: &Base{ - addr: addr, - proto: proto.Shadowsocks, - }, + Base: internal.New(Protocol, addr), cipher: cipher, obfsMode: obfsMode, obfsHost: obfsHost, }, nil } +func Parse(proxyURL *url.URL) (proxy.Proxy, error) { + var ( + address = proxyURL.Host + method, password string + obfsMode, obfsHost string + ) + + if ss := proxyURL.User.String(); ss == "" { + method = "dummy" // none cipher mode + } else if pass, set := proxyURL.User.Password(); set { + method = proxyURL.User.Username() + password = pass + } else { + data, _ := base64.RawURLEncoding.DecodeString(ss) + userInfo := strings.SplitN(string(data), ":", 2) + if len(userInfo) == 2 { + method = userInfo[0] + password = userInfo[1] + } + } + + rawQuery, _ := url.QueryUnescape(proxyURL.RawQuery) + for _, s := range strings.Split(rawQuery, ";") { + data := strings.SplitN(s, "=", 2) + if len(data) != 2 { + continue + } + key := data[0] + value := data[1] + + switch key { + case "obfs": + obfsMode = value + case "obfs-host": + obfsHost = value + } + } + + return New(address, method, password, obfsMode, obfsHost) +} + func (ss *Shadowsocks) DialContext(ctx context.Context, metadata *M.Metadata) (c net.Conn, err error) { - c, err = dialer.DialContext(ctx, "tcp", ss.Addr()) + c, err = dialer.DialContext(ctx, "tcp", ss.Address()) if err != nil { - return nil, fmt.Errorf("connect to %s: %w", ss.Addr(), err) + return nil, fmt.Errorf("connect to %s: %w", ss.Address(), err) } - setKeepAlive(c) + internal.SetKeepAlive(c) defer func(c net.Conn) { - safeConnClose(c, err) + internal.SafeConnClose(c, err) }(c) switch ss.obfsMode { case "tls": c = obfs.NewTLSObfs(c, ss.obfsHost) case "http": - _, port, _ := net.SplitHostPort(ss.addr) + _, port, _ := net.SplitHostPort(ss.Address()) c = obfs.NewHTTPObfs(c, ss.obfsHost, port) } c = ss.cipher.StreamConn(c) - _, err = c.Write(serializeSocksAddr(metadata)) + _, err = c.Write(internal.SerializeSocksAddr(metadata)) return } @@ -72,9 +116,9 @@ func (ss *Shadowsocks) DialUDP(*M.Metadata) (net.PacketConn, error) { return nil, fmt.Errorf("listen packet: %w", err) } - udpAddr, err := net.ResolveUDPAddr("udp", ss.Addr()) + udpAddr, err := net.ResolveUDPAddr("udp", ss.Address()) if err != nil { - return nil, fmt.Errorf("resolve udp address %s: %w", ss.Addr(), err) + return nil, fmt.Errorf("resolve udp address %s: %w", ss.Address(), err) } pc = ss.cipher.PacketConn(pc) @@ -90,7 +134,7 @@ type ssPacketConn struct { func (pc *ssPacketConn) WriteTo(b []byte, addr net.Addr) (n int, err error) { var packet []byte if ma, ok := addr.(*M.Addr); ok { - packet, err = socks5.EncodeUDPPacket(serializeSocksAddr(ma.Metadata()), b) + packet, err = socks5.EncodeUDPPacket(internal.SerializeSocksAddr(ma.Metadata()), b) } else { packet, err = socks5.EncodeUDPPacket(socks5.ParseAddr(addr), b) } @@ -120,3 +164,7 @@ func (pc *ssPacketConn) ReadFrom(b []byte) (int, net.Addr, error) { copy(b, b[len(addr):]) return n - len(addr), udpAddr, err } + +func init() { + proxy.RegisterProtocol(Protocol, Parse) +} diff --git a/proxy/socks4.go b/proxy/socks4.go deleted file mode 100644 index d8501172..00000000 --- a/proxy/socks4.go +++ /dev/null @@ -1,45 +0,0 @@ -package proxy - -import ( - "context" - "fmt" - "net" - - "github.com/xjasonlyu/tun2socks/v2/dialer" - M "github.com/xjasonlyu/tun2socks/v2/metadata" - "github.com/xjasonlyu/tun2socks/v2/proxy/proto" - "github.com/xjasonlyu/tun2socks/v2/transport/socks4" -) - -var _ Proxy = (*Socks4)(nil) - -type Socks4 struct { - *Base - - userID string -} - -func NewSocks4(addr, userID string) (*Socks4, error) { - return &Socks4{ - Base: &Base{ - addr: addr, - proto: proto.Socks4, - }, - userID: userID, - }, nil -} - -func (ss *Socks4) DialContext(ctx context.Context, metadata *M.Metadata) (c net.Conn, err error) { - c, err = dialer.DialContext(ctx, "tcp", ss.Addr()) - if err != nil { - return nil, fmt.Errorf("connect to %s: %w", ss.Addr(), err) - } - setKeepAlive(c) - - defer func(c net.Conn) { - safeConnClose(c, err) - }(c) - - err = socks4.ClientHandshake(c, metadata.DestinationAddress(), socks4.CmdConnect, ss.userID) - return -} diff --git a/proxy/socks4/socks4.go b/proxy/socks4/socks4.go new file mode 100644 index 00000000..ec43f0a5 --- /dev/null +++ b/proxy/socks4/socks4.go @@ -0,0 +1,55 @@ +package socks4 + +import ( + "context" + "fmt" + "net" + "net/url" + + "github.com/xjasonlyu/tun2socks/v2/dialer" + M "github.com/xjasonlyu/tun2socks/v2/metadata" + "github.com/xjasonlyu/tun2socks/v2/proxy" + "github.com/xjasonlyu/tun2socks/v2/proxy/internal" + "github.com/xjasonlyu/tun2socks/v2/transport/socks4" +) + +var _ proxy.Proxy = (*Socks4)(nil) + +const Protocol = "socks4" + +type Socks4 struct { + *internal.Base + + userID string +} + +func New(addr, userID string) (*Socks4, error) { + return &Socks4{ + Base: internal.New(Protocol, addr), + userID: userID, + }, nil +} + +func Parse(proxyURL *url.URL) (proxy.Proxy, error) { + address, userID := proxyURL.Host, proxyURL.User.Username() + return New(address, userID) +} + +func (ss *Socks4) DialContext(ctx context.Context, metadata *M.Metadata) (c net.Conn, err error) { + c, err = dialer.DialContext(ctx, "tcp", ss.Address()) + if err != nil { + return nil, fmt.Errorf("connect to %s: %w", ss.Address(), err) + } + internal.SetKeepAlive(c) + + defer func(c net.Conn) { + internal.SafeConnClose(c, err) + }(c) + + err = socks4.ClientHandshake(c, metadata.DestinationAddress(), socks4.CmdConnect, ss.userID) + return +} + +func init() { + proxy.RegisterProtocol(Protocol, Parse) +} diff --git a/proxy/socks5.go b/proxy/socks5/socks5.go similarity index 72% rename from proxy/socks5.go rename to proxy/socks5/socks5.go index bdc9b04c..241f8ba2 100644 --- a/proxy/socks5.go +++ b/proxy/socks5/socks5.go @@ -1,4 +1,4 @@ -package proxy +package socks5 import ( "context" @@ -6,17 +6,21 @@ import ( "fmt" "io" "net" + "net/url" "github.com/xjasonlyu/tun2socks/v2/dialer" M "github.com/xjasonlyu/tun2socks/v2/metadata" - "github.com/xjasonlyu/tun2socks/v2/proxy/proto" + "github.com/xjasonlyu/tun2socks/v2/proxy" + "github.com/xjasonlyu/tun2socks/v2/proxy/internal" "github.com/xjasonlyu/tun2socks/v2/transport/socks5" ) -var _ Proxy = (*Socks5)(nil) +var _ proxy.Proxy = (*Socks5)(nil) + +const Protocol = "socks5" type Socks5 struct { - *Base + *internal.Base user string pass string @@ -25,32 +29,40 @@ type Socks5 struct { unix bool } -func NewSocks5(addr, user, pass string) (*Socks5, error) { +func New(addr, user, pass string) (*Socks5, error) { return &Socks5{ - Base: &Base{ - addr: addr, - proto: proto.Socks5, - }, + Base: internal.New(Protocol, addr), user: user, pass: pass, unix: len(addr) > 0 && addr[0] == '/', }, nil } +func Parse(proxyURL *url.URL) (proxy.Proxy, error) { + address, username := proxyURL.Host, proxyURL.User.Username() + password, _ := proxyURL.User.Password() + + // Socks5 over UDS + if address == "" { + address = proxyURL.Path + } + return New(address, username, password) +} + func (ss *Socks5) DialContext(ctx context.Context, metadata *M.Metadata) (c net.Conn, err error) { network := "tcp" if ss.unix { network = "unix" } - c, err = dialer.DialContext(ctx, network, ss.Addr()) + c, err = dialer.DialContext(ctx, network, ss.Address()) if err != nil { - return nil, fmt.Errorf("connect to %s: %w", ss.Addr(), err) + return nil, fmt.Errorf("connect to %s: %w", ss.Address(), err) } - setKeepAlive(c) + internal.SetKeepAlive(c) defer func(c net.Conn) { - safeConnClose(c, err) + internal.SafeConnClose(c, err) }(c) var user *socks5.User @@ -61,7 +73,7 @@ func (ss *Socks5) DialContext(ctx context.Context, metadata *M.Metadata) (c net. } } - _, err = socks5.ClientHandshake(c, serializeSocksAddr(metadata), socks5.CmdConnect, user) + _, err = socks5.ClientHandshake(c, internal.SerializeSocksAddr(metadata), socks5.CmdConnect, user) return } @@ -70,15 +82,15 @@ func (ss *Socks5) DialUDP(*M.Metadata) (_ net.PacketConn, err error) { return nil, fmt.Errorf("%w when unix domain socket is enabled", errors.ErrUnsupported) } - ctx, cancel := context.WithTimeout(context.Background(), tcpConnectTimeout) + ctx, cancel := context.WithTimeout(context.Background(), proxy.TCPConnectTimeout) defer cancel() - c, err := dialer.DialContext(ctx, "tcp", ss.Addr()) + c, err := dialer.DialContext(ctx, "tcp", ss.Address()) if err != nil { - err = fmt.Errorf("connect to %s: %w", ss.Addr(), err) + err = fmt.Errorf("connect to %s: %w", ss.Address(), err) return } - setKeepAlive(c) + internal.SetKeepAlive(c) defer func() { if err != nil && c != nil { @@ -128,9 +140,9 @@ func (ss *Socks5) DialUDP(*M.Metadata) (_ net.PacketConn, err error) { } if bindAddr.IP.IsUnspecified() { /* e.g. "0.0.0.0" or "::" */ - udpAddr, err := net.ResolveUDPAddr("udp", ss.Addr()) + udpAddr, err := net.ResolveUDPAddr("udp", ss.Address()) if err != nil { - return nil, fmt.Errorf("resolve udp address %s: %w", ss.Addr(), err) + return nil, fmt.Errorf("resolve udp address %s: %w", ss.Address(), err) } bindAddr.IP = udpAddr.IP } @@ -148,7 +160,7 @@ type socksPacketConn struct { func (pc *socksPacketConn) WriteTo(b []byte, addr net.Addr) (n int, err error) { var packet []byte if ma, ok := addr.(*M.Addr); ok { - packet, err = socks5.EncodeUDPPacket(serializeSocksAddr(ma.Metadata()), b) + packet, err = socks5.EncodeUDPPacket(internal.SerializeSocksAddr(ma.Metadata()), b) } else { packet, err = socks5.EncodeUDPPacket(socks5.ParseAddr(addr), b) } @@ -185,6 +197,6 @@ func (pc *socksPacketConn) Close() error { return pc.PacketConn.Close() } -func serializeSocksAddr(m *M.Metadata) socks5.Addr { - return socks5.SerializeAddr("", m.DstIP, m.DstPort) +func init() { + proxy.RegisterProtocol(Protocol, Parse) } diff --git a/proxy/util.go b/proxy/util.go deleted file mode 100644 index fdf77292..00000000 --- a/proxy/util.go +++ /dev/null @@ -1,25 +0,0 @@ -package proxy - -import ( - "net" - "time" -) - -const ( - tcpKeepAlivePeriod = 30 * time.Second -) - -// setKeepAlive sets tcp keepalive option for tcp connection. -func setKeepAlive(c net.Conn) { - if tcp, ok := c.(*net.TCPConn); ok { - tcp.SetKeepAlive(true) - tcp.SetKeepAlivePeriod(tcpKeepAlivePeriod) - } -} - -// safeConnClose closes tcp connection safely. -func safeConnClose(c net.Conn, err error) { - if c != nil && err != nil { - c.Close() - } -}