redesign client

This commit is contained in:
rui.zheng 2017-07-21 18:14:31 +08:00
parent 962fd9df51
commit 289acfd058
11 changed files with 506 additions and 353 deletions

View File

@ -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
}
cn, err := nodes[len(nodes)-1].Client.Connect(ctx, conn, addr)
conn = cc
}
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
}

66
gost/cli/cli.go Normal file
View File

@ -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))
}

View File

@ -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
switch protocol {
case "ss": // shadowsocks
rawaddr, err := ss.RawAddr(addr)
if err != nil {
return nil, err
return c.Connector.Connect(ctx, conn, addr)
}
var method, password string
if c.User != nil {
method = c.User.Username()
password, _ = c.User.Password()
type Connector interface {
Connect(ctx context.Context, conn net.Conn, addr string) (net.Conn, error)
}
cipher, err := ss.NewCipher(method, password)
if err != nil {
return nil, err
type Transporter interface {
Network() string
Handshake(ctx context.Context, conn net.Conn) (net.Conn, error)
}
sc, err := ss.DialWithRawAddrConn(rawaddr, conn, cipher)
if err != nil {
return nil, err
}
conn = 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")
type tcpTransporter struct {
}
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
func TCPTransporter() Transporter {
return &tcpTransporter{}
}
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
}

View File

@ -1,3 +1,11 @@
package gost
import (
"github.com/go-log/log"
)
var Debug bool
func init() {
log.DefaultLogger = &logger{}
}

68
gost/http.go Normal file
View File

@ -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
}

18
gost/log.go Normal file
View File

@ -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...)
}

View File

@ -2,5 +2,7 @@ package gost
type Node struct {
Addr string
Protocol string
Transport string
Client *Client
}

225
gost/socks.go Normal file
View File

@ -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
}

View File

@ -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
}

23
gost/tls.go Normal file
View File

@ -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
}

View File

@ -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
}