diff --git a/gost/chain.go b/gost/chain.go index a798cbc..c2b9fae 100644 --- a/gost/chain.go +++ b/gost/chain.go @@ -9,6 +9,12 @@ type Chain struct { Nodes []Node } +func NewChain(nodes ...Node) *Chain { + return &Chain{ + Nodes: nodes, + } +} + func (c *Chain) Dial(ctx context.Context, addr string) (net.Conn, error) { if len(c.Nodes) == 0 { return net.Dial("tcp", addr) @@ -20,23 +26,35 @@ func (c *Chain) Dial(ctx context.Context, addr string) (net.Conn, error) { return nil, err } + conn, err = nodes[0].Client.Handshake(ctx, conn) + if err != nil { + return nil, err + } + for i, node := range nodes { if i == len(nodes)-1 { break } - cn, err := node.Client.Connect(ctx, conn, nodes[i+1].Addr) + next := nodes[i+1] + cc, err := node.Client.Connect(ctx, conn, next.Addr) if err != nil { conn.Close() return nil, err } - conn = cn + cc, err = next.Client.Handshake(ctx, cc) + if err != nil { + conn.Close() + return nil, err + } + + conn = cc } - cn, err := nodes[len(nodes)-1].Client.Connect(ctx, conn, addr) + cc, err := nodes[len(nodes)-1].Client.Connect(ctx, conn, addr) if err != nil { conn.Close() return nil, err } - return cn, nil + return cc, nil } diff --git a/gost/cli/cli.go b/gost/cli/cli.go new file mode 100644 index 0000000..142d960 --- /dev/null +++ b/gost/cli/cli.go @@ -0,0 +1,66 @@ +package main + +import ( + "bufio" + "context" + "crypto/tls" + "log" + "net/http" + "net/http/httputil" + "net/url" + + "github.com/ginuerzh/gost/gost" +) + +func init() { + log.SetFlags(log.LstdFlags | log.Lshortfile) + gost.Debug = true +} + +func main() { + chain := gost.NewChain( + gost.Node{ + Addr: "127.0.0.1:1080", + Client: gost.NewClient( + gost.HTTPConnector(url.UserPassword("admin", "123456")), + gost.TCPTransporter(), + ), + }, + gost.Node{ + Addr: "172.24.222.54:8338", + Client: gost.NewClient( + gost.ShadowConnector(url.UserPassword("chacha20", "123456")), + gost.TCPTransporter(), + ), + }, + gost.Node{ + Addr: "172.24.222.54:8080", + Client: gost.NewClient( + gost.SOCKS5Connector(url.UserPassword("cmdsh", "cmdsh123456")), + gost.TCPTransporter(), + ), + }, + ) + conn, err := chain.Dial(context.Background(), "baidu.com:443") + if err != nil { + log.Fatal(err) + } + conn = tls.Client(conn, &tls.Config{InsecureSkipVerify: true}) + req, err := http.NewRequest(http.MethodGet, "https://www.baidu.com", nil) + if err != nil { + log.Fatal(err) + } + if err := req.Write(conn); err != nil { + log.Fatal(err) + } + resp, err := http.ReadResponse(bufio.NewReader(conn), req) + if err != nil { + log.Fatal(err) + } + defer resp.Body.Close() + + rb, _ := httputil.DumpRequest(req, true) + log.Println(string(rb)) + rb, _ = httputil.DumpResponse(resp, true) + log.Println(string(rb)) +} diff --git a/gost/client.go b/gost/client.go index 8763f0a..0b2c407 100644 --- a/gost/client.go +++ b/gost/client.go @@ -1,167 +1,58 @@ package gost import ( - "bufio" "context" - "crypto/tls" - "encoding/base64" - "errors" - "fmt" "net" - "net/http" - "net/http/httputil" - "net/url" - "strconv" - - "github.com/ginuerzh/gosocks4" - "github.com/ginuerzh/gosocks5" - "github.com/go-log/log" - ss "github.com/shadowsocks/shadowsocks-go/shadowsocks" ) type Client struct { - Protocol string - Transport *Transport - User *url.Userinfo + Connector Connector + Transporter Transporter } +// DefaultClient is a standard HTTP proxy +var DefaultClient = NewClient(HTTPConnector(nil), TCPTransporter()) + +func NewClient(c Connector, tr Transporter) *Client { + return &Client{ + Connector: c, + Transporter: tr, + } +} + +// Dial connects to the target address func (c *Client) Dial(ctx context.Context, addr string) (net.Conn, error) { - return c.Transport.Dial(ctx, addr) + return net.Dial(c.Transporter.Network(), addr) +} + +func (c *Client) Handshake(ctx context.Context, conn net.Conn) (net.Conn, error) { + return c.Transporter.Handshake(ctx, conn) } func (c *Client) Connect(ctx context.Context, conn net.Conn, addr string) (net.Conn, error) { - protocol := c.Protocol + return c.Connector.Connect(ctx, conn, addr) +} - switch protocol { - case "ss": // shadowsocks - rawaddr, err := ss.RawAddr(addr) - if err != nil { - return nil, err - } +type Connector interface { + Connect(ctx context.Context, conn net.Conn, addr string) (net.Conn, error) +} - var method, password string - if c.User != nil { - method = c.User.Username() - password, _ = c.User.Password() - } +type Transporter interface { + Network() string + Handshake(ctx context.Context, conn net.Conn) (net.Conn, error) +} - cipher, err := ss.NewCipher(method, password) - if err != nil { - return nil, err - } +type tcpTransporter struct { +} - sc, err := ss.DialWithRawAddrConn(rawaddr, conn, cipher) - if err != nil { - return nil, err - } - conn = ShadowConn(sc) +func TCPTransporter() Transporter { + return &tcpTransporter{} +} - case "socks", "socks5": - host, port, err := net.SplitHostPort(addr) - if err != nil { - return nil, err - } - p, _ := strconv.Atoi(port) - req := gosocks5.NewRequest(gosocks5.CmdConnect, &gosocks5.Addr{ - Type: gosocks5.AddrDomain, - Host: host, - Port: uint16(p), - }) - if err := req.Write(conn); err != nil { - return nil, err - } - log.Log("[socks5]", req) - - reply, err := gosocks5.ReadReply(conn) - if err != nil { - return nil, err - } - log.Log("[socks5]", reply) - if reply.Rep != gosocks5.Succeeded { - return nil, errors.New("Service unavailable") - } - - case "socks4", "socks4a": - atype := gosocks4.AddrDomain - host, port, err := net.SplitHostPort(addr) - if err != nil { - return nil, err - } - p, _ := strconv.Atoi(port) - - if protocol == "socks4" { - taddr, err := net.ResolveTCPAddr("tcp4", addr) - if err != nil { - return nil, err - } - host = taddr.IP.String() - p = taddr.Port - atype = gosocks4.AddrIPv4 - } - req := gosocks4.NewRequest(gosocks4.CmdConnect, - &gosocks4.Addr{Type: atype, Host: host, Port: uint16(p)}, nil) - if err := req.Write(conn); err != nil { - return nil, err - } - log.Logf("[%s] %s", protocol, req) - - reply, err := gosocks4.ReadReply(conn) - if err != nil { - return nil, err - } - log.Logf("[%s] %s", protocol, reply) - - if reply.Code != gosocks4.Granted { - return nil, fmt.Errorf("%s: code=%d", protocol, reply.Code) - } - case "http": - fallthrough - default: - req := &http.Request{ - Method: http.MethodConnect, - URL: &url.URL{Host: addr}, - Host: addr, - ProtoMajor: 1, - ProtoMinor: 1, - Header: make(http.Header), - } - req.Header.Set("Proxy-Connection", "keep-alive") - if c.User != nil { - s := c.User.String() - if _, set := c.User.Password(); !set { - s += ":" - } - req.Header.Set("Proxy-Authorization", - "Basic "+base64.StdEncoding.EncodeToString([]byte(s))) - } - if err := req.Write(conn); err != nil { - return nil, err - } - - if Debug { - dump, _ := httputil.DumpRequest(req, false) - log.Log(string(dump)) - } - - resp, err := http.ReadResponse(bufio.NewReader(conn), req) - if err != nil { - return nil, err - } - - if Debug { - dump, _ := httputil.DumpResponse(resp, false) - log.Log(string(dump)) - } - - if resp.StatusCode != http.StatusOK { - return nil, fmt.Errorf("%d %s", resp.StatusCode, resp.Status) - } - } +func (tr *tcpTransporter) Network() string { + return "tcp" +} +func (tr *tcpTransporter) Handshake(ctx context.Context, conn net.Conn) (net.Conn, error) { return conn, nil } - -type Transport struct { - Dial func(ctx context.Context, addr string) (net.Conn, error) - TLSClientConfig *tls.Config -} diff --git a/gost/gost.go b/gost/gost.go index 12bf836..f57b59b 100644 --- a/gost/gost.go +++ b/gost/gost.go @@ -1,3 +1,11 @@ package gost +import ( + "github.com/go-log/log" +) + var Debug bool + +func init() { + log.DefaultLogger = &logger{} +} diff --git a/gost/http.go b/gost/http.go new file mode 100644 index 0000000..03c8f74 --- /dev/null +++ b/gost/http.go @@ -0,0 +1,68 @@ +package gost + +import ( + "bufio" + "context" + "encoding/base64" + "fmt" + "net" + "net/http" + "net/http/httputil" + "net/url" + + "github.com/go-log/log" +) + +type httpConnector struct { + User *url.Userinfo +} + +func HTTPConnector(user *url.Userinfo) Connector { + return &httpConnector{User: user} +} + +func (c *httpConnector) Connect(ctx context.Context, conn net.Conn, addr string) (net.Conn, error) { + req := &http.Request{ + Method: http.MethodConnect, + URL: &url.URL{Host: addr}, + Host: addr, + ProtoMajor: 1, + ProtoMinor: 1, + Header: make(http.Header), + } + req.Header.Set("Proxy-Connection", "keep-alive") + + if c.User != nil { + s := c.User.String() + if _, set := c.User.Password(); !set { + s += ":" + } + req.Header.Set("Proxy-Authorization", + "Basic "+base64.StdEncoding.EncodeToString([]byte(s))) + } + + if err := req.Write(conn); err != nil { + return nil, err + } + + if Debug { + dump, _ := httputil.DumpRequest(req, false) + log.Log(string(dump)) + } + + resp, err := http.ReadResponse(bufio.NewReader(conn), req) + if err != nil { + return nil, err + } + + if Debug { + dump, _ := httputil.DumpResponse(resp, false) + log.Log(string(dump)) + } + + if resp.StatusCode != http.StatusOK { + return nil, fmt.Errorf("%s", resp.Status) + } + + return conn, nil +} diff --git a/gost/log.go b/gost/log.go new file mode 100644 index 0000000..b55303e --- /dev/null +++ b/gost/log.go @@ -0,0 +1,18 @@ +package gost + +import "log" + +func init() { + log.SetFlags(log.LstdFlags | log.Lshortfile) +} + +type logger struct { +} + +func (l *logger) Log(v ...interface{}) { + log.Println(v...) +} + +func (l *logger) Logf(format string, v ...interface{}) { + log.Printf(format, v...) +} diff --git a/gost/node.go b/gost/node.go index db82c79..8c0aaa9 100644 --- a/gost/node.go +++ b/gost/node.go @@ -1,6 +1,8 @@ package gost type Node struct { - Addr string - Client *Client + Addr string + Protocol string + Transport string + Client *Client } diff --git a/gost/socks.go b/gost/socks.go new file mode 100644 index 0000000..1c079e4 --- /dev/null +++ b/gost/socks.go @@ -0,0 +1,225 @@ +package gost + +import ( + "context" + "crypto/tls" + "errors" + "fmt" + "net" + "net/url" + "strconv" + + "github.com/ginuerzh/gosocks4" + "github.com/ginuerzh/gosocks5" + "github.com/go-log/log" +) + +const ( + MethodTLS uint8 = 0x80 // extended method for tls + MethodTLSAuth uint8 = 0x82 // extended method for tls+auth +) + +const ( + CmdUdpTun uint8 = 0xF3 // extended method for udp over tcp +) + +type ClientSelector struct { + methods []uint8 + User *url.Userinfo + TLSConfig *tls.Config +} + +func (selector *ClientSelector) Methods() []uint8 { + return selector.methods +} + +func (selector *ClientSelector) AddMethod(methods ...uint8) { + selector.methods = append(selector.methods, methods...) +} + +func (selector *ClientSelector) Select(methods ...uint8) (method uint8) { + return +} + +func (selector *ClientSelector) OnSelected(method uint8, conn net.Conn) (net.Conn, error) { + switch method { + case MethodTLS: + conn = tls.Client(conn, selector.TLSConfig) + + case gosocks5.MethodUserPass, MethodTLSAuth: + if method == MethodTLSAuth { + conn = tls.Client(conn, selector.TLSConfig) + } + + var username, password string + if selector.User != nil { + username = selector.User.Username() + password, _ = selector.User.Password() + } + + req := gosocks5.NewUserPassRequest(gosocks5.UserPassVer, username, password) + if err := req.Write(conn); err != nil { + log.Log("[socks5]", err) + return nil, err + } + if Debug { + log.Log("[socks5]", req) + } + resp, err := gosocks5.ReadUserPassResponse(conn) + if err != nil { + log.Log("[socks5]", err) + return nil, err + } + if Debug { + log.Log("[socks5]", resp) + } + if resp.Status != gosocks5.Succeeded { + return nil, gosocks5.ErrAuthFailure + } + case gosocks5.MethodNoAcceptable: + return nil, gosocks5.ErrBadMethod + } + + return conn, nil +} + +type socks5Connector struct { + User *url.Userinfo +} + +func SOCKS5Connector(user *url.Userinfo) Connector { + return &socks5Connector{User: user} +} + +func (c *socks5Connector) Connect(ctx context.Context, conn net.Conn, addr string) (net.Conn, error) { + selector := &ClientSelector{ + TLSConfig: &tls.Config{InsecureSkipVerify: true}, + User: c.User, + } + selector.AddMethod( + gosocks5.MethodNoAuth, + gosocks5.MethodUserPass, + MethodTLS, + ) + + cc := gosocks5.ClientConn(conn, selector) + if err := cc.Handleshake(); err != nil { + return nil, err + } + conn = cc + + host, port, err := net.SplitHostPort(addr) + if err != nil { + return nil, err + } + p, _ := strconv.Atoi(port) + req := gosocks5.NewRequest(gosocks5.CmdConnect, &gosocks5.Addr{ + Type: gosocks5.AddrDomain, + Host: host, + Port: uint16(p), + }) + if err := req.Write(conn); err != nil { + return nil, err + } + + if Debug { + log.Log("[socks5]", req) + } + + reply, err := gosocks5.ReadReply(conn) + if err != nil { + return nil, err + } + + if Debug { + log.Log("[socks5]", reply) + } + + if reply.Rep != gosocks5.Succeeded { + return nil, errors.New("Service unavailable") + } + + return conn, nil +} + +type socks4Connector struct{} + +func SOCKS4Connector() Connector { + return &socks4Connector{} +} + +func (c *socks4Connector) Connect(ctx context.Context, conn net.Conn, addr string) (net.Conn, error) { + taddr, err := net.ResolveTCPAddr("tcp4", addr) + if err != nil { + return nil, err + } + + req := gosocks4.NewRequest(gosocks4.CmdConnect, + &gosocks4.Addr{ + Type: gosocks4.AddrIPv4, + Host: taddr.IP.String(), + Port: uint16(taddr.Port), + }, nil, + ) + if err := req.Write(conn); err != nil { + return nil, err + } + + if Debug { + log.Logf("[socks4] %s", req) + } + + reply, err := gosocks4.ReadReply(conn) + if err != nil { + return nil, err + } + + if Debug { + log.Logf("[socks4] %s", reply) + } + + if reply.Code != gosocks4.Granted { + return nil, fmt.Errorf("[socks4] %d", reply.Code) + } + + return conn, nil +} + +type socks4aConnector struct{} + +func SOCKS4AConnector() Connector { + return &socks4aConnector{} +} + +func (c *socks4aConnector) Connect(ctx context.Context, conn net.Conn, addr string) (net.Conn, error) { + host, port, err := net.SplitHostPort(addr) + if err != nil { + return nil, err + } + p, _ := strconv.Atoi(port) + + req := gosocks4.NewRequest(gosocks4.CmdConnect, + &gosocks4.Addr{Type: gosocks4.AddrDomain, Host: host, Port: uint16(p)}, nil) + if err := req.Write(conn); err != nil { + return nil, err + } + + if Debug { + log.Logf("[socks4] %s", req) + } + + reply, err := gosocks4.ReadReply(conn) + if err != nil { + return nil, err + } + + if Debug { + log.Logf("[socks4] %s", reply) + } + + if reply.Code != gosocks4.Granted { + return nil, fmt.Errorf("[socks4] %d", reply.Code) + } + + return conn, nil +} diff --git a/gost/ss.go b/gost/ss.go index 49b6483..34cdd68 100644 --- a/gost/ss.go +++ b/gost/ss.go @@ -1,8 +1,12 @@ package gost import ( + "context" "net" + "net/url" "time" + + ss "github.com/shadowsocks/shadowsocks-go/shadowsocks" ) // Due to in/out byte length is inconsistent of the shadowsocks.Conn.Write, @@ -48,3 +52,35 @@ func (c *shadowConn) SetReadDeadline(t time.Time) error { func (c *shadowConn) SetWriteDeadline(t time.Time) error { return c.conn.SetWriteDeadline(t) } + +type shadowConnector struct { + Cipher *url.Userinfo +} + +func ShadowConnector(cipher *url.Userinfo) Connector { + return &shadowConnector{Cipher: cipher} +} + +func (c *shadowConnector) Connect(ctx context.Context, conn net.Conn, addr string) (net.Conn, error) { + rawaddr, err := ss.RawAddr(addr) + if err != nil { + return nil, err + } + + var method, password string + if c.Cipher != nil { + method = c.Cipher.Username() + password, _ = c.Cipher.Password() + } + + cipher, err := ss.NewCipher(method, password) + if err != nil { + return nil, err + } + + sc, err := ss.DialWithRawAddrConn(rawaddr, conn, cipher) + if err != nil { + return nil, err + } + return &shadowConn{conn: sc}, nil +} diff --git a/gost/tls.go b/gost/tls.go new file mode 100644 index 0000000..798fc3e --- /dev/null +++ b/gost/tls.go @@ -0,0 +1,23 @@ +package gost + +import ( + "context" + "crypto/tls" + "net" +) + +type tlsTransporter struct { + TLSClientConfig *tls.Config +} + +func TLSTransporter(cfg *tls.Config) Transporter { + return &tlsTransporter{TLSClientConfig: cfg} +} + +func (tr *tlsTransporter) Network() string { + return "tcp" +} + +func (tr *tlsTransporter) Handshake(ctx context.Context, conn net.Conn) (net.Conn, error) { + return tls.Client(conn, tr.TLSClientConfig), nil +} diff --git a/tcp/client.go b/tcp/client.go deleted file mode 100644 index 447a981..0000000 --- a/tcp/client.go +++ /dev/null @@ -1,202 +0,0 @@ -package tcp - -import ( - "bufio" - "crypto/tls" - "encoding/base64" - "errors" - "fmt" - "net" - "net/http" - "net/http/httputil" - "net/url" - "strconv" - - "github.com/ginuerzh/gosocks4" - "github.com/ginuerzh/gosocks5" - "github.com/ginuerzh/gost" - "github.com/ginuerzh/gost/client" - "github.com/go-log/log" - ss "github.com/shadowsocks/shadowsocks-go/shadowsocks" -) - -type nodeClient struct { - options *client.Options -} - -func (c *nodeClient) Init(opts ...client.Option) { - for _, opt := range opts { - opt(c.options) - } -} - -func (c *nodeClient) Options() *Options { - return c.options -} - -func (c *nodeClient) Connect() (net.Conn, error) { - return net.Dial("tcp", c.options.Addr) -} - -func (c *nodeClient) Handshake(conn net.Conn) (net.Conn, error) { - return conn, nil -} - -func (c *nodeClient) Dial(conn net.Conn, addr string) (net.Conn, error) { - if c.options.Protocol == "socks5" { - selector := &gost.ClientSelector{ - TLSConfig: &tls.Config{ - InsecureSkipVerify: !c.options.SecureVerify, - ServerName: c.options.ServerName, - }, - } - selector.AddMethod( - gosocks5.MethodNoAuth, - gosocks5.MethodUserPass, - gost.MethodTLS, - ) - users := c.options.Users - if len(users) > 0 { - selector.User = &users[0] - } - - cc := gosocks5.ClientConn(conn, selector) - if err := cc.Handleshake(); err != nil { - return nil, err - } - conn = cc - } - - return c.dial(conn, addr) -} - -func (c *nodeClient) dial(conn net.Conn, addr string) (net.Conn, error) { - protocol := c.options.Protocol - switch protocol { - case "ss": // shadowsocks - rawaddr, err := ss.RawAddr(addr) - if err != nil { - return nil, err - } - - var method, password string - users := c.options.Users - if len(users) > 0 { - method = users[0].Username() - password, _ = users[0].Password() - } - - cipher, err := ss.NewCipher(method, password) - if err != nil { - return nil, err - } - - sc, err := ss.DialWithRawAddrConn(rawaddr, conn, cipher) - if err != nil { - return nil, err - } - conn = gost.ShadowConn(sc) - - case "socks", "socks5": - host, port, err := net.SplitHostPort(addr) - if err != nil { - return nil, err - } - p, _ := strconv.Atoi(port) - req := gosocks5.NewRequest(gosocks5.CmdConnect, &gosocks5.Addr{ - Type: gosocks5.AddrDomain, - Host: host, - Port: uint16(p), - }) - if err := req.Write(conn); err != nil { - return nil, err - } - log.Log("[socks5]", req) - - reply, err := gosocks5.ReadReply(conn) - if err != nil { - return nil, err - } - log.Log("[socks5]", reply) - if reply.Rep != gosocks5.Succeeded { - return nil, errors.New("Service unavailable") - } - - case "socks4", "socks4a": - atype := gosocks4.AddrDomain - host, port, err := net.SplitHostPort(addr) - if err != nil { - return nil, err - } - p, _ := strconv.Atoi(port) - - if protocol == "socks4" { - taddr, err := net.ResolveTCPAddr("tcp4", addr) - if err != nil { - return nil, err - } - host = taddr.IP.String() - p = taddr.Port - atype = gosocks4.AddrIPv4 - } - req := gosocks4.NewRequest(gosocks4.CmdConnect, - &gosocks4.Addr{Type: atype, Host: host, Port: uint16(p)}, nil) - if err := req.Write(conn); err != nil { - return nil, err - } - log.Logf("[%s] %s", protocol, req) - - reply, err := gosocks4.ReadReply(conn) - if err != nil { - return nil, err - } - log.Logf("[%s] %s", protocol, reply) - - if reply.Code != gosocks4.Granted { - return nil, fmt.Errorf("%s: code=%d", protocol, reply.Code) - } - case "http": - fallthrough - default: - req := &http.Request{ - Method: http.MethodConnect, - URL: &url.URL{Host: addr}, - Host: addr, - ProtoMajor: 1, - ProtoMinor: 1, - Header: make(http.Header), - } - req.Header.Set("Proxy-Connection", "keep-alive") - users := c.options.Users - if len(users) > 0 { - user := users[0] - s := user.String() - if _, set := user.Password(); !set { - s += ":" - } - req.Header.Set("Proxy-Authorization", - "Basic "+base64.StdEncoding.EncodeToString([]byte(s))) - } - if err := req.Write(conn); err != nil { - return nil, err - } - //if glog.V(LDEBUG) { - dump, _ := httputil.DumpRequest(req, false) - log.Log(string(dump)) - //} - - resp, err := http.ReadResponse(bufio.NewReader(conn), req) - if err != nil { - return nil, err - } - //if glog.V(LDEBUG) { - dump, _ = httputil.DumpResponse(resp, false) - log.Log(string(dump)) - //} - if resp.StatusCode != http.StatusOK { - return nil, fmt.Errorf("%d %s", resp.StatusCode, resp.Status) - } - } - - return conn, nil -}