diff --git a/.travis.yml b/.travis.yml index 8fdbf99d7a3ca1d7cb90c7b2c68174404031040d..b5b6a21b67a875601f9d029c8f308c9e2dc88fa9 100644 --- a/.travis.yml +++ b/.travis.yml @@ -3,7 +3,7 @@ language: go go: - 1.4.2 - - 1.5.1 + - 1.5.3 install: - make diff --git a/Dockerfile b/Dockerfile index bbfc6e13c26c5115f37eb319f51b8c48a7a96108..5480f421fe695aba6a59e0232c8ac164bb7e473a 100644 --- a/Dockerfile +++ b/Dockerfile @@ -2,7 +2,7 @@ FROM golang:1.5 MAINTAINER fatedier -RUN echo "[common]\nbind_addr = 0.0.0.0\nbind_port = 7000\n[wiki]\npasswd = 123\nbind_addr = 0.0.0.0\nlisten_port = 80" > /usr/share/frps.ini +RUN echo "[common]\nbind_addr = 0.0.0.0\nbind_port = 7000\n[test]\npasswd = 123\nbind_addr = 0.0.0.0\nlisten_port = 80" > /usr/share/frps.ini ADD ./ /usr/share/frp/ @@ -11,4 +11,4 @@ RUN cd /usr/share/frp && make EXPOSE 80 EXPOSE 7000 -CMD ["/usr/share/frp/bin/frps -c /usr/share/frps.ini"] +CMD ["/usr/share/frp/bin/frps", "-c", "/usr/share/frps.ini"] diff --git a/README.md b/README.md index 26d83a8f650ad62bfc43456b30f80f022b807a19..2ed7ffc7c90dadfae27e7a7deeba152b34dc896b 100644 --- a/README.md +++ b/README.md @@ -2,13 +2,17 @@ [![Build Status](https://travis-ci.org/fatedier/frp.svg)](https://travis-ci.org/fatedier/frp) +[README](README.md) | [中文文档](README_zh.md) + ## What is frp? -frp is a fast reverse proxy which can help you expose a local server behind a NAT or firewall to the internet. +frp is a fast reverse proxy to help you expose a local server behind a NAT or firewall to the internet. ## Status -frp is under development and you can try it with available version 0.2.0. +frp is under development and you can try it with latest release version.Master branch for releasing stable version when dev branch for developing. + +**We may change any protocol and can't promise backward compatible before version 1.x.** ## Quick Start @@ -28,6 +32,11 @@ Read the [QuickStart](doc/quick_start_en.md) | [使用文档](doc/quick_start_zh Interested in getting involved? We would love to help you! -For simple bug fixes, just submit a PR with the fix and we can discuss the fix directly in the PR. If the fix is more complex, start with an issue. +* Take a look at our [issues list](https://github.com/fatedier/frp/issues) and consider submitting a patch +* If you have some wanderful ideas, send email to fatedier@gmail.com. + +## Contributors -If you have some wanderful ideas, send email to fatedier@gmail.com. +* [fatedier](https://github.com/fatedier) +* [Hurricanezwf](https://github.com/Hurricanezwf) +* [vashstorm](https://github.com/vashstorm) diff --git a/README_zh.md b/README_zh.md new file mode 100644 index 0000000000000000000000000000000000000000..4b272577a7e0063f682c44c45c73e52a00e3a85f --- /dev/null +++ b/README_zh.md @@ -0,0 +1,40 @@ +# frp + +[![Build Status](https://travis-ci.org/fatedier/frp.svg)](https://travis-ci.org/fatedier/frp) + +[README](README.md) | [中文文档](README_zh.md) + +>frp 是一个高性能的反向代理应用,可以帮助你轻松的进行内网穿透,对外网提供服务。 + +## 开发状态 + +frp 目前正在前期开发阶段,master分支用于发布稳定版本,dev分支用于开发,您可以尝试下载最新的 release 版本进行测试。 + +**在 1.x 版本以前,交互协议都可能会被改变,不能保证向后兼容。** + +## 快速开始 + +[QuickStart](doc/quick_start_en.md) | [使用文档](doc/quick_start_zh.md) + +## 架构 + +![architecture](doc/pic/architecture.png) + +## frp 的作用? + +* 利用处于内网或防火墙后的机器,对外网环境提供http服务。(针对http的优化正在开发中) +* 利用处于内网或防火墙后的机器,对外网环境提供tcp服务。 +* 可查看通过代理的所有http请求和响应信息。(待开发) + +## 贡献代码 + +如果您对这个项目感兴趣,并且想要参与其中,我们非常欢迎! + +* 如果您需要提交问题,可以通过 [issues](https://github.com/fatedier/frp/issues) 来完成。 +* 如果您有新的功能需求,可以反馈至 fatedier@gmail.com 共同讨论。 + +## 贡献者 + +* [fatedier](https://github.com/fatedier) +* [Hurricanezwf](https://github.com/Hurricanezwf) +* [vashstorm](https://github.com/vashstorm) diff --git a/conf/frpc.ini b/conf/frpc.ini index 09dfeb752927b1c0cd05caab303b62a263844a02..2f48a80ccd34099f3664c59bfde2116ab296e486 100644 --- a/conf/frpc.ini +++ b/conf/frpc.ini @@ -6,9 +6,12 @@ server_port = 7000 log_file = console # debug, info, warn, error log_level = debug +# for authentication +auth_token = 123 # test1 is the proxy name same as server's configuration [test1] -passwd = 123 local_ip = 127.0.0.1 local_port = 22 +# true or false, if true, messages between frps and frpc will be encrypted, default is false +use_encryption = true diff --git a/conf/frps.ini b/conf/frps.ini index 0358a7ad81ffab1e75a98ea0db555df65838f21b..c410410d07edf496f87da1531556d8f592955fdb 100644 --- a/conf/frps.ini +++ b/conf/frps.ini @@ -7,8 +7,8 @@ log_file = console # debug, info, warn, error log_level = debug -# test1 is the proxy name, client will use this name and passwd to connect to server +# test1 is the proxy name, client will use this name and auth_token to connect to server [test1] -passwd = 123 +auth_token = 123 bind_addr = 0.0.0.0 listen_port = 6000 diff --git a/doc/quick_start_en.md b/doc/quick_start_en.md index 46fa3c22f6b9169a91a01e5d9a8a0c55f604712a..6b2a00c84386f0e4ad67c60daa41bdacfa862904 100644 --- a/doc/quick_start_en.md +++ b/doc/quick_start_en.md @@ -44,7 +44,7 @@ log_level = info # test is the custom name of proxy and there can be many proxies with unique name in one configure file [test] -passwd = 123 +auth_token = 123 bind_addr = 0.0.0.0 # finally we connect to server A by this port listen_port = 6000 @@ -59,10 +59,13 @@ server_addr = x.x.x.x server_port = 7000 log_file = ./frpc.log log_level = info +# for authentication +auth_token = 123 # test is proxy name same with configure in frps.ini [test] -passwd = 123 # local port which need to be transferred local_port = 22 +# if use_encryption equals true, messages between frpc and frps will be encrypted, default is false +use_encryption = true ``` diff --git a/doc/quick_start_zh.md b/doc/quick_start_zh.md index 61fb3bdae9ce5e46993d39a38f56ddf77904e1dc..f48b55d7fdfc1c1eae0f8e7e063e91d14b3e3327 100644 --- a/doc/quick_start_zh.md +++ b/doc/quick_start_zh.md @@ -42,7 +42,7 @@ log_level = info # test 为代理的自定义名称,可以有多个,不能重复,和frpc中名称对应 [test] -passwd = 123 +auth_token = 123 bind_addr = 0.0.0.0 # 最后将通过此端口访问后端服务 listen_port = 6000 @@ -57,10 +57,13 @@ server_addr = x.x.x.x server_port = 7000 log_file = ./frpc.log log_level = info +# 用于身份验证 +auth_token = 123 # test需要和 frps.ini 中配置一致 [test] -passwd = 123 # 需要转发的本地端口 local_port = 22 +# 启用加密,frpc与frps之间通信加密,默认为 false +use_encryption = true ``` diff --git a/src/frp/cmd/frpc/control.go b/src/frp/cmd/frpc/control.go index 137be068e44da1c18e8593725ddf1baa97d72369..06a4fdaeb937d5f2077527d7a6388d882a676a41 100644 --- a/src/frp/cmd/frpc/control.go +++ b/src/frp/cmd/frpc/control.go @@ -26,66 +26,101 @@ import ( "frp/models/msg" "frp/utils/conn" "frp/utils/log" + "frp/utils/pcrypto" ) -var connection *conn.Conn = nil -var heartBeatTimer *time.Timer = nil - func ControlProcess(cli *client.ProxyClient, wait *sync.WaitGroup) { defer wait.Done() + msgSendChan := make(chan interface{}, 1024) + c, err := loginToServer(cli) if err != nil { log.Error("ProxyName [%s], connect to server failed!", cli.Name) return } - connection = c - defer connection.Close() + defer c.Close() + + go heartbeatSender(c, msgSendChan) + + go msgSender(cli, c, msgSendChan) + msgReader(cli, c, msgSendChan) + + close(msgSendChan) +} + +// loop for reading messages from frpc after control connection is established +func msgReader(cli *client.ProxyClient, c *conn.Conn, msgSendChan chan interface{}) error { + // for heartbeat + var heartbeatTimeout bool = false + timer := time.AfterFunc(time.Duration(client.HeartBeatTimeout)*time.Second, func() { + heartbeatTimeout = true + c.Close() + log.Error("ProxyName [%s], heartbeatRes from frps timeout", cli.Name) + }) + defer timer.Stop() for { - // ignore response content now - content, err := connection.ReadLine() - if err == io.EOF || nil == connection || connection.IsClosed() { - log.Debug("ProxyName [%s], server close this control conn", cli.Name) - var sleepTime time.Duration = 1 + buf, err := c.ReadLine() + if err == io.EOF || c == nil || c.IsClosed() { + c.Close() + log.Warn("ProxyName [%s], frps close this control conn!", cli.Name) + var delayTime time.Duration = 1 - // loop until connect to server + // loop until reconnect to frps for { - log.Debug("ProxyName [%s], try to reconnect to server[%s:%d]...", cli.Name, client.ServerAddr, client.ServerPort) - tmpConn, err := loginToServer(cli) + log.Info("ProxyName [%s], try to reconnect to frps [%s:%d]...", cli.Name, client.ServerAddr, client.ServerPort) + c, err = loginToServer(cli) if err == nil { - connection.Close() - connection = tmpConn + go heartbeatSender(c, msgSendChan) break } - if sleepTime < 60 { - sleepTime = sleepTime * 2 + if delayTime < 60 { + delayTime = delayTime * 2 } - time.Sleep(sleepTime * time.Second) + time.Sleep(delayTime * time.Second) } - continue } else if err != nil { - log.Warn("ProxyName [%s], read from server error, %v", cli.Name, err) + log.Warn("ProxyName [%s], read from frps error: %v", cli.Name, err) continue } - clientCtlRes := &msg.ClientCtlRes{} - if err := json.Unmarshal([]byte(content), clientCtlRes); err != nil { - log.Warn("Parse err: %v : %s", err, content) + ctlRes := &msg.ControlRes{} + if err := json.Unmarshal([]byte(buf), &ctlRes); err != nil { + log.Warn("ProxyName [%s], parse msg from frps error: %v : %s", cli.Name, err, buf) continue } - if consts.SCHeartBeatRes == clientCtlRes.GeneralRes.Code { - if heartBeatTimer != nil { - log.Debug("Client rcv heartbeat response") - heartBeatTimer.Reset(time.Duration(client.HeartBeatTimeout) * time.Second) - } else { - log.Error("heartBeatTimer is nil") - } - continue + + switch ctlRes.Type { + case consts.HeartbeatRes: + log.Debug("ProxyName [%s], receive heartbeat response", cli.Name) + timer.Reset(time.Duration(client.HeartBeatTimeout) * time.Second) + case consts.NoticeUserConn: + log.Debug("ProxyName [%s], new user connection", cli.Name) + cli.StartTunnel(client.ServerAddr, client.ServerPort) + default: + log.Warn("ProxyName [%s}, unsupport msgType [%d]", cli.Name, ctlRes.Type) + } + } + return nil +} + +// loop for sending messages from channel to frps +func msgSender(cli *client.ProxyClient, c *conn.Conn, msgSendChan chan interface{}) { + for { + msg, ok := <-msgSendChan + if !ok { + break } - cli.StartTunnel(client.ServerAddr, client.ServerPort) + buf, _ := json.Marshal(msg) + err := c.Write(string(buf) + "\n") + if err != nil { + log.Warn("ProxyName [%s], write to client error, proxy exit", cli.Name) + c.Close() + break + } } } @@ -96,10 +131,14 @@ func loginToServer(cli *client.ProxyClient) (c *conn.Conn, err error) { return } - req := &msg.ClientCtlReq{ - Type: consts.CtlConn, - ProxyName: cli.Name, - Passwd: cli.Passwd, + nowTime := time.Now().Unix() + authKey := pcrypto.GetAuthKey(cli.Name + cli.AuthToken + fmt.Sprintf("%d", nowTime)) + req := &msg.ControlReq{ + Type: consts.NewCtlConn, + ProxyName: cli.Name, + AuthKey: authKey, + UseEncryption: cli.UseEncryption, + Timestamp: nowTime, } buf, _ := json.Marshal(req) err = c.Write(string(buf) + "\n") @@ -115,53 +154,31 @@ func loginToServer(cli *client.ProxyClient) (c *conn.Conn, err error) { } log.Debug("ProxyName [%s], read [%s]", cli.Name, res) - clientCtlRes := &msg.ClientCtlRes{} - if err = json.Unmarshal([]byte(res), &clientCtlRes); err != nil { + ctlRes := &msg.ControlRes{} + if err = json.Unmarshal([]byte(res), &ctlRes); err != nil { log.Error("ProxyName [%s], format server response error, %v", cli.Name, err) return } - if clientCtlRes.Code != 0 { - log.Error("ProxyName [%s], start proxy error, %s", cli.Name, clientCtlRes.Msg) - return c, fmt.Errorf("%s", clientCtlRes.Msg) + if ctlRes.Code != 0 { + log.Error("ProxyName [%s], start proxy error, %s", cli.Name, ctlRes.Msg) + return c, fmt.Errorf("%s", ctlRes.Msg) } - go startHeartBeat(c) - log.Debug("ProxyName [%s], connect to server[%s:%d] success!", cli.Name, client.ServerAddr, client.ServerPort) - + log.Debug("ProxyName [%s], connect to server [%s:%d] success!", cli.Name, client.ServerAddr, client.ServerPort) return } -func startHeartBeat(c *conn.Conn) { - f := func() { - log.Error("HeartBeat timeout!") - if c != nil { - c.Close() - } - } - heartBeatTimer = time.AfterFunc(time.Duration(client.HeartBeatTimeout)*time.Second, f) - defer heartBeatTimer.Stop() - - clientCtlReq := &msg.ClientCtlReq{ - Type: consts.CSHeartBeatReq, - ProxyName: "", - Passwd: "", +func heartbeatSender(c *conn.Conn, msgSendChan chan interface{}) { + heartbeatReq := &msg.ControlReq{ + Type: consts.HeartbeatReq, } - request, err := json.Marshal(clientCtlReq) - if err != nil { - log.Warn("Serialize clientCtlReq err! Err: %v", err) - } - - log.Debug("Start to send heartbeat") + log.Info("Start to send heartbeat to frps") for { time.Sleep(time.Duration(client.HeartBeatInterval) * time.Second) if c != nil && !c.IsClosed() { log.Debug("Send heartbeat to server") - err = c.Write(string(request) + "\n") - if err != nil { - log.Error("Send hearbeat to server failed! Err:%v", err) - continue - } + msgSendChan <- heartbeatReq } else { break } diff --git a/src/frp/cmd/frps/control.go b/src/frp/cmd/frps/control.go index b60365b64444f6bb20cb20da1d4e8a1f39b57bde..67cd6428e80f53708b7a407e85f5e09cbc681375 100644 --- a/src/frp/cmd/frps/control.go +++ b/src/frp/cmd/frps/control.go @@ -25,6 +25,7 @@ import ( "frp/models/server" "frp/utils/conn" "frp/utils/log" + "frp/utils/pcrypto" ) func ProcessControlConn(l *conn.Listener) { @@ -33,87 +34,162 @@ func ProcessControlConn(l *conn.Listener) { if err != nil { return } - log.Debug("Get one new conn, %v", c.GetRemoteAddr()) + log.Debug("Get new connection, %v", c.GetRemoteAddr()) go controlWorker(c) } } // connection from every client and server func controlWorker(c *conn.Conn) { - // the first message is from client to server - // if error, close connection - res, err := c.ReadLine() + // if login message type is NewWorkConn, don't close this connection + var closeFlag bool = true + var s *server.ProxyServer + defer func() { + if closeFlag { + c.Close() + if s != nil { + s.Close() + } + } + }() + + // get login message + buf, err := c.ReadLine() if err != nil { log.Warn("Read error, %v", err) return } - log.Debug("get: %s", res) + log.Debug("Get msg from frpc: %s", buf) - clientCtlReq := &msg.ClientCtlReq{} - clientCtlRes := &msg.ClientCtlRes{} - if err := json.Unmarshal([]byte(res), &clientCtlReq); err != nil { - log.Warn("Parse err: %v : %s", err, res) + cliReq := &msg.ControlReq{} + if err := json.Unmarshal([]byte(buf), &cliReq); err != nil { + log.Warn("Parse msg from frpc error: %v : %s", err, buf) return } - // check - succ, info, needRes := checkProxy(clientCtlReq, c) - if !succ { - clientCtlRes.Code = 1 - clientCtlRes.Msg = info + // do login when type is NewCtlConn or NewWorkConn + ret, info := doLogin(cliReq, c) + s, ok := server.ProxyServers[cliReq.ProxyName] + if !ok { + log.Warn("ProxyName [%s] is not exist", cliReq.ProxyName) + return } - - if needRes { - defer c.Close() - - buf, _ := json.Marshal(clientCtlRes) - err = c.Write(string(buf) + "\n") + // if login type is NewWorkConn, nothing will be send to frpc + if cliReq.Type != consts.NewWorkConn { + cliRes := &msg.ControlRes{ + Type: consts.NewCtlConnRes, + Code: ret, + Msg: info, + } + byteBuf, _ := json.Marshal(cliRes) + err = c.Write(string(byteBuf) + "\n") if err != nil { - log.Warn("Write error, %v", err) + log.Warn("ProxyName [%s], write to client error, proxy exit", s.Name) time.Sleep(1 * time.Second) return } } else { - // work conn, just return + closeFlag = false return } - // other messages is from server to client - s, ok := server.ProxyServers[clientCtlReq.ProxyName] - if !ok { - log.Warn("ProxyName [%s] is not exist", clientCtlReq.ProxyName) - return - } + // create a channel for sending messages + msgSendChan := make(chan interface{}, 1024) + go msgSender(s, c, msgSendChan) + go noticeUserConn(s, msgSendChan) - // read control msg from client - go readControlMsgFromClient(s, c) + // loop for reading control messages from frpc and deal with different types + msgReader(s, c, msgSendChan) - serverCtlReq := &msg.ClientCtlReq{} - serverCtlReq.Type = consts.WorkConn + close(msgSendChan) + log.Info("ProxyName [%s], I'm dead!", s.Name) + return +} + +// when frps get one new user connection, send NoticeUserConn message to frpc and accept one new WorkConn later +func noticeUserConn(s *server.ProxyServer, msgSendChan chan interface{}) { for { closeFlag := s.WaitUserConn() if closeFlag { - log.Debug("ProxyName [%s], goroutine for dealing user conn is closed", s.Name) + log.Debug("ProxyName [%s], goroutine for noticing user conn is closed", s.Name) break } - buf, _ := json.Marshal(serverCtlReq) - err = c.Write(string(buf) + "\n") + notice := &msg.ControlRes{ + Type: consts.NoticeUserConn, + } + msgSendChan <- notice + log.Debug("ProxyName [%s], notice client to add work conn", s.Name) + } +} + +// loop for reading messages from frpc after control connection is established +func msgReader(s *server.ProxyServer, c *conn.Conn, msgSendChan chan interface{}) error { + // for heartbeat + var heartbeatTimeout bool = false + timer := time.AfterFunc(time.Duration(server.HeartBeatTimeout)*time.Second, func() { + heartbeatTimeout = true + s.Close() + c.Close() + log.Error("ProxyName [%s], client heartbeat timeout", s.Name) + }) + defer timer.Stop() + + for { + buf, err := c.ReadLine() if err != nil { - log.Warn("ProxyName [%s], write to client error, proxy exit", s.Name) - s.Close() - return + if err == io.EOF { + log.Warn("ProxyName [%s], client is dead!", s.Name) + return err + } else if c == nil || c.IsClosed() { + log.Warn("ProxyName [%s], client connection is closed", s.Name) + return err + } + log.Warn("ProxyName [%s], read error: %v", s.Name, err) + continue + } + + cliReq := &msg.ControlReq{} + if err := json.Unmarshal([]byte(buf), &cliReq); err != nil { + log.Warn("ProxyName [%s], parse msg from frpc error: %v : %s", s.Name, err, buf) + continue } - log.Debug("ProxyName [%s], write to client to add work conn success", s.Name) + switch cliReq.Type { + case consts.HeartbeatReq: + log.Debug("ProxyName [%s], get heartbeat", s.Name) + timer.Reset(time.Duration(server.HeartBeatTimeout) * time.Second) + heartbeatRes := msg.ControlRes{ + Type: consts.HeartbeatRes, + } + msgSendChan <- heartbeatRes + default: + log.Warn("ProxyName [%s}, unsupport msgType [%d]", s.Name, cliReq.Type) + } } + return nil +} - log.Info("ProxyName [%s], I'm dead!", s.Name) - return +// loop for sending messages from channel to frpc +func msgSender(s *server.ProxyServer, c *conn.Conn, msgSendChan chan interface{}) { + for { + msg, ok := <-msgSendChan + if !ok { + break + } + + buf, _ := json.Marshal(msg) + err := c.Write(string(buf) + "\n") + if err != nil { + log.Warn("ProxyName [%s], write to client error, proxy exit", s.Name) + s.Close() + break + } + } } -func checkProxy(req *msg.ClientCtlReq, c *conn.Conn) (succ bool, info string, needRes bool) { - succ = false - needRes = true +// if success, ret equals 0, otherwise greater than 0 +func doLogin(req *msg.ControlReq, c *conn.Conn) (ret int64, info string) { + ret = 1 // check if proxy name exist s, ok := server.ProxyServers[req.ProxyName] if !ok { @@ -122,97 +198,53 @@ func checkProxy(req *msg.ClientCtlReq, c *conn.Conn) (succ bool, info string, ne return } - // check password - if req.Passwd != s.Passwd { - info = fmt.Sprintf("ProxyName [%s], password is not correct", req.ProxyName) + // check authKey + nowTime := time.Now().Unix() + authKey := pcrypto.GetAuthKey(req.ProxyName + s.AuthToken + fmt.Sprintf("%d", req.Timestamp)) + // authKey avaiable in 15 minutes + if nowTime-req.Timestamp > 15*60 { + info = fmt.Sprintf("ProxyName [%s], authorization timeout", req.ProxyName) + log.Warn(info) + return + } else if req.AuthKey != authKey { + info = fmt.Sprintf("ProxyName [%s], authorization failed", req.ProxyName) log.Warn(info) return } // control conn - if req.Type == consts.CtlConn { - if s.Status != consts.Idle { + if req.Type == consts.NewCtlConn { + if s.Status == consts.Working { info = fmt.Sprintf("ProxyName [%s], already in use", req.ProxyName) log.Warn(info) return } - // start proxy and listen for user conn, no block + // set infomations from frpc + s.UseEncryption = req.UseEncryption + + // start proxy and listen for user connections, no block err := s.Start() if err != nil { - info = fmt.Sprintf("ProxyName [%s], start proxy error: %v", req.ProxyName, err.Error()) + info = fmt.Sprintf("ProxyName [%s], start proxy error: %v", req.ProxyName, err) log.Warn(info) return } - log.Info("ProxyName [%s], start proxy success", req.ProxyName) - } else if req.Type == consts.WorkConn { + } else if req.Type == consts.NewWorkConn { // work conn - needRes = false if s.Status != consts.Working { - log.Warn("ProxyName [%s], is not working when it gets one new work conn", req.ProxyName) + log.Warn("ProxyName [%s], is not working when it gets one new work connnection", req.ProxyName) return } - - s.GetNewCliConn(c) + // the connection will close after join over + s.RecvNewWorkConn(c) } else { - info = fmt.Sprintf("ProxyName [%s], type [%d] unsupport", req.ProxyName, req.Type) - log.Warn(info) + info = fmt.Sprintf("Unsupport login message type [%d]", req.Type) + log.Warn("Unsupport login message type [%d]", req.Type) return } - succ = true + ret = 0 return } - -func readControlMsgFromClient(s *server.ProxyServer, c *conn.Conn) { - isContinueRead := true - f := func() { - isContinueRead = false - s.Close() - log.Error("ProxyName [%s], client heartbeat timeout", s.Name) - } - timer := time.AfterFunc(time.Duration(server.HeartBeatTimeout)*time.Second, f) - defer timer.Stop() - - for isContinueRead { - content, err := c.ReadLine() - if err != nil { - if err == io.EOF { - log.Warn("ProxyName [%s], client is dead!", s.Name) - s.Close() - break - } else if nil == c || c.IsClosed() { - log.Warn("ProxyName [%s], client connection is closed", s.Name) - break - } - - log.Error("ProxyName [%s], read error: %v", s.Name, err) - continue - } - - clientCtlReq := &msg.ClientCtlReq{} - if err := json.Unmarshal([]byte(content), clientCtlReq); err != nil { - log.Warn("Parse err: %v : %s", err, content) - continue - } - if consts.CSHeartBeatReq == clientCtlReq.Type { - log.Debug("ProxyName [%s], get heartbeat", s.Name) - timer.Reset(time.Duration(server.HeartBeatTimeout) * time.Second) - - clientCtlRes := &msg.ClientCtlRes{} - clientCtlRes.GeneralRes.Code = consts.SCHeartBeatRes - response, err := json.Marshal(clientCtlRes) - if err != nil { - log.Warn("Serialize ClientCtlRes err! err: %v", err) - continue - } - - err = c.Write(string(response) + "\n") - if err != nil { - log.Error("Send heartbeat response to client failed! Err:%v", err) - continue - } - } - } -} diff --git a/src/frp/models/client/client.go b/src/frp/models/client/client.go index ce99f098c4cb83188567e5c6ae0c1f4a631758d7..b0b947b80dfe73d7319b4d6f799b0875621e94f6 100644 --- a/src/frp/models/client/client.go +++ b/src/frp/models/client/client.go @@ -16,18 +16,22 @@ package client import ( "encoding/json" + "fmt" + "time" "frp/models/consts" "frp/models/msg" "frp/utils/conn" "frp/utils/log" + "frp/utils/pcrypto" ) type ProxyClient struct { - Name string - Passwd string - LocalIp string - LocalPort int64 + Name string + AuthToken string + LocalIp string + LocalPort int64 + UseEncryption bool } func (p *ProxyClient) GetLocalConn() (c *conn.Conn, err error) { @@ -51,10 +55,13 @@ func (p *ProxyClient) GetRemoteConn(addr string, port int64) (c *conn.Conn, err return } - req := &msg.ClientCtlReq{ - Type: consts.WorkConn, + nowTime := time.Now().Unix() + authKey := pcrypto.GetAuthKey(p.Name + p.AuthToken + fmt.Sprintf("%d", nowTime)) + req := &msg.ControlReq{ + Type: consts.NewWorkConn, ProxyName: p.Name, - Passwd: p.Passwd, + AuthKey: authKey, + Timestamp: nowTime, } buf, _ := json.Marshal(req) @@ -79,8 +86,13 @@ func (p *ProxyClient) StartTunnel(serverAddr string, serverPort int64) (err erro } // l means local, r means remote - log.Debug("Join two conns, (l[%s] r[%s]) (l[%s] r[%s])", localConn.GetLocalAddr(), localConn.GetRemoteAddr(), + log.Debug("Join two connections, (l[%s] r[%s]) (l[%s] r[%s])", localConn.GetLocalAddr(), localConn.GetRemoteAddr(), remoteConn.GetLocalAddr(), remoteConn.GetRemoteAddr()) - go conn.Join(localConn, remoteConn) + if p.UseEncryption { + go conn.JoinMore(localConn, remoteConn, p.AuthToken) + } else { + go conn.Join(localConn, remoteConn) + } + return nil } diff --git a/src/frp/models/client/config.go b/src/frp/models/client/config.go index e41fec82ca72d5a5e437ec603ca1035fd202ef4d..3c09c7513181c7996b0f147f29c3125d43ee5de7 100644 --- a/src/frp/models/client/config.go +++ b/src/frp/models/client/config.go @@ -69,23 +69,32 @@ func LoadConf(confFile string) (err error) { LogLevel = tmpStr } + var authToken string + tmpStr, ok = conf.Get("common", "auth_token") + if ok { + authToken = tmpStr + } else { + return fmt.Errorf("auth_token not found") + } + // proxies for name, section := range conf { if name != "common" { proxyClient := &ProxyClient{} + // name proxyClient.Name = name - proxyClient.Passwd, ok = section["passwd"] - if !ok { - return fmt.Errorf("Parse ini file error: proxy [%s] no passwd found", proxyClient.Name) - } + // auth_token + proxyClient.AuthToken = authToken + // local_ip proxyClient.LocalIp, ok = section["local_ip"] if !ok { // use 127.0.0.1 as default proxyClient.LocalIp = "127.0.0.1" } + // local_port portStr, ok := section["local_port"] if ok { proxyClient.LocalPort, err = strconv.ParseInt(portStr, 10, 64) @@ -96,6 +105,13 @@ func LoadConf(confFile string) (err error) { return fmt.Errorf("Parse ini file error: proxy [%s] local_port not found", proxyClient.Name) } + // use_encryption + proxyClient.UseEncryption = false + useEncryptionStr, ok := section["use_encryption"] + if ok && useEncryptionStr == "true" { + proxyClient.UseEncryption = true + } + ProxyClients[proxyClient.Name] = proxyClient } } diff --git a/src/frp/models/consts/consts.go b/src/frp/models/consts/consts.go index 56191ff6bd710ae9af04fc934b5031cae37a26cc..7bb094436f40f32750e5b0559fe862e7de30f04a 100644 --- a/src/frp/models/consts/consts.go +++ b/src/frp/models/consts/consts.go @@ -18,20 +18,15 @@ package consts const ( Idle = iota Working + Closed ) -// connection type +// msg type const ( - CtlConn = iota - WorkConn -) - -// msg from client to server -const ( - CSHeartBeatReq = 1 -) - -// msg from server to client -const ( - SCHeartBeatRes = 100 + NewCtlConn = iota + NewWorkConn + NoticeUserConn + NewCtlConnRes + HeartbeatReq + HeartbeatRes ) diff --git a/src/frp/models/msg/msg.go b/src/frp/models/msg/msg.go index 5c62bfb60467b8598e866f41776cdba27e491673..d1b57ad15796f7b414712e8e71d399f89fe7f400 100644 --- a/src/frp/models/msg/msg.go +++ b/src/frp/models/msg/msg.go @@ -19,16 +19,17 @@ type GeneralRes struct { Msg string `json:"msg"` } -type ClientCtlReq struct { - Type int64 `json:"type"` - ProxyName string `json:"proxy_name"` - Passwd string `json:"passwd"` +// messages between control connection of frpc and frps +type ControlReq struct { + Type int64 `json:"type"` + ProxyName string `json:"proxy_name,omitempty"` + AuthKey string `json:"auth_key, omitempty"` + UseEncryption bool `json:"use_encryption, omitempty"` + Timestamp int64 `json:"timestamp, omitempty"` } -type ClientCtlRes struct { - GeneralRes -} - -type ServerCtlReq struct { - Type int64 `json:"type"` +type ControlRes struct { + Type int64 `json:"type"` + Code int64 `json:"code"` + Msg string `json:"msg"` } diff --git a/src/frp/models/server/config.go b/src/frp/models/server/config.go index b2eb63f405452dcfa90e769cfb37314ded94c8b1..23fd9699b089aa5bb876e6d793aa05ef0a3b65c2 100644 --- a/src/frp/models/server/config.go +++ b/src/frp/models/server/config.go @@ -75,9 +75,9 @@ func LoadConf(confFile string) (err error) { proxyServer := &ProxyServer{} proxyServer.Name = name - proxyServer.Passwd, ok = section["passwd"] + proxyServer.AuthToken, ok = section["auth_token"] if !ok { - return fmt.Errorf("Parse ini file error: proxy [%s] no passwd found", proxyServer.Name) + return fmt.Errorf("Parse ini file error: proxy [%s] no auth_token found", proxyServer.Name) } proxyServer.BindAddr, ok = section["bind_addr"] diff --git a/src/frp/models/server/server.go b/src/frp/models/server/server.go index 0b2c3d4981fd5acbfe4cc15063f7ce3e05023742..d6e06dafa00e32b0496aa897a85b65f96b20028c 100644 --- a/src/frp/models/server/server.go +++ b/src/frp/models/server/server.go @@ -25,22 +25,23 @@ import ( ) type ProxyServer struct { - Name string - Passwd string - BindAddr string - ListenPort int64 - Status int64 + Name string + AuthToken string + UseEncryption bool + BindAddr string + ListenPort int64 + Status int64 listener *conn.Listener // accept new connection from remote users ctlMsgChan chan int64 // every time accept a new user conn, put "1" to the channel - cliConnChan chan *conn.Conn // get client conns from control goroutine + workConnChan chan *conn.Conn // get new work conns from control goroutine userConnList *list.List // store user conns mutex sync.Mutex } func (p *ProxyServer) Init() { p.Status = consts.Idle - p.cliConnChan = make(chan *conn.Conn) + p.workConnChan = make(chan *conn.Conn) p.ctlMsgChan = make(chan int64) p.userConnList = list.New() } @@ -109,7 +110,7 @@ func (p *ProxyServer) Start() (err error) { // start another goroutine for join two conns from client and user go func() { for { - cliConn, ok := <-p.cliConnChan + workConn, ok := <-p.workConnChan if !ok { return } @@ -122,7 +123,7 @@ func (p *ProxyServer) Start() (err error) { userConn = element.Value.(*conn.Conn) p.userConnList.Remove(element) } else { - cliConn.Close() + workConn.Close() p.Unlock() continue } @@ -130,9 +131,14 @@ func (p *ProxyServer) Start() (err error) { // msg will transfer to another without modifying // l means local, r means remote - log.Debug("Join two conns, (l[%s] r[%s]) (l[%s] r[%s])", cliConn.GetLocalAddr(), cliConn.GetRemoteAddr(), + log.Debug("Join two connections, (l[%s] r[%s]) (l[%s] r[%s])", workConn.GetLocalAddr(), workConn.GetRemoteAddr(), userConn.GetLocalAddr(), userConn.GetRemoteAddr()) - go conn.Join(cliConn, userConn) + + if p.UseEncryption { + go conn.JoinMore(userConn, workConn, p.AuthToken) + } else { + go conn.Join(userConn, workConn) + } } }() @@ -141,13 +147,15 @@ func (p *ProxyServer) Start() (err error) { func (p *ProxyServer) Close() { p.Lock() - p.Status = consts.Idle - if p.listener != nil { - p.listener.Close() + if p.Status != consts.Closed { + p.Status = consts.Closed + if p.listener != nil { + p.listener.Close() + } + close(p.ctlMsgChan) + close(p.workConnChan) + p.userConnList = list.New() } - close(p.ctlMsgChan) - close(p.cliConnChan) - p.userConnList = list.New() p.Unlock() } @@ -161,6 +169,6 @@ func (p *ProxyServer) WaitUserConn() (closeFlag bool) { return } -func (p *ProxyServer) GetNewCliConn(c *conn.Conn) { - p.cliConnChan <- c +func (p *ProxyServer) RecvNewWorkConn(c *conn.Conn) { + p.workConnChan <- c } diff --git a/src/frp/utils/conn/conn.go b/src/frp/utils/conn/conn.go index fbf4be9944cece0104f7424f6a6dd17c675e601e..eb064c4a8d8e2aa55f6040bb4e1c8af39e13029b 100644 --- a/src/frp/utils/conn/conn.go +++ b/src/frp/utils/conn/conn.go @@ -22,6 +22,7 @@ import ( "sync" "frp/utils/log" + "frp/utils/pcrypto" ) type Listener struct { @@ -127,6 +128,7 @@ func (c *Conn) ReadLine() (buff string, err error) { func (c *Conn) Write(content string) (err error) { _, err = c.TcpConn.Write([]byte(content)) return err + } func (c *Conn) Close() { @@ -151,7 +153,7 @@ func Join(c1 *Conn, c2 *Conn) { var err error _, err = io.Copy(to.TcpConn, from.TcpConn) if err != nil { - log.Warn("join conns error, %v", err) + log.Warn("join connections error, %v", err) } } @@ -161,3 +163,93 @@ func Join(c1 *Conn, c2 *Conn) { wait.Wait() return } + +// messages from c1 to c2 will be encrypted +// and from c2 to c1 will be decrypted +func JoinMore(c1 *Conn, c2 *Conn, cryptKey string) { + var wait sync.WaitGroup + encryptPipe := func(from *Conn, to *Conn, key string) { + defer from.Close() + defer to.Close() + defer wait.Done() + + // we don't care about errors here + PipeEncrypt(from.TcpConn, to.TcpConn, key) + } + + decryptPipe := func(to *Conn, from *Conn, key string) { + defer from.Close() + defer to.Close() + defer wait.Done() + + // we don't care about errors here + PipeDecrypt(to.TcpConn, from.TcpConn, key) + } + + wait.Add(2) + go encryptPipe(c1, c2, cryptKey) + go decryptPipe(c2, c1, cryptKey) + wait.Wait() + log.Debug("One tunnel stopped") + return +} + +// decrypt msg from reader, then write into writer +func PipeDecrypt(r net.Conn, w net.Conn, key string) error { + laes := new(pcrypto.Pcrypto) + if err := laes.Init([]byte(key)); err != nil { + log.Error("Pcrypto Init error: %v", err) + return fmt.Errorf("Pcrypto Init error: %v", err) + } + + nreader := bufio.NewReader(r) + for { + buf, err := nreader.ReadBytes('\n') + if err != nil { + return err + } + + res, err := laes.Decrypt(buf) + if err != nil { + log.Error("Decrypt [%s] error, %v", string(buf), err) + return fmt.Errorf("Decrypt [%s] error: %v", string(buf), err) + } + + _, err = w.Write(res) + if err != nil { + return err + } + } + return nil +} + +// recvive msg from reader, then encrypt msg into write +func PipeEncrypt(r net.Conn, w net.Conn, key string) error { + laes := new(pcrypto.Pcrypto) + if err := laes.Init([]byte(key)); err != nil { + log.Error("Pcrypto Init error: %v", err) + return fmt.Errorf("Pcrypto Init error: %v", err) + } + + nreader := bufio.NewReader(r) + buf := make([]byte, 10*1024) + + for { + n, err := nreader.Read(buf) + if err != nil { + return err + } + res, err := laes.Encrypt(buf[:n]) + if err != nil { + log.Error("Encrypt error: %v", err) + return fmt.Errorf("Encrypt error: %v", err) + } + + res = append(res, '\n') + _, err = w.Write(res) + if err != nil { + return err + } + } + return nil +} diff --git a/src/frp/utils/pcrypto/pcrypto.go b/src/frp/utils/pcrypto/pcrypto.go index e260a3e97739a83ac84975dfee5d8c62c0a4695a..a4772a821e7be980cc54749dc39f64811ed96762 100644 --- a/src/frp/utils/pcrypto/pcrypto.go +++ b/src/frp/utils/pcrypto/pcrypto.go @@ -19,6 +19,7 @@ import ( "compress/gzip" "crypto/aes" "crypto/cipher" + "crypto/md5" "encoding/base64" "encoding/hex" "errors" @@ -33,43 +34,40 @@ type Pcrypto struct { func (pc *Pcrypto) Init(key []byte) error { var err error - pc.pkey = PKCS7Padding(key, aes.BlockSize) + pc.pkey = pKCS7Padding(key, aes.BlockSize) pc.paes, err = aes.NewCipher(pc.pkey) return err } -func (pc *Pcrypto) Encrypto(src []byte) ([]byte, error) { - // aes - src = PKCS7Padding(src, aes.BlockSize) - blockMode := cipher.NewCBCEncrypter(pc.paes, pc.pkey) - crypted := make([]byte, len(src)) - blockMode.CryptBlocks(crypted, src) - +func (pc *Pcrypto) Encrypt(src []byte) ([]byte, error) { // gzip var zbuf bytes.Buffer - zwr := gzip.NewWriter(&zbuf) + zwr, err := gzip.NewWriterLevel(&zbuf, -1) + if err != nil { + return nil, err + } defer zwr.Close() - zwr.Write(crypted) + zwr.Write(src) zwr.Flush() + // aes + src = pKCS7Padding(zbuf.Bytes(), aes.BlockSize) + blockMode := cipher.NewCBCEncrypter(pc.paes, pc.pkey) + crypted := make([]byte, len(src)) + blockMode.CryptBlocks(crypted, src) + // base64 - return []byte(base64.StdEncoding.EncodeToString(zbuf.Bytes())), nil + return []byte(base64.StdEncoding.EncodeToString(crypted)), nil } -func (pc *Pcrypto) Decrypto(str []byte) ([]byte, error) { +func (pc *Pcrypto) Decrypt(str []byte) ([]byte, error) { // base64 data, err := base64.StdEncoding.DecodeString(string(str)) if err != nil { return nil, err } - // gunzip - zbuf := bytes.NewBuffer(data) - zrd, _ := gzip.NewReader(zbuf) - defer zrd.Close() - data, _ = ioutil.ReadAll(zrd) - // aes decryptText, err := hex.DecodeString(fmt.Sprintf("%x", data)) if err != nil { @@ -83,19 +81,35 @@ func (pc *Pcrypto) Decrypto(str []byte) ([]byte, error) { blockMode := cipher.NewCBCDecrypter(pc.paes, pc.pkey) blockMode.CryptBlocks(decryptText, decryptText) - decryptText = PKCS7UnPadding(decryptText) + decryptText = pKCS7UnPadding(decryptText) - return decryptText, nil + // gunzip + zbuf := bytes.NewBuffer(decryptText) + zrd, err := gzip.NewReader(zbuf) + if err != nil { + return nil, err + } + defer zrd.Close() + data, _ = ioutil.ReadAll(zrd) + + return data, nil } -func PKCS7Padding(ciphertext []byte, blockSize int) []byte { +func pKCS7Padding(ciphertext []byte, blockSize int) []byte { padding := blockSize - len(ciphertext)%blockSize padtext := bytes.Repeat([]byte{byte(padding)}, padding) return append(ciphertext, padtext...) } -func PKCS7UnPadding(origData []byte) []byte { +func pKCS7UnPadding(origData []byte) []byte { length := len(origData) unpadding := int(origData[length-1]) return origData[:(length - unpadding)] } + +func GetAuthKey(str string) (authKey string) { + md5Ctx := md5.New() + md5Ctx.Write([]byte(str)) + md5Str := md5Ctx.Sum(nil) + return hex.EncodeToString(md5Str) +} diff --git a/src/frp/utils/pcrypto/pcrypto_test.go b/src/frp/utils/pcrypto/pcrypto_test.go index 43e38f0c6cd7c19c7fca2324329f574c21211cff..e86762fe989574053a90222a3e4b4797dad8ce59 100644 --- a/src/frp/utils/pcrypto/pcrypto_test.go +++ b/src/frp/utils/pcrypto/pcrypto_test.go @@ -15,15 +15,14 @@ package pcrypto import ( - "crypto/aes" "fmt" "testing" ) -func TestEncrypto(t *testing.T) { +func TestEncrypt(t *testing.T) { pp := new(Pcrypto) pp.Init([]byte("Hana")) - res, err := pp.Encrypto([]byte("Just One Test!")) + res, err := pp.Encrypt([]byte("Just One Test!")) if err != nil { t.Fatal(err) } @@ -31,31 +30,18 @@ func TestEncrypto(t *testing.T) { fmt.Printf("[%x]\n", res) } -func TestDecrypto(t *testing.T) { +func TestDecrypt(t *testing.T) { pp := new(Pcrypto) pp.Init([]byte("Hana")) - res, err := pp.Encrypto([]byte("Just One Test!")) + res, err := pp.Encrypt([]byte("Just One Test!")) if err != nil { t.Fatal(err) } - res, err = pp.Decrypto(res) + res, err = pp.Decrypt(res) if err != nil { t.Fatal(err) } fmt.Printf("[%s]\n", string(res)) } - -func TestPKCS7Padding(t *testing.T) { - ltt := []byte("Test_PKCS7Padding") - ltt = PKCS7Padding(ltt, aes.BlockSize) - // fmt.Printf("[%x]\n", (ltt)) -} - -func TestPKCS7UnPadding(t *testing.T) { - ltt := []byte("Test_PKCS7Padding") - ltt = PKCS7Padding(ltt, aes.BlockSize) - ltt = PKCS7UnPadding(ltt) - // fmt.Printf("[%x]\n", ltt) -} diff --git a/src/frp/utils/version/version.go b/src/frp/utils/version/version.go index 680fa8c106f3f3f41690448b989546e9f31d5d79..075a4c8d068075166074c4b3b419abc9ef5d0394 100644 --- a/src/frp/utils/version/version.go +++ b/src/frp/utils/version/version.go @@ -19,7 +19,7 @@ import ( "strings" ) -var version string = "0.2.0" +var version string = "0.3.0" func Full() string { return version