diff --git a/README.md b/README.md index a01b653..b307237 100644 --- a/README.md +++ b/README.md @@ -16,6 +16,7 @@ gost - GO Simple Tunnel * 支持端口转发 (>=2.1) * 支持HTTP2.0 (>=2.2) * 实验性支持QUIC (>=2.3) +* KCP (>=2.3) 二进制文件下载:https://github.com/ginuerzh/gost/releases @@ -34,7 +35,7 @@ Google讨论组: https://groups.google.com/d/forum/go-gost ``` scheme分为两部分: protocol+transport -protocol: 代理协议类型(http, socks5, shadowsocks), transport: 数据传输方式(ws, wss, tls, http2, quic), 二者可以任意组合,或单独使用: +protocol: 代理协议类型(http, socks5, shadowsocks), transport: 数据传输方式(ws, wss, tls, http2, quic, kcp), 二者可以任意组合,或单独使用: > http - 作为HTTP代理: http://:8080 @@ -52,6 +53,8 @@ protocol: 代理协议类型(http, socks5, shadowsocks), transport: 数据传输 > quic - 作为QUIC代理,quic://:6121 +> kcp - 作为KCP代理,kcp://:8388 + #### 端口转发 适用于-L参数 @@ -169,6 +172,22 @@ chrome --enable-quic --proxy-server=quic://server_ip:6121 **注:** 由于Chrome自身的限制,目前只能通过QUIC访问HTTP网站,无法访问HTTPS网站。 +#### KCP +gost对KCP的支持是基于[kcp-go](https://github.com/xtaci/kcp-go)和[kcptun](https://github.com/xtaci/kcptun)库。 + +服务端: +```bash +gost -L=kcp://:8388 +``` + +客户端: +```bash +gost -L=:8080 -F=kcp://server_ip:8388 +``` + +**注:** 客户端若要开启KCP转发,当且仅当代理链不为空且首个代理节点(第一个-F参数)为kcp类型。 +当KCP转发开启,代理链中的其他代理节点将被忽略。 + 加密机制 ------ #### HTTP diff --git a/chain.go b/chain.go index 78763d1..a323642 100644 --- a/chain.go +++ b/chain.go @@ -13,6 +13,7 @@ import ( "net/http/httputil" "net/url" "strings" + "sync" ) // Proxy chain holds a list of proxy nodes @@ -22,6 +23,10 @@ type ProxyChain struct { http2NodeIndex int http2Enabled bool http2Client *http.Client + kcpEnabled bool + kcpConfig *KCPConfig + kcpSession *KCPSession + kcpMutex sync.Mutex } func NewProxyChain(nodes ...ProxyNode) *ProxyChain { @@ -61,11 +66,12 @@ func (c *ProxyChain) SetNode(index int, node ProxyNode) { } } -// TryEnableHttp2 initialize HTTP2 if available. +// Init initialize the proxy chain. +// KCP will be enabled if the first proxy node is KCP proxy (transport == kcp), the remaining nodes are ignored. // HTTP2 will be enabled when at least one HTTP2 proxy node (scheme == http2) is present. // -// NOTE: Should be called immediately when proxy nodes are ready, HTTP2 will not be enabled if this function not be called. -func (c *ProxyChain) TryEnableHttp2() { +// NOTE: Should be called immediately when proxy nodes are ready. +func (c *ProxyChain) Init() { length := len(c.nodes) if length == 0 { return @@ -73,6 +79,17 @@ func (c *ProxyChain) TryEnableHttp2() { c.lastNode = &c.nodes[length-1] + if c.nodes[0].Transport == "kcp" { + glog.V(LINFO).Infoln("KCP is enabled") + c.kcpEnabled = true + config, err := ParseKCPConfig(c.nodes[0].Get("c")) + if err != nil { + glog.V(LWARNING).Infoln("[kcp]", err) + } + c.kcpConfig = config + return + } + // HTTP2 restrict: HTTP2 will be enabled when at least one HTTP2 proxy node is present. for i, node := range c.nodes { if node.Transport == "http2" { @@ -88,6 +105,10 @@ func (c *ProxyChain) TryEnableHttp2() { } } +func (c *ProxyChain) KCPEnabled() bool { + return c.kcpEnabled +} + func (c *ProxyChain) Http2Enabled() bool { return c.http2Enabled } @@ -130,6 +151,19 @@ func (c *ProxyChain) GetConn() (net.Conn, error) { return nil, ErrEmptyChain } + if c.KCPEnabled() { + kcpConn, err := c.getKCPConn() + if err != nil { + return nil, err + } + pc := NewProxyConn(kcpConn, c.nodes[0]) + if err := pc.Handshake(); err != nil { + pc.Close() + return nil, err + } + return pc, nil + } + if c.Http2Enabled() { nodes = nodes[c.http2NodeIndex+1:] if len(nodes) == 0 { @@ -162,6 +196,23 @@ func (c *ProxyChain) dialWithNodes(withHttp2 bool, addr string, nodes ...ProxyNo return net.DialTimeout("tcp", addr, DialTimeout) } + if c.KCPEnabled() { + kcpConn, err := c.getKCPConn() + if err != nil { + return nil, err + } + pc := NewProxyConn(kcpConn, nodes[0]) + if err := pc.Handshake(); err != nil { + pc.Close() + return nil, err + } + if err := pc.Connect(addr); err != nil { + pc.Close() + return nil, err + } + return pc, nil + } + if withHttp2 && c.Http2Enabled() { nodes = nodes[c.http2NodeIndex+1:] if len(nodes) == 0 { @@ -219,6 +270,28 @@ func (c *ProxyChain) travelNodes(withHttp2 bool, nodes ...ProxyNode) (conn *Prox return } +func (c *ProxyChain) initKCPSession() (err error) { + c.kcpMutex.Lock() + defer c.kcpMutex.Unlock() + + if c.kcpSession == nil || c.kcpSession.IsClosed() { + glog.V(LINFO).Infoln("[kcp] new kcp session") + c.kcpSession, err = DialKCP(c.nodes[0].Addr, c.kcpConfig) + } + return +} + +func (c *ProxyChain) getKCPConn() (conn net.Conn, err error) { + if !c.KCPEnabled() { + return nil, errors.New("KCP is not enabled") + } + + if err = c.initKCPSession(); err != nil { + return nil, err + } + return c.kcpSession.GetConn() +} + // Initialize an HTTP2 transport if HTTP2 is enabled. func (c *ProxyChain) getHttp2Conn(header http.Header) (net.Conn, error) { if !c.Http2Enabled() { diff --git a/cmd/gost/main.go b/cmd/gost/main.go index 12ccbe9..ae15b9e 100644 --- a/cmd/gost/main.go +++ b/cmd/gost/main.go @@ -43,8 +43,7 @@ func main() { if err := chain.AddProxyNodeString(chainNodes...); err != nil { glog.Fatal(err) } - // enable HTTP2 - chain.TryEnableHttp2() + chain.Init() var wg sync.WaitGroup for _, ns := range serverNodes { diff --git a/cmd/tools/gost_client.go b/cmd/tools/gost_client.go deleted file mode 100644 index 64030b3..0000000 --- a/cmd/tools/gost_client.go +++ /dev/null @@ -1,80 +0,0 @@ -package main - -import ( - "bufio" - "flag" - "fmt" - "github.com/ginuerzh/gost" - "github.com/golang/glog" - "golang.org/x/net/http2" - "log" - "net/http" - "net/http/httputil" - "net/url" -) - -var ( - proxyNodes stringlist - urls []string -) - -func init() { - flag.Var(&proxyNodes, "F", "forward address, can make a forward chain") - flag.Parse() - if flag.NArg() == 0 { - log.Fatal("please specific at least one request URL") - } - urls = flag.Args() - if glog.V(5) { - http2.VerboseLogs = true - } -} - -type stringlist []string - -func (list *stringlist) String() string { - return fmt.Sprintf("%s", *list) -} -func (list *stringlist) Set(value string) error { - *list = append(*list, value) - return nil -} - -func main() { - chain := gost.NewProxyChain() - if err := chain.AddProxyNodeString(proxyNodes...); err != nil { - log.Fatal(err) - } - chain.TryEnableHttp2() - - for _, u := range urls { - url, err := url.Parse(u) - if err != nil { - log.Println("Invalid url:", u) - continue - } - - log.Println("GET", u) - conn, err := chain.Dial(url.Host) - if err != nil { - log.Fatal(err) - } - req, err := http.NewRequest("GET", u, 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() - - header, _ := httputil.DumpResponse(resp, false) - log.Println(string(header)) - } - -} diff --git a/cmd/tools/gost_server.go b/cmd/tools/gost_server.go deleted file mode 100644 index a830c3b..0000000 --- a/cmd/tools/gost_server.go +++ /dev/null @@ -1,53 +0,0 @@ -package main - -import ( - "crypto/tls" - "flag" - "fmt" - "github.com/ginuerzh/gost" - "log" - "sync" -) - -var ( - proxyNodes stringlist -) - -func init() { - flag.Var(&proxyNodes, "L", "proxy server node") - flag.Parse() -} - -type stringlist []string - -func (list *stringlist) String() string { - return fmt.Sprintf("%s", *list) -} -func (list *stringlist) Set(value string) error { - *list = append(*list, value) - return nil -} - -func main() { - chain := gost.NewProxyChain() - var wg sync.WaitGroup - for _, ns := range proxyNodes { - serverNode, err := gost.ParseProxyNode(ns) - if err != nil { - log.Println(err) - continue - } - wg.Add(1) - go func(node gost.ProxyNode) { - defer wg.Done() - cert, err := gost.LoadCertificate(node.Get("cert"), node.Get("key")) - if err != nil { - log.Println(err) - return - } - server := gost.NewProxyServer(node, chain, &tls.Config{Certificates: []tls.Certificate{cert}}) - log.Fatal(server.Serve()) - }(serverNode) - } - wg.Wait() -} diff --git a/conn.go b/conn.go index 053c672..f31e7ae 100644 --- a/conn.go +++ b/conn.go @@ -82,6 +82,8 @@ func (c *ProxyConn) handshake() error { c.conn = tls.Client(c.conn, cfg) case "h2": // same as http2, but just set a flag for later using. tlsUsed = true + case "kcp": // kcp connection + tlsUsed = true default: } diff --git a/kcp.go b/kcp.go new file mode 100644 index 0000000..10752c3 --- /dev/null +++ b/kcp.go @@ -0,0 +1,369 @@ +// KCP feature is based on https://github.com/xtaci/kcptun + +package gost + +import ( + "crypto/sha1" + "encoding/json" + "github.com/golang/glog" + "github.com/klauspost/compress/snappy" + "golang.org/x/crypto/pbkdf2" + "gopkg.in/xtaci/kcp-go.v2" + "gopkg.in/xtaci/smux.v1" + "net" + "os" + "time" +) + +const ( + DefaultKCPConfigFile = "kcp.json" +) + +var ( + SALT = "kcp-go" +) + +type KCPConfig struct { + Key string `json:"key"` + Crypt string `json:"crypt"` + Mode string `json:"mode"` + MTU int `json:"mtu"` + SndWnd int `json:"sndwnd"` + RcvWnd int `json:"rcvwnd"` + DataShard int `json:"datashard"` + ParityShard int `json:"parityshard"` + DSCP int `json:"dscp"` + NoComp bool `json:"nocomp"` + AckNodelay bool `json:"acknodelay"` + NoDelay int `json:"nodelay"` + Interval int `json:"interval"` + Resend int `json:"resend"` + NoCongestion int `json:"nc"` + SockBuf int `json:"sockbuf"` + KeepAlive int `json:"keepalive"` +} + +func ParseKCPConfig(configFile string) (*KCPConfig, error) { + if configFile == "" { + configFile = DefaultKCPConfigFile + } + file, err := os.Open(configFile) + if err != nil { + return nil, err + } + defer file.Close() + + config := &KCPConfig{} + if err = json.NewDecoder(file).Decode(config); err != nil { + return nil, err + } + return config, nil +} + +func (c *KCPConfig) Init() { + switch c.Mode { + case "normal": + c.NoDelay, c.Interval, c.Resend, c.NoCongestion = 0, 30, 2, 1 + case "fast2": + c.NoDelay, c.Interval, c.Resend, c.NoCongestion = 1, 20, 2, 1 + case "fast3": + c.NoDelay, c.Interval, c.Resend, c.NoCongestion = 1, 10, 2, 1 + case "fast": + fallthrough + default: + c.NoDelay, c.Interval, c.Resend, c.NoCongestion = 0, 20, 2, 1 + } +} + +var ( + DefaultKCPConfig = &KCPConfig{ + Key: "it's a secrect", + Crypt: "aes", + Mode: "fast", + MTU: 1350, + SndWnd: 1024, + RcvWnd: 1024, + DataShard: 10, + ParityShard: 3, + DSCP: 0, + NoComp: false, + AckNodelay: false, + NoDelay: 0, + Interval: 40, + Resend: 0, + NoCongestion: 0, + SockBuf: 4194304, + KeepAlive: 10, + } +) + +type KCPServer struct { + Base *ProxyServer + Config *KCPConfig +} + +func NewKCPServer(base *ProxyServer, config *KCPConfig) *KCPServer { + return &KCPServer{Base: base, Config: config} +} + +func (s *KCPServer) ListenAndServe() (err error) { + if s.Config == nil { + s.Config = DefaultKCPConfig + } + s.Config.Init() + + ln, err := kcp.ListenWithOptions(s.Base.Node.Addr, + blockCrypt(s.Config.Key, s.Config.Crypt, SALT), s.Config.DataShard, s.Config.ParityShard) + if err != nil { + return err + } + if err = ln.SetDSCP(s.Config.DSCP); err != nil { + glog.V(LWARNING).Infoln("[kcp]", err) + } + if err = ln.SetReadBuffer(s.Config.SockBuf); err != nil { + glog.V(LWARNING).Infoln("[kcp]", err) + } + if err = ln.SetWriteBuffer(s.Config.SockBuf); err != nil { + glog.V(LWARNING).Infoln("[kcp]", err) + } + + for { + conn, err := ln.AcceptKCP() + if err != nil { + glog.V(LWARNING).Infoln(err) + continue + } + + conn.SetStreamMode(true) + conn.SetNoDelay(s.Config.NoDelay, s.Config.Interval, s.Config.Resend, s.Config.NoCongestion) + conn.SetMtu(s.Config.MTU) + conn.SetWindowSize(s.Config.SndWnd, s.Config.RcvWnd) + conn.SetACKNoDelay(s.Config.AckNodelay) + conn.SetKeepAlive(s.Config.KeepAlive) + + go s.handleMux(conn) + } +} + +func (s *KCPServer) handleMux(conn net.Conn) { + smuxConfig := smux.DefaultConfig() + smuxConfig.MaxReceiveBuffer = s.Config.SockBuf + + glog.V(LINFO).Infof("[kcp] %s - %s", conn.RemoteAddr(), s.Base.Node.Addr) + + if !s.Config.NoComp { + conn = newCompStreamConn(conn) + } + + mux, err := smux.Server(conn, smuxConfig) + if err != nil { + glog.V(LWARNING).Infoln("[kcp]", err) + return + } + defer mux.Close() + + glog.V(LINFO).Infof("[kcp] %s <-> %s", conn.RemoteAddr(), s.Base.Node.Addr) + defer glog.V(LINFO).Infof("[kcp] %s >-< %s", conn.RemoteAddr(), s.Base.Node.Addr) + + for { + stream, err := mux.AcceptStream() + if err != nil { + glog.V(LWARNING).Infoln("[kcp]", err) + return + } + go s.Base.handleConn(NewKCPConn(conn, stream)) + } +} + +func blockCrypt(key, crypt, salt string) (block kcp.BlockCrypt) { + pass := pbkdf2.Key([]byte(key), []byte(salt), 4096, 32, sha1.New) + + switch crypt { + case "tea": + block, _ = kcp.NewTEABlockCrypt(pass[:16]) + case "xor": + block, _ = kcp.NewSimpleXORBlockCrypt(pass) + case "none": + block, _ = kcp.NewNoneBlockCrypt(pass) + case "aes-128": + block, _ = kcp.NewAESBlockCrypt(pass[:16]) + case "aes-192": + block, _ = kcp.NewAESBlockCrypt(pass[:24]) + case "blowfish": + block, _ = kcp.NewBlowfishBlockCrypt(pass) + case "twofish": + block, _ = kcp.NewTwofishBlockCrypt(pass) + case "cast5": + block, _ = kcp.NewCast5BlockCrypt(pass[:16]) + case "3des": + block, _ = kcp.NewTripleDESBlockCrypt(pass[:24]) + case "xtea": + block, _ = kcp.NewXTEABlockCrypt(pass[:16]) + case "salsa20": + block, _ = kcp.NewSalsa20BlockCrypt(pass) + case "aes": + fallthrough + default: // aes + block, _ = kcp.NewAESBlockCrypt(pass) + } + return +} + +type KCPSession struct { + conn net.Conn + session *smux.Session +} + +func DialKCP(addr string, config *KCPConfig) (*KCPSession, error) { + if config == nil { + config = DefaultKCPConfig + } + config.Init() + + kcpconn, err := kcp.DialWithOptions(addr, + blockCrypt(config.Key, config.Crypt, SALT), config.DataShard, config.ParityShard) + if err != nil { + return nil, err + } + + kcpconn.SetStreamMode(true) + kcpconn.SetNoDelay(config.NoDelay, config.Interval, config.Resend, config.NoCongestion) + kcpconn.SetWindowSize(config.SndWnd, config.RcvWnd) + kcpconn.SetMtu(config.MTU) + kcpconn.SetACKNoDelay(config.AckNodelay) + kcpconn.SetKeepAlive(config.KeepAlive) + + if err := kcpconn.SetDSCP(config.DSCP); err != nil { + glog.V(LWARNING).Infoln("[kcp]", err) + } + if err := kcpconn.SetReadBuffer(config.SockBuf); err != nil { + glog.V(LWARNING).Infoln("[kcp]", err) + } + if err := kcpconn.SetWriteBuffer(config.SockBuf); err != nil { + glog.V(LWARNING).Infoln("[kcp]", err) + } + + // stream multiplex + smuxConfig := smux.DefaultConfig() + smuxConfig.MaxReceiveBuffer = config.SockBuf + var conn net.Conn = kcpconn + if !config.NoComp { + conn = newCompStreamConn(kcpconn) + } + session, err := smux.Client(conn, smuxConfig) + if err != nil { + conn.Close() + return nil, err + } + return &KCPSession{conn: conn, session: session}, nil +} + +func (session *KCPSession) GetConn() (*KCPConn, error) { + stream, err := session.session.OpenStream() + if err != nil { + session.Close() + return nil, err + } + return NewKCPConn(session.conn, stream), nil +} + +func (session *KCPSession) Close() error { + return session.session.Close() +} + +func (session *KCPSession) IsClosed() bool { + return session.session.IsClosed() +} + +func (session *KCPSession) NumStreams() int { + return session.session.NumStreams() +} + +type KCPConn struct { + conn net.Conn + stream *smux.Stream +} + +func NewKCPConn(conn net.Conn, stream *smux.Stream) *KCPConn { + return &KCPConn{conn: conn, stream: stream} +} + +func (c *KCPConn) Read(b []byte) (n int, err error) { + return c.stream.Read(b) +} + +func (c *KCPConn) Write(b []byte) (n int, err error) { + return c.stream.Write(b) +} + +func (c *KCPConn) Close() error { + return c.stream.Close() +} + +func (c *KCPConn) LocalAddr() net.Addr { + return c.conn.LocalAddr() +} + +func (c *KCPConn) RemoteAddr() net.Addr { + return c.conn.RemoteAddr() +} + +func (c *KCPConn) SetDeadline(t time.Time) error { + return c.conn.SetDeadline(t) +} + +func (c *KCPConn) SetReadDeadline(t time.Time) error { + return c.conn.SetReadDeadline(t) +} + +func (c *KCPConn) SetWriteDeadline(t time.Time) error { + return c.conn.SetWriteDeadline(t) +} + +type compStreamConn struct { + conn net.Conn + w *snappy.Writer + r *snappy.Reader +} + +func newCompStreamConn(conn net.Conn) *compStreamConn { + c := new(compStreamConn) + c.conn = conn + c.w = snappy.NewBufferedWriter(conn) + c.r = snappy.NewReader(conn) + return c +} + +func (c *compStreamConn) Read(b []byte) (n int, err error) { + return c.r.Read(b) +} + +func (c *compStreamConn) Write(b []byte) (n int, err error) { + n, err = c.w.Write(b) + err = c.w.Flush() + return n, err +} + +func (c *compStreamConn) Close() error { + return c.conn.Close() +} + +func (c *compStreamConn) LocalAddr() net.Addr { + return c.conn.LocalAddr() +} + +func (c *compStreamConn) RemoteAddr() net.Addr { + return c.conn.RemoteAddr() +} + +func (c *compStreamConn) SetDeadline(t time.Time) error { + return c.conn.SetDeadline(t) +} + +func (c *compStreamConn) SetReadDeadline(t time.Time) error { + return c.conn.SetReadDeadline(t) +} + +func (c *compStreamConn) SetWriteDeadline(t time.Time) error { + return c.conn.SetWriteDeadline(t) +} diff --git a/node.go b/node.go index 8a4f043..5c730ae 100644 --- a/node.go +++ b/node.go @@ -57,7 +57,7 @@ func ParseProxyNode(s string) (node ProxyNode, err error) { } switch node.Transport { - case "ws", "wss", "tls", "http2", "ssu", "quic": + case "ws", "wss", "tls", "http2", "ssu", "quic", "kcp": case "https": node.Protocol = "http" node.Transport = "tls" diff --git a/server.go b/server.go index 8ca5597..20da5d9 100644 --- a/server.go +++ b/server.go @@ -84,6 +84,12 @@ func (s *ProxyServer) Serve() error { return NewShadowUdpServer(s).ListenAndServe() case "quic": return NewQuicServer(s).ListenAndServeTLS(s.TLSConfig) + case "kcp": + config, err := ParseKCPConfig(s.Node.Get("c")) + if err != nil { + glog.V(LWARNING).Infoln("[kcp]", err) + } + return NewKCPServer(s, config).ListenAndServe() default: ln, err = net.Listen("tcp", node.Addr) } @@ -135,7 +141,6 @@ func (s *ProxyServer) handleConn(conn net.Conn) { return } - glog.V(LINFO).Infof("%s - %s", conn.RemoteAddr(), s.Node.Addr) // http or socks5 b := make([]byte, MediumBufferSize) diff --git a/ss.go b/ss.go index 48fa6e4..12bc167 100644 --- a/ss.go +++ b/ss.go @@ -101,6 +101,7 @@ func (s *ShadowUdpServer) ListenAndServe() error { } } +// TODO: shadowsocks udp relay handler func (s *ShadowUdpServer) HandleConn(conn *net.UDPConn, addr *net.UDPAddr, data []byte) { }