Repository: fatedier/frp (language: go)

Issue:
reduce heartbeat traffic cost



Discussion / hints:

Fix patch:
diff --git a/README.md b/README.md
index 028ff914474..b62553bd109 100644
--- a/README.md
+++ b/README.md
@@ -12,6 +12,21 @@ frp is a fast reverse proxy to help you expose a local server behind a NAT or fi
 
 frp also has a P2P connect mode.
 
+<h3 align="center">Platinum Sponsors</h3>
+<!--platinum sponsors start-->
+
+<p align="center">
+  <a href="https://www.doppler.com/?utm_campaign=github_repo&utm_medium=referral&utm_content=frp&utm_source=github" target="_blank">
+    <img width="400px" src="https://raw.githubusercontent.com/fatedier/frp/dev/doc/pic/sponsor_doppler.png">
+  </a>
+</p>
+
+<!--platinum sponsors end-->
+
+<h3 align="center">Silver Sponsors</h3>
+
+* Sakura Frp - 欢迎点击 "加入我们"
+
 ## Table of Contents
 
 <!-- vim-markdown-toc GFM -->
@@ -77,7 +92,9 @@ frp also has a P2P connect mode.
 
 frp is under development. Try the latest release version in the `master` branch, or use the `dev` branch for the version in development.
 
-**The protocol might change at a release and we don't promise backwards compatibility. Please check the release log when upgrading the client and the server.**
+We are working on v2 version and trying to do some code refactor and improvements. It won't be compatible with v1.
+
+We will switch v0 to v1 at the right time and only accept bug fixes and improvements instead of big feature requirements.
 
 ## Architecture
 
@@ -867,7 +884,7 @@ In this example, it will set header `X-From-Where: frp` in the HTTP request.
 
 This feature is for http proxy only.
 
-You can get user's real IP from HTTP request headers `X-Forwarded-For` and `X-Real-IP`.
+You can get user's real IP from HTTP request headers `X-Forwarded-For`.
 
 #### Proxy Protocol
 
@@ -983,11 +1000,13 @@ server_port = 7000
 type = tcpmux
 multiplexer = httpconnect
 custom_domains = test1
+local_port = 80
 
 [proxy2]
 type = tcpmux
 multiplexer = httpconnect
 custom_domains = test2
+local_port = 8080
 ```
 
 In the above configuration - frps can be contacted on port 1337 with a HTTP CONNECT header such as:
diff --git a/README_zh.md b/README_zh.md
index 4a37b370992..1ecf2606202 100644
--- a/README_zh.md
+++ b/README_zh.md
@@ -7,6 +7,21 @@
 
 frp 是一个专注于内网穿透的高性能的反向代理应用，支持 TCP、UDP、HTTP、HTTPS 等多种协议。可以将内网服务以安全、便捷的方式通过具有公网 IP 节点的中转暴露到公网。
 
+<h3 align="center">Platinum Sponsors</h3>
+<!--platinum sponsors start-->
+
+<p align="center">
+  <a href="https://www.doppler.com/?utm_campaign=github_repo&utm_medium=referral&utm_content=frp&utm_source=github" target="_blank">
+    <img width="400px" src="https://raw.githubusercontent.com/fatedier/frp/dev/doc/pic/sponsor_doppler.png">
+  </a>
+</p>
+
+<!--platinum sponsors end-->
+
+<h3 align="center">Silver Sponsors</h3>
+
+* Sakura Frp - 欢迎点击 "加入我们"
+
 ## 为什么使用 frp ？
 
 通过在具有公网 IP 的节点上部署 frp 服务端，可以轻松地将内网服务穿透到公网，同时提供诸多专业的功能特性，这包括：
@@ -25,6 +40,10 @@ frp 目前已被很多公司广泛用于测试、生产环境。
 
 master 分支用于发布稳定版本，dev 分支用于开发，您可以尝试下载最新的 release 版本进行测试。
 
+我们正在进行 v2 大版本的开发，将会尝试在各个方面进行重构和升级，且不会与 v1 版本进行兼容，预计会持续一段时间。
+
+现在的 v0 版本将会在合适的时间切换为 v1 版本并且保证兼容性，后续只做 bug 修复和优化，不再进行大的功能性更新。
+
 ## 文档
 
 完整文档已经迁移至 [https://gofrp.org](https://gofrp.org/docs)。
diff --git a/Release.md b/Release.md
index 53e2d0be49c..bd57c959c85 100644
--- a/Release.md
+++ b/Release.md
@@ -1,8 +1,12 @@
 ### New
 
-* Add `/healthz` API.
-* frpc support `disable_custom_tls_first_byte` .If set true, frpc will not send custom header byte.
+* Added `connect_server_local_ip` in frpc to specify local IP connected to frps.
+* Added `tcp_mux_keepalive_interval` both in frpc and frps to set `tcp_mux` keepalive interval seconds if `tcp_mux` is enabled. After using this params, you can set `heartbeat_interval` to `-1` to disable application layer heartbeat to reduce traffic usage(Make sure frps is in the latest version).
 
 ### Improve
 
-* Use go standard embed package instead of statik.
+* Server Plugin: Added `client_address` in Login Operation.
+
+### Fix
+
+* Remove authentication for healthz api.
diff --git a/client/admin.go b/client/admin.go
index 364b3105eb3..fff169f82b6 100644
--- a/client/admin.go
+++ b/client/admin.go
@@ -34,20 +34,22 @@ func (svr *Service) RunAdminServer(address string) (err error) {
 	// url router
 	router := mux.NewRouter()
 
+	router.HandleFunc("/healthz", svr.healthz)
+
+	subRouter := router.NewRoute().Subrouter()
 	user, passwd := svr.cfg.AdminUser, svr.cfg.AdminPwd
-	router.Use(frpNet.NewHTTPAuthMiddleware(user, passwd).Middleware)
+	subRouter.Use(frpNet.NewHTTPAuthMiddleware(user, passwd).Middleware)
 
 	// api, see admin_api.go
-	router.HandleFunc("/healthz", svr.healthz)
-	router.HandleFunc("/api/reload", svr.apiReload).Methods("GET")
-	router.HandleFunc("/api/status", svr.apiStatus).Methods("GET")
-	router.HandleFunc("/api/config", svr.apiGetConfig).Methods("GET")
-	router.HandleFunc("/api/config", svr.apiPutConfig).Methods("PUT")
+	subRouter.HandleFunc("/api/reload", svr.apiReload).Methods("GET")
+	subRouter.HandleFunc("/api/status", svr.apiStatus).Methods("GET")
+	subRouter.HandleFunc("/api/config", svr.apiGetConfig).Methods("GET")
+	subRouter.HandleFunc("/api/config", svr.apiPutConfig).Methods("PUT")
 
 	// view
-	router.Handle("/favicon.ico", http.FileServer(assets.FileSystem)).Methods("GET")
-	router.PathPrefix("/static/").Handler(frpNet.MakeHTTPGzipHandler(http.StripPrefix("/static/", http.FileServer(assets.FileSystem)))).Methods("GET")
-	router.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
+	subRouter.Handle("/favicon.ico", http.FileServer(assets.FileSystem)).Methods("GET")
+	subRouter.PathPrefix("/static/").Handler(frpNet.MakeHTTPGzipHandler(http.StripPrefix("/static/", http.FileServer(assets.FileSystem)))).Methods("GET")
+	subRouter.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
 		http.Redirect(w, r, "/static/", http.StatusMovedPermanently)
 	})
 
diff --git a/client/control.go b/client/control.go
index 067fe37f581..f9af8958eae 100644
--- a/client/control.go
+++ b/client/control.go
@@ -34,6 +34,7 @@ import (
 
 	"github.com/fatedier/golib/control/shutdown"
 	"github.com/fatedier/golib/crypto"
+	libdial "github.com/fatedier/golib/net/dial"
 	fmux "github.com/hashicorp/yamux"
 )
 
@@ -234,12 +235,36 @@ func (ctl *Control) connectServer() (conn net.Conn, err error) {
 			}
 		}
 
-		address := net.JoinHostPort(ctl.clientCfg.ServerAddr, strconv.Itoa(ctl.clientCfg.ServerPort))
-		conn, err = frpNet.ConnectServerByProxyWithTLS(ctl.clientCfg.HTTPProxy, ctl.clientCfg.Protocol, address, tlsConfig, ctl.clientCfg.DisableCustomTLSFirstByte)
-
+		proxyType, addr, auth, err := libdial.ParseProxyURL(ctl.clientCfg.HTTPProxy)
+		if err != nil {
+			xl.Error("fail to parse proxy url")
+			return nil, err
+		}
+		dialOptions := []libdial.DialOption{}
+		protocol := ctl.clientCfg.Protocol
+		if protocol == "websocket" {
+			protocol = "tcp"
+			dialOptions = append(dialOptions, libdial.WithAfterHook(libdial.AfterHook{Hook: frpNet.DialHookWebsocket()}))
+		}
+		if ctl.clientCfg.ConnectServerLocalIP != "" {
+			dialOptions = append(dialOptions, libdial.WithLocalAddr(ctl.clientCfg.ConnectServerLocalIP))
+		}
+		dialOptions = append(dialOptions,
+			libdial.WithProtocol(protocol),
+			libdial.WithProxy(proxyType, addr),
+			libdial.WithProxyAuth(auth),
+			libdial.WithTLSConfig(tlsConfig),
+			libdial.WithAfterHook(libdial.AfterHook{
+				Hook: frpNet.DialHookCustomTLSHeadByte(tlsConfig != nil, ctl.clientCfg.DisableCustomTLSFirstByte),
+			}),
+		)
+		conn, err = libdial.Dial(
+			net.JoinHostPort(ctl.clientCfg.ServerAddr, strconv.Itoa(ctl.clientCfg.ServerPort)),
+			dialOptions...,
+		)
 		if err != nil {
 			xl.Warn("start new connection to server error: %v", err)
-			return
+			return nil, err
 		}
 	}
 	return
@@ -308,16 +333,27 @@ func (ctl *Control) msgHandler() {
 	}()
 	defer ctl.msgHandlerShutdown.Done()
 
-	hbSend := time.NewTicker(time.Duration(ctl.clientCfg.HeartbeatInterval) * time.Second)
-	defer hbSend.Stop()
-	hbCheck := time.NewTicker(time.Second)
-	defer hbCheck.Stop()
+	var hbSendCh <-chan time.Time
+	// TODO(fatedier): disable heartbeat if TCPMux is enabled.
+	// Just keep it here to keep compatible with old version frps.
+	if ctl.clientCfg.HeartbeatInterval > 0 {
+		hbSend := time.NewTicker(time.Duration(ctl.clientCfg.HeartbeatInterval) * time.Second)
+		defer hbSend.Stop()
+		hbSendCh = hbSend.C
+	}
 
-	ctl.lastPong = time.Now()
+	var hbCheckCh <-chan time.Time
+	// Check heartbeat timeout only if TCPMux is not enabled and users don't disable heartbeat feature.
+	if ctl.clientCfg.HeartbeatInterval > 0 && ctl.clientCfg.HeartbeatTimeout > 0 && !ctl.clientCfg.TCPMux {
+		hbCheck := time.NewTicker(time.Second)
+		defer hbCheck.Stop()
+		hbCheckCh = hbCheck.C
+	}
 
+	ctl.lastPong = time.Now()
 	for {
 		select {
-		case <-hbSend.C:
+		case <-hbSendCh:
 			// send heartbeat to server
 			xl.Debug("send heartbeat to server")
 			pingMsg := &msg.Ping{}
@@ -326,7 +362,7 @@ func (ctl *Control) msgHandler() {
 				return
 			}
 			ctl.sendCh <- pingMsg
-		case <-hbCheck.C:
+		case <-hbCheckCh:
 			if time.Since(ctl.lastPong) > time.Duration(ctl.clientCfg.HeartbeatTimeout)*time.Second {
 				xl.Warn("heartbeat timeout")
 				// let reader() stop
diff --git a/client/proxy/proxy.go b/client/proxy/proxy.go
index 47ab03ca42f..39c26d26741 100644
--- a/client/proxy/proxy.go
+++ b/client/proxy/proxy.go
@@ -35,6 +35,7 @@ import (
 
 	"github.com/fatedier/golib/errors"
 	frpIo "github.com/fatedier/golib/io"
+	libdial "github.com/fatedier/golib/net/dial"
 	"github.com/fatedier/golib/pool"
 	fmux "github.com/hashicorp/yamux"
 	pp "github.com/pires/go-proxyproto"
@@ -790,7 +791,7 @@ func HandleTCPWorkConnection(ctx context.Context, localInfo *config.LocalSvrConf
 		return
 	}
 
-	localConn, err := frpNet.ConnectServer("tcp", fmt.Sprintf("%s:%d", localInfo.LocalIP, localInfo.LocalPort))
+	localConn, err := libdial.Dial(net.JoinHostPort(localInfo.LocalIP, strconv.Itoa(localInfo.LocalPort)))
 	if err != nil {
 		workConn.Close()
 		xl.Error("connect to local service [%s:%d] error: %v", localInfo.LocalIP, localInfo.LocalPort, err)
diff --git a/client/service.go b/client/service.go
index 8b88003477a..815145b7b59 100644
--- a/client/service.go
+++ b/client/service.go
@@ -36,6 +36,7 @@ import (
 	frpNet "github.com/fatedier/frp/pkg/util/net"
 	"github.com/fatedier/frp/pkg/util/version"
 	"github.com/fatedier/frp/pkg/util/xlog"
+	libdial "github.com/fatedier/golib/net/dial"
 
 	fmux "github.com/hashicorp/yamux"
 )
@@ -228,8 +229,33 @@ func (svr *Service) login() (conn net.Conn, session *fmux.Session, err error) {
 		}
 	}
 
-	address := net.JoinHostPort(svr.cfg.ServerAddr, strconv.Itoa(svr.cfg.ServerPort))
-	conn, err = frpNet.ConnectServerByProxyWithTLS(svr.cfg.HTTPProxy, svr.cfg.Protocol, address, tlsConfig, svr.cfg.DisableCustomTLSFirstByte)
+	proxyType, addr, auth, err := libdial.ParseProxyURL(svr.cfg.HTTPProxy)
+	if err != nil {
+		xl.Error("fail to parse proxy url")
+		return
+	}
+	dialOptions := []libdial.DialOption{}
+	protocol := svr.cfg.Protocol
+	if protocol == "websocket" {
+		protocol = "tcp"
+		dialOptions = append(dialOptions, libdial.WithAfterHook(libdial.AfterHook{Hook: frpNet.DialHookWebsocket()}))
+	}
+	if svr.cfg.ConnectServerLocalIP != "" {
+		dialOptions = append(dialOptions, libdial.WithLocalAddr(svr.cfg.ConnectServerLocalIP))
+	}
+	dialOptions = append(dialOptions,
+		libdial.WithProtocol(protocol),
+		libdial.WithProxy(proxyType, addr),
+		libdial.WithProxyAuth(auth),
+		libdial.WithTLSConfig(tlsConfig),
+		libdial.WithAfterHook(libdial.AfterHook{
+			Hook: frpNet.DialHookCustomTLSHeadByte(tlsConfig != nil, svr.cfg.DisableCustomTLSFirstByte),
+		}),
+	)
+	conn, err = libdial.Dial(
+		net.JoinHostPort(svr.cfg.ServerAddr, strconv.Itoa(svr.cfg.ServerPort)),
+		dialOptions...,
+	)
 	if err != nil {
 		return
 	}
@@ -245,7 +271,7 @@ func (svr *Service) login() (conn net.Conn, session *fmux.Session, err error) {
 
 	if svr.cfg.TCPMux {
 		fmuxCfg := fmux.DefaultConfig()
-		fmuxCfg.KeepAliveInterval = 20 * time.Second
+		fmuxCfg.KeepAliveInterval = time.Duration(svr.cfg.TCPMuxKeepaliveInterval) * time.Second
 		fmuxCfg.LogOutput = io.Discard
 		session, err = fmux.Client(conn, fmuxCfg)
 		if err != nil {
diff --git a/conf/frpc_full.ini b/conf/frpc_full.ini
index 21fc5351e10..7be2608d12d 100644
--- a/conf/frpc_full.ini
+++ b/conf/frpc_full.ini
@@ -61,6 +61,9 @@ pool_count = 5
 
 # if tcp stream multiplexing is used, default is true, it must be same with frps
 tcp_mux = true
+# specify keep alive interval for tcp mux.
+# only valid if tcp_mux is true.
+# tcp_mux_keepalive_interval = 60
 
 # your proxy name will be changed to {user}.{proxy}
 user = your_name
@@ -73,6 +76,10 @@ login_fail_exit = true
 # now it supports tcp, kcp and websocket, default is tcp
 protocol = tcp
 
+# set client binding ip when connect server, default is empty.
+# only when protocol = tcp or websocket, the value will be used.
+connect_server_local_ip = 0.0.0.0
+
 # if tls_enable is true, frpc will connect frps by tls
 tls_enable = true
 
@@ -89,7 +96,8 @@ tls_enable = true
 # start = ssh,dns
 
 # heartbeat configure, it's not recommended to modify the default value
-# the default value of heartbeat_interval is 10 and heartbeat_timeout is 90
+# The default value of heartbeat_interval is 10 and heartbeat_timeout is 90. Set negative value
+# to disable it.
 # heartbeat_interval = 30
 # heartbeat_timeout = 90
 
diff --git a/conf/frps_full.ini b/conf/frps_full.ini
index c3da4e2c142..4aef9774d55 100644
--- a/conf/frps_full.ini
+++ b/conf/frps_full.ini
@@ -92,7 +92,7 @@ oidc_skip_expiry_check = false
 oidc_skip_issuer_check = false
 
 # heartbeat configure, it's not recommended to modify the default value
-# the default value of heartbeat_timeout is 90
+# the default value of heartbeat_timeout is 90. Set negative value to disable it.
 # heartbeat_timeout = 90
 
 # user_conn_timeout configure, it's not recommended to modify the default value
@@ -121,6 +121,9 @@ subdomain_host = frps.com
 
 # if tcp stream multiplexing is used, default is true
 tcp_mux = true
+# specify keep alive interval for tcp mux.
+# only valid if tcp_mux is true.
+# tcp_mux_keepalive_interval = 60
 
 # custom 404 page for HTTP requests
 # custom_404_page = /path/to/404.html
diff --git a/doc/pic/sponsor_doppler.png b/doc/pic/sponsor_doppler.png
new file mode 100644
index 00000000000..ea817558d13
Binary files /dev/null and b/doc/pic/sponsor_doppler.png differ
diff --git a/doc/server_plugin.md b/doc/server_plugin.md
index 9087307c8ae..3697053b30e 100644
--- a/doc/server_plugin.md
+++ b/doc/server_plugin.md
@@ -88,7 +88,8 @@ Client login operation
         "privilege_key": <string>,
         "run_id": <string>,
         "pool_count": <int>,
-        "metas": map<string>string
+        "metas": map<string>string,
+        "client_address": <string>
     }
 }
 ```
diff --git a/doc/server_plugin_zh.md b/doc/server_plugin_zh.md
deleted file mode 100644
index 353330b209a..00000000000
--- a/doc/server_plugin_zh.md
+++ /dev/null
@@ -1,228 +0,0 @@
-### 服务端管理插件
-
-frp 管理插件的作用是在不侵入自身代码的前提下，扩展 frp 服务端的能力。
-
-frp 管理插件会以单独进程的形式运行，并且监听在一个端口上，对外提供 RPC 接口，响应 frps 的请求。
-
-frps 在执行某些操作前，会根据配置向管理插件发送 RPC 请求，根据管理插件的响应来执行相应的操作。
-
-### RPC 请求
-
-管理插件接收到操作请求后，可以给出三种回应。
-
-* 拒绝操作，需要返回拒绝操作的原因。
-* 允许操作，不需要修改操作内容。
-* 允许操作，对操作请求进行修改后，返回修改后的内容。
-
-### 接口
-
-接口路径可以在 frps 配置中为每个插件单独配置，这里以 `/handler` 为例。
-
-Request
-
-```
-POST /handler
-{
-    "version": "0.1.0",
-    "op": "Login",
-    "content": {
-        ... // 具体的操作信息
-    }
-}
-
-请求 Header
-X-Frp-Reqid: 用于追踪请求
-```
-
-Response
-
-非 200 的返回都认为是请求异常。
-
-拒绝执行操作
-
-```
-{
-   "reject": true,
-   "reject_reason": "invalid user"
-}
-```
-
-允许且内容不需要变动
-
-```
-{
-    "reject": false,
-    "unchange": true
-}
-```
-
-允许且需要替换操作内容
-
-```
-{
-    "unchange": "false",
-    "content": {
-        ... // 替换后的操作信息，格式必须和请求时的一致
-    }
-}
-```
-
-### 操作类型
-
-目前插件支持管理的操作类型有 `Login`、`NewProxy`、`Ping`、`NewWorkConn` 和 `NewUserConn`。
-
-#### Login
-
-用户登录操作信息
-
-```
-{
-    "content": {
-        "version": <string>,
-        "hostname": <string>,
-        "os": <string>,
-        "arch": <string>,
-        "user": <string>,
-        "timestamp": <int64>,
-        "privilege_key": <string>,
-        "run_id": <string>,
-        "pool_count": <int>,
-        "metas": map<string>string
-    }
-}
-```
-
-#### NewProxy
-
-创建代理的相关信息
-
-```
-{
-    "content": {
-        "user": {
-            "user": <string>,
-            "metas": map<string>string
-        },
-        "proxy_name": <string>,
-        "proxy_type": <string>,
-        "use_encryption": <bool>,
-        "use_compression": <bool>,
-        "group": <string>,
-        "group_key": <string>,
-
-        // tcp and udp only
-        "remote_port": <int>,
-
-        // http and https only
-        "custom_domains": []<string>,
-        "subdomain": <string>,
-        "locations": <string>,
-        "http_user": <string>,
-        "http_pwd": <string>,
-        "host_header_rewrite": <string>,
-        "headers": map<string>string,
-
-        "metas": map<string>string
-    }
-}
-```
-
-#### Ping
-
-心跳相关信息
-
-```
-{
-    "content": {
-        "user": {
-            "user": <string>,
-            "metas": map<string>string
-            "run_id": <string>
-        },
-        "timestamp": <int64>,
-        "privilege_key": <string>
-    }
-}
-```
-
-#### NewWorkConn
-
-新增 `frpc` 连接相关信息
-
-```
-{
-    "content": {
-        "user": {
-            "user": <string>,
-            "metas": map<string>string
-            "run_id": <string>
-        },
-        "run_id": <string>
-        "timestamp": <int64>,
-        "privilege_key": <string>
-    }
-}
-```
-
-#### NewUserConn
-
-新增 `proxy` 连接相关信息 (支持 `tcp`、`stcp`、`https` 和 `tcpmux` 协议)。
-
-```
-{
-    "content": {
-        "user": {
-            "user": <string>,
-            "metas": map<string>string
-            "run_id": <string>
-        },
-        "proxy_name": <string>,
-        "proxy_type": <string>,
-        "remote_addr": <string>
-    }
-}
-```
-
-
-### frps 中插件配置
-
-```ini
-[common]
-bind_port = 7000
-
-[plugin.user-manager]
-addr = 127.0.0.1:9000
-path = /handler
-ops = Login
-
-[plugin.port-manager]
-addr = 127.0.0.1:9001
-path = /handler
-ops = NewProxy
-```
-
-addr: 插件监听的网络地址。
-path: 插件监听的 HTTP 请求路径。
-ops: 插件需要处理的操作列表，多个 op 以英文逗号分隔。
-
-### 元数据
-
-为了减少 frps 的代码修改，同时提高管理插件的扩展能力，在 frpc 的配置文件中引入自定义元数据的概念。元数据会在调用 RPC 请求时发送给插件。
-
-元数据以 `meta_` 开头，可以配置多个，元数据分为两种，一种配置在 `common` 下，一种配置在各个 proxy 中。
-
-```
-# frpc.ini
-[common]
-server_addr = 127.0.0.1
-server_port = 7000
-user = fake
-meta_token = fake
-meta_version = 1.0.0
-
-[ssh]
-type = tcp
-local_port = 22
-remote_port = 6000
-meta_id = 123
-```
diff --git a/dockerfiles/Dockerfile-for-frpc b/dockerfiles/Dockerfile-for-frpc
index ad3c81dc7f4..fce3d57a38e 100644
--- a/dockerfiles/Dockerfile-for-frpc
+++ b/dockerfiles/Dockerfile-for-frpc
@@ -1,11 +1,11 @@
-FROM alpine:3.12.0 AS temp
+FROM alpine:3 AS temp
 
 COPY bin/frpc /tmp
 
 RUN chmod -R 777 /tmp/frpc
 
 
-FROM alpine:3.12.0
+FROM alpine:3
 
 WORKDIR /app
 
diff --git a/dockerfiles/Dockerfile-for-frps b/dockerfiles/Dockerfile-for-frps
index 771080417e5..3d65a9e4617 100644
--- a/dockerfiles/Dockerfile-for-frps
+++ b/dockerfiles/Dockerfile-for-frps
@@ -1,11 +1,11 @@
-FROM alpine:3.12.0 AS temp
+FROM alpine:3 AS temp
 
 COPY bin/frps /tmp
 
 RUN chmod -R 777 /tmp/frps
 
 
-FROM alpine:3.12.0
+FROM alpine:3
 
 WORKDIR /app
 
diff --git a/go.mod b/go.mod
index 938089c7e74..fd5759bf245 100644
--- a/go.mod
+++ b/go.mod
@@ -6,15 +6,13 @@ require (
 	github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5
 	github.com/coreos/go-oidc v2.2.1+incompatible
 	github.com/fatedier/beego v0.0.0-20171024143340-6c6a4f5bd5eb
-	github.com/fatedier/golib v0.1.1-0.20200901083111-1f870741e185
+	github.com/fatedier/golib v0.1.1-0.20220119075718-78e5cf8c00ee
 	github.com/fatedier/kcp-go v2.0.4-0.20190803094908-fe8645b0a904+incompatible
 	github.com/go-playground/validator/v10 v10.6.1
 	github.com/google/uuid v1.2.0
 	github.com/gorilla/mux v1.8.0
 	github.com/gorilla/websocket v1.4.2
 	github.com/hashicorp/yamux v0.0.0-20210707203944-259a57b3608c
-	github.com/klauspost/cpuid v1.2.0 // indirect
-	github.com/klauspost/reedsolomon v1.9.1 // indirect
 	github.com/leodido/go-urn v1.2.1 // indirect
 	github.com/onsi/ginkgo v1.16.4
 	github.com/onsi/gomega v1.13.0
@@ -24,10 +22,6 @@ require (
 	github.com/rodaine/table v1.0.1
 	github.com/spf13/cobra v1.1.3
 	github.com/stretchr/testify v1.7.0
-	github.com/templexxx/cpufeat v0.0.0-20170927014610-3794dfbfb047 // indirect
-	github.com/templexxx/xor v0.0.0-20170926022130-0af8e873c554 // indirect
-	github.com/tjfoc/gmsm v0.0.0-20171124023159-98aa888b79d8 // indirect
-	github.com/xtaci/lossyconn v0.0.0-20190602105132-8df528c0c9ae // indirect
 	golang.org/x/net v0.0.0-20210428140749-89ef3d95e781
 	golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d
 	golang.org/x/sys v0.0.0-20210616094352-59db8d763f22 // indirect
diff --git a/go.sum b/go.sum
index de5929c80a8..95ba6744a44 100644
--- a/go.sum
+++ b/go.sum
@@ -63,6 +63,7 @@ github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWR
 github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI=
 github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU=
 github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
+github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc=
 github.com/coreos/bbolt v1.3.2/go.mod h1:iRUV2dpdMOn7Bo10OQBFzIJO9kkE559Wcmn+qkEiiKk=
 github.com/coreos/etcd v3.3.13+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE=
 github.com/coreos/go-oidc v2.2.1+incompatible h1:mh48q/BqXqgjVHpy2ZY7WnWAbenxRjsz9N1i1YxjHAk=
@@ -80,13 +81,15 @@ github.com/dgryski/go-sip13 v0.0.0-20181026042036-e10d5fee7954/go.mod h1:vAd38F8
 github.com/docopt/docopt-go v0.0.0-20180111231733-ee0de3bc6815/go.mod h1:WwZ+bS3ebgob9U8Nd0kOddGdZWjyMGR8Wziv+TBNwSE=
 github.com/elazarl/goproxy v0.0.0-20180725130230-947c36da3153/go.mod h1:/Zj4wYkgs4iZTTu3o/KG3Itv/qCCa8VVMlb3i9OVuzc=
 github.com/emicklei/go-restful v0.0.0-20170410110728-ff4f55a20633/go.mod h1:otzb+WCGbkyDHkqmQmT5YD2WR4BBwUdeQoFo8l/7tVs=
+github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
 github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
+github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98=
 github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
 github.com/evanphx/json-patch v4.9.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk=
 github.com/fatedier/beego v0.0.0-20171024143340-6c6a4f5bd5eb h1:wCrNShQidLmvVWn/0PikGmpdP0vtQmnvyRg3ZBEhczw=
 github.com/fatedier/beego v0.0.0-20171024143340-6c6a4f5bd5eb/go.mod h1:wx3gB6dbIfBRcucp94PI9Bt3I0F2c/MyNEWuhzpWiwk=
-github.com/fatedier/golib v0.1.1-0.20200901083111-1f870741e185 h1:2p4W5xYizIYwhiGQgeHOQcRD2O84j0tjD40P6gUCRrk=
-github.com/fatedier/golib v0.1.1-0.20200901083111-1f870741e185/go.mod h1:MUs+IH/MGJNz5Cj2JVJBPZBKw2exON7LzO3HrJHmGiQ=
+github.com/fatedier/golib v0.1.1-0.20220119075718-78e5cf8c00ee h1:iS0wlj2uZPxh3pciAf/HTzi88Kqu7DPh1jNKgJaFhtI=
+github.com/fatedier/golib v0.1.1-0.20220119075718-78e5cf8c00ee/go.mod h1:fLV0TLwHqrnB/L3jbNl67Gn6PCLggDGHniX1wLrA2Qo=
 github.com/fatedier/kcp-go v2.0.4-0.20190803094908-fe8645b0a904+incompatible h1:ssXat9YXFvigNge/IkkZvFMn8yeYKFX+uI6wn2mLJ74=
 github.com/fatedier/kcp-go v2.0.4-0.20190803094908-fe8645b0a904+incompatible/go.mod h1:YpCOaxj7vvMThhIQ9AfTOPW2sfztQR5WDfs7AflSy4s=
 github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4=
@@ -231,10 +234,10 @@ github.com/julienschmidt/httprouter v1.3.0/go.mod h1:JR6WtHb+2LUe8TCKY3cZOxFyyO8
 github.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvWXihfKN4Q=
 github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8=
 github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
-github.com/klauspost/cpuid v1.2.0 h1:NMpwD2G9JSFOE1/TJjGSo5zG7Yb2bTe7eq1jH+irmeE=
-github.com/klauspost/cpuid v1.2.0/go.mod h1:Pj4uuM528wm8OyEC2QMXAi2YiTZ96dNQPGgoMS4s3ek=
-github.com/klauspost/reedsolomon v1.9.1 h1:kYrT1MlR4JH6PqOpC+okdb9CDTcwEC/BqpzK4WFyXL8=
-github.com/klauspost/reedsolomon v1.9.1/go.mod h1:CwCi+NUr9pqSVktrkN+Ondf06rkhYZ/pcNv7fu+8Un4=
+github.com/klauspost/cpuid/v2 v2.0.6 h1:dQ5ueTiftKxp0gyjKSx5+8BtPWkyQbd95m8Gys/RarI=
+github.com/klauspost/cpuid/v2 v2.0.6/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
+github.com/klauspost/reedsolomon v1.9.15 h1:g2erWKD2M6rgnPf89fCji6jNlhMKMdXcuNHMW1SYCIo=
+github.com/klauspost/reedsolomon v1.9.15/go.mod h1:eqPAcE7xar5CIzcdfwydOEdcmchAKAP/qs14y4GCBOk=
 github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
 github.com/konsorten/go-windows-terminal-sequences v1.0.3/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
 github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc=
@@ -371,16 +374,16 @@ github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/
 github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY=
 github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
 github.com/subosito/gotenv v1.2.0/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69rRypqCw=
-github.com/templexxx/cpufeat v0.0.0-20170927014610-3794dfbfb047 h1:K+jtWCOuZgCra7eXZ/VWn2FbJmrA/D058mTXhh2rq+8=
-github.com/templexxx/cpufeat v0.0.0-20170927014610-3794dfbfb047/go.mod h1:wM7WEvslTq+iOEAMDLSzhVuOt5BRZ05WirO+b09GHQU=
-github.com/templexxx/xor v0.0.0-20170926022130-0af8e873c554 h1:pexgSe+JCFuxG+uoMZLO+ce8KHtdHGhst4cs6rw3gmk=
-github.com/templexxx/xor v0.0.0-20170926022130-0af8e873c554/go.mod h1:5XA7W9S6mni3h5uvOC75dA3m9CCCaS83lltmc0ukdi4=
-github.com/tjfoc/gmsm v0.0.0-20171124023159-98aa888b79d8 h1:6CNSDqI1wiE+JqyOy5Qt/yo/DoNI2/QmmOZeiCid2Nw=
-github.com/tjfoc/gmsm v0.0.0-20171124023159-98aa888b79d8/go.mod h1:XxO4hdhhrzAd+G4CjDqaOkd0hUzmtPR/d3EiBBMn/wc=
+github.com/templexxx/cpufeat v0.0.0-20180724012125-cef66df7f161 h1:89CEmDvlq/F7SJEOqkIdNDGJXrQIhuIx9D2DBXjavSU=
+github.com/templexxx/cpufeat v0.0.0-20180724012125-cef66df7f161/go.mod h1:wM7WEvslTq+iOEAMDLSzhVuOt5BRZ05WirO+b09GHQU=
+github.com/templexxx/xor v0.0.0-20191217153810-f85b25db303b h1:fj5tQ8acgNUr6O8LEplsxDhUIe2573iLkJc+PqnzZTI=
+github.com/templexxx/xor v0.0.0-20191217153810-f85b25db303b/go.mod h1:5XA7W9S6mni3h5uvOC75dA3m9CCCaS83lltmc0ukdi4=
+github.com/tjfoc/gmsm v1.4.1 h1:aMe1GlZb+0bLjn+cKTPEvvn9oUEBlJitaZiiBwsbgho=
+github.com/tjfoc/gmsm v1.4.1/go.mod h1:j4INPkHWMrhJb38G+J6W4Tw0AbuN8Thu3PbdVYhVcTE=
 github.com/tmc/grpc-websocket-proxy v0.0.0-20190109142713-0ad062ec5ee5/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U=
 github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU=
-github.com/xtaci/lossyconn v0.0.0-20190602105132-8df528c0c9ae h1:J0GxkO96kL4WF+AIT3M4mfUVinOCPgf2uUWYFUzN0sM=
-github.com/xtaci/lossyconn v0.0.0-20190602105132-8df528c0c9ae/go.mod h1:gXtu8J62kEgmN++bm9BVICuT/e8yiLI2KFobd/TRFsE=
+github.com/xtaci/lossyconn v0.0.0-20200209145036-adba10fffc37 h1:EWU6Pktpas0n8lLQwDsRyZfmkPeRbdgPtW609es+/9E=
+github.com/xtaci/lossyconn v0.0.0-20200209145036-adba10fffc37/go.mod h1:HpMP7DB2CyokmAh4lp0EQnnWhmycP/TvwBGzvuie+H0=
 github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
 github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
 go.etcd.io/bbolt v1.3.2/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU=
@@ -393,7 +396,6 @@ go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/
 go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q=
 golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
 golang.org/x/crypto v0.0.0-20181029021203-45a5f77698d3/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
-golang.org/x/crypto v0.0.0-20190228161510-8dd112bcdc25/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
 golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
 golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
 golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
@@ -401,6 +403,7 @@ golang.org/x/crypto v0.0.0-20190611184440-5c40567a22f8/go.mod h1:yigFU9vqHzYiE8U
 golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
 golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
 golang.org/x/crypto v0.0.0-20201002170205-7f63de1d35b0/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
+golang.org/x/crypto v0.0.0-20201012173705-84dcc777aaee/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
 golang.org/x/crypto v0.0.0-20210220033148-5ea612d1eb83 h1:/ZScEX8SfEmUGRHs0gxpqteO5nfNW6axyZbBdw9A12g=
 golang.org/x/crypto v0.0.0-20210220033148-5ea612d1eb83/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I=
 golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
@@ -442,7 +445,6 @@ golang.org/x/net v0.0.0-20181201002055-351d144fa1fc/go.mod h1:mL1N/T3taQHkDXs73r
 golang.org/x/net v0.0.0-20181220203305-927f97764cc3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
 golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
 golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
-golang.org/x/net v0.0.0-20190228165749-92fc7df08ae7/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
 golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
 golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
 golang.org/x/net v0.0.0-20190501004415-9ce7a6920f09/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
@@ -461,6 +463,7 @@ golang.org/x/net v0.0.0-20200301022130-244492dfa37a/go.mod h1:z5CRVTTTmAJ677TzLL
 golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
 golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
 golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
+golang.org/x/net v0.0.0-20201010224723-4f7140c49acb/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
 golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
 golang.org/x/net v0.0.0-20210224082022-3d97a244fca7/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
 golang.org/x/net v0.0.0-20210428140749-89ef3d95e781 h1:DzZ89McO9/gWPsQXS/FVKAlG02ZjaQ6AlZRBimEYOd0=
@@ -619,9 +622,11 @@ google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZi
 google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38=
 google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM=
 google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=
+google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY=
 google.golang.org/grpc v1.26.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
 google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
 google.golang.org/grpc v1.27.1/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
+google.golang.org/grpc v1.31.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak=
 google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
 google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
 google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=
diff --git a/pkg/config/client.go b/pkg/config/client.go
index b2efb79afb7..cfc262e9ed7 100644
--- a/pkg/config/client.go
+++ b/pkg/config/client.go
@@ -38,6 +38,10 @@ type ClientCommonConf struct {
 	// ServerPort specifies the port to connect to the server on. By default,
 	// this value is 7000.
 	ServerPort int `ini:"server_port" json:"server_port"`
+	// ConnectServerLocalIP specifies the address of the client bind when it connect to server.
+	// By default, this value is empty.
+	// this value only use in TCP/Websocket protocol. Not support in KCP protocol.
+	ConnectServerLocalIP string `ini:"connect_server_local_ip" json:"connect_server_local_ip"`
 	// HTTPProxy specifies a proxy address to connect to the server through. If
 	// this value is "", the server will be connected to directly. By default,
 	// this value is read from the "http_proxy" environment variable.
@@ -86,6 +90,9 @@ type ClientCommonConf struct {
 	// the server must have TCP multiplexing enabled as well. By default, this
 	// value is true.
 	TCPMux bool `ini:"tcp_mux" json:"tcp_mux"`
+	// TCPMuxKeepaliveInterval specifies the keep alive interval for TCP stream multipler.
+	// If TCPMux is true, heartbeat of application layer is unnecessary because it can only rely on heartbeat in TCPMux.
+	TCPMuxKeepaliveInterval int64 `ini:"tcp_mux_keepalive_interval" json:"tcp_mux_keepalive_interval"`
 	// User specifies a prefix for proxy names to distinguish them from other
 	// clients. If this value is not "", proxy names will automatically be
 	// changed to "{user}.{proxy_name}". By default, this value is "".
@@ -129,11 +136,11 @@ type ClientCommonConf struct {
 	DisableCustomTLSFirstByte bool `ini:"disable_custom_tls_first_byte" json:"disable_custom_tls_first_byte"`
 	// HeartBeatInterval specifies at what interval heartbeats are sent to the
 	// server, in seconds. It is not recommended to change this value. By
-	// default, this value is 30.
+	// default, this value is 30. Set negative value to disable it.
 	HeartbeatInterval int64 `ini:"heartbeat_interval" json:"heartbeat_interval"`
 	// HeartBeatTimeout specifies the maximum allowed heartbeat response delay
 	// before the connection is terminated, in seconds. It is not recommended
-	// to change this value. By default, this value is 90.
+	// to change this value. By default, this value is 90. Set negative value to disable it.
 	HeartbeatTimeout int64 `ini:"heartbeat_timeout" json:"heartbeat_timeout"`
 	// Client meta info
 	Metas map[string]string `ini:"-" json:"metas"`
@@ -147,36 +154,37 @@ type ClientCommonConf struct {
 // GetDefaultClientConf returns a client configuration with default values.
 func GetDefaultClientConf() ClientCommonConf {
 	return ClientCommonConf{
-		ClientConfig:       auth.GetDefaultClientConf(),
-		ServerAddr:         "0.0.0.0",
-		ServerPort:         7000,
-		HTTPProxy:          os.Getenv("http_proxy"),
-		LogFile:            "console",
-		LogWay:             "console",
-		LogLevel:           "info",
-		LogMaxDays:         3,
-		DisableLogColor:    false,
-		AdminAddr:          "127.0.0.1",
-		AdminPort:          0,
-		AdminUser:          "",
-		AdminPwd:           "",
-		AssetsDir:          "",
-		PoolCount:          1,
-		TCPMux:             true,
-		User:               "",
-		DNSServer:          "",
-		LoginFailExit:      true,
-		Start:              make([]string, 0),
-		Protocol:           "tcp",
-		TLSEnable:          false,
-		TLSCertFile:        "",
-		TLSKeyFile:         "",
-		TLSTrustedCaFile:   "",
-		HeartbeatInterval:  30,
-		HeartbeatTimeout:   90,
-		Metas:              make(map[string]string),
-		UDPPacketSize:      1500,
-		IncludeConfigFiles: make([]string, 0),
+		ClientConfig:            auth.GetDefaultClientConf(),
+		ServerAddr:              "0.0.0.0",
+		ServerPort:              7000,
+		HTTPProxy:               os.Getenv("http_proxy"),
+		LogFile:                 "console",
+		LogWay:                  "console",
+		LogLevel:                "info",
+		LogMaxDays:              3,
+		DisableLogColor:         false,
+		AdminAddr:               "127.0.0.1",
+		AdminPort:               0,
+		AdminUser:               "",
+		AdminPwd:                "",
+		AssetsDir:               "",
+		PoolCount:               1,
+		TCPMux:                  true,
+		TCPMuxKeepaliveInterval: 60,
+		User:                    "",
+		DNSServer:               "",
+		LoginFailExit:           true,
+		Start:                   make([]string, 0),
+		Protocol:                "tcp",
+		TLSEnable:               false,
+		TLSCertFile:             "",
+		TLSKeyFile:              "",
+		TLSTrustedCaFile:        "",
+		HeartbeatInterval:       30,
+		HeartbeatTimeout:        90,
+		Metas:                   make(map[string]string),
+		UDPPacketSize:           1500,
+		IncludeConfigFiles:      make([]string, 0),
 	}
 }
 
@@ -189,12 +197,10 @@ func (cfg *ClientCommonConf) Complete() {
 }
 
 func (cfg *ClientCommonConf) Validate() error {
-	if cfg.HeartbeatInterval <= 0 {
-		return fmt.Errorf("invalid heartbeat_interval")
-	}
-
-	if cfg.HeartbeatTimeout < cfg.HeartbeatInterval {
-		return fmt.Errorf("invalid heartbeat_timeout, heartbeat_timeout is less than heartbeat_interval")
+	if cfg.HeartbeatTimeout > 0 && cfg.HeartbeatInterval > 0 {
+		if cfg.HeartbeatTimeout < cfg.HeartbeatInterval {
+			return fmt.Errorf("invalid heartbeat_timeout, heartbeat_timeout is less than heartbeat_interval")
+		}
 	}
 
 	if cfg.TLSEnable == false {
diff --git a/pkg/config/server.go b/pkg/config/server.go
index 8e7f7ad2dfe..d002f9ff26e 100644
--- a/pkg/config/server.go
+++ b/pkg/config/server.go
@@ -118,6 +118,9 @@ type ServerCommonConf struct {
 	// from a client to share a single TCP connection. By default, this value
 	// is true.
 	TCPMux bool `ini:"tcp_mux" json:"tcp_mux"`
+	// TCPMuxKeepaliveInterval specifies the keep alive interval for TCP stream multipler.
+	// If TCPMux is true, heartbeat of application layer is unnecessary because it can only rely on heartbeat in TCPMux.
+	TCPMuxKeepaliveInterval int64 `ini:"tcp_mux_keepalive_interval" json:"tcp_mux_keepalive_interval"`
 	// Custom404Page specifies a path to a custom 404 page to display. If this
 	// value is "", a default page will be displayed. By default, this value is
 	// "".
@@ -154,7 +157,7 @@ type ServerCommonConf struct {
 	TLSTrustedCaFile string `ini:"tls_trusted_ca_file" json:"tls_trusted_ca_file"`
 	// HeartBeatTimeout specifies the maximum time to wait for a heartbeat
 	// before terminating the connection. It is not recommended to change this
-	// value. By default, this value is 90.
+	// value. By default, this value is 90. Set negative value to disable it.
 	HeartbeatTimeout int64 `ini:"heartbeat_timeout" json:"heartbeat_timeout"`
 	// UserConnTimeout specifies the maximum time to wait for a work
 	// connection. By default, this value is 10.
@@ -170,42 +173,43 @@ type ServerCommonConf struct {
 // defaults.
 func GetDefaultServerConf() ServerCommonConf {
 	return ServerCommonConf{
-		ServerConfig:           auth.GetDefaultServerConf(),
-		BindAddr:               "0.0.0.0",
-		BindPort:               7000,
-		BindUDPPort:            0,
-		KCPBindPort:            0,
-		ProxyBindAddr:          "",
-		VhostHTTPPort:          0,
-		VhostHTTPSPort:         0,
-		TCPMuxHTTPConnectPort:  0,
-		VhostHTTPTimeout:       60,
-		DashboardAddr:          "0.0.0.0",
-		DashboardPort:          0,
-		DashboardUser:          "",
-		DashboardPwd:           "",
-		EnablePrometheus:       false,
-		AssetsDir:              "",
-		LogFile:                "console",
-		LogWay:                 "console",
-		LogLevel:               "info",
-		LogMaxDays:             3,
-		DisableLogColor:        false,
-		DetailedErrorsToClient: true,
-		SubDomainHost:          "",
-		TCPMux:                 true,
-		AllowPorts:             make(map[int]struct{}),
-		MaxPoolCount:           5,
-		MaxPortsPerClient:      0,
-		TLSOnly:                false,
-		TLSCertFile:            "",
-		TLSKeyFile:             "",
-		TLSTrustedCaFile:       "",
-		HeartbeatTimeout:       90,
-		UserConnTimeout:        10,
-		Custom404Page:          "",
-		HTTPPlugins:            make(map[string]plugin.HTTPPluginOptions),
-		UDPPacketSize:          1500,
+		ServerConfig:            auth.GetDefaultServerConf(),
+		BindAddr:                "0.0.0.0",
+		BindPort:                7000,
+		BindUDPPort:             0,
+		KCPBindPort:             0,
+		ProxyBindAddr:           "",
+		VhostHTTPPort:           0,
+		VhostHTTPSPort:          0,
+		TCPMuxHTTPConnectPort:   0,
+		VhostHTTPTimeout:        60,
+		DashboardAddr:           "0.0.0.0",
+		DashboardPort:           0,
+		DashboardUser:           "",
+		DashboardPwd:            "",
+		EnablePrometheus:        false,
+		AssetsDir:               "",
+		LogFile:                 "console",
+		LogWay:                  "console",
+		LogLevel:                "info",
+		LogMaxDays:              3,
+		DisableLogColor:         false,
+		DetailedErrorsToClient:  true,
+		SubDomainHost:           "",
+		TCPMux:                  true,
+		TCPMuxKeepaliveInterval: 60,
+		AllowPorts:              make(map[int]struct{}),
+		MaxPoolCount:            5,
+		MaxPortsPerClient:       0,
+		TLSOnly:                 false,
+		TLSCertFile:             "",
+		TLSKeyFile:              "",
+		TLSTrustedCaFile:        "",
+		HeartbeatTimeout:        90,
+		UserConnTimeout:         10,
+		Custom404Page:           "",
+		HTTPPlugins:             make(map[string]plugin.HTTPPluginOptions),
+		UDPPacketSize:           1500,
 	}
 }
 
diff --git a/pkg/plugin/server/types.go b/pkg/plugin/server/types.go
index 82d4032cb2a..4df79f46d62 100644
--- a/pkg/plugin/server/types.go
+++ b/pkg/plugin/server/types.go
@@ -33,6 +33,8 @@ type Response struct {
 
 type LoginContent struct {
 	msg.Login
+
+	ClientAddress string `json:"client_address,omitempty"`
 }
 
 type UserInfo struct {
diff --git a/pkg/util/net/conn.go b/pkg/util/net/conn.go
index ccb199e5d7c..f3d8caee400 100644
--- a/pkg/util/net/conn.go
+++ b/pkg/util/net/conn.go
@@ -16,18 +16,13 @@ package net
 
 import (
 	"context"
-	"crypto/tls"
 	"errors"
-	"fmt"
 	"io"
 	"net"
 	"sync/atomic"
 	"time"
 
 	"github.com/fatedier/frp/pkg/util/xlog"
-
-	gnet "github.com/fatedier/golib/net"
-	kcp "github.com/fatedier/kcp-go"
 )
 
 type ContextGetter interface {
@@ -188,56 +183,3 @@ func (statsConn *StatsConn) Close() (err error) {
 	}
 	return
 }
-
-func ConnectServer(protocol string, addr string) (c net.Conn, err error) {
-	switch protocol {
-	case "tcp":
-		return net.Dial("tcp", addr)
-	case "kcp":
-		kcpConn, errRet := kcp.DialWithOptions(addr, nil, 10, 3)
-		if errRet != nil {
-			err = errRet
-			return
-		}
-		kcpConn.SetStreamMode(true)
-		kcpConn.SetWriteDelay(true)
-		kcpConn.SetNoDelay(1, 20, 2, 1)
-		kcpConn.SetWindowSize(128, 512)
-		kcpConn.SetMtu(1350)
-		kcpConn.SetACKNoDelay(false)
-		kcpConn.SetReadBuffer(4194304)
-		kcpConn.SetWriteBuffer(4194304)
-		c = kcpConn
-		return
-	default:
-		return nil, fmt.Errorf("unsupport protocol: %s", protocol)
-	}
-}
-
-func ConnectServerByProxy(proxyURL string, protocol string, addr string) (c net.Conn, err error) {
-	switch protocol {
-	case "tcp":
-		return gnet.DialTcpByProxy(proxyURL, addr)
-	case "kcp":
-		// http proxy is not supported for kcp
-		return ConnectServer(protocol, addr)
-	case "websocket":
-		return ConnectWebsocketServer(addr)
-	default:
-		return nil, fmt.Errorf("unsupport protocol: %s", protocol)
-	}
-}
-
-func ConnectServerByProxyWithTLS(proxyURL string, protocol string, addr string, tlsConfig *tls.Config, disableCustomTLSHeadByte bool) (c net.Conn, err error) {
-	c, err = ConnectServerByProxy(proxyURL, protocol, addr)
-	if err != nil {
-		return
-	}
-
-	if tlsConfig == nil {
-		return
-	}
-
-	c = WrapTLSClientConn(c, tlsConfig, disableCustomTLSHeadByte)
-	return
-}
diff --git a/pkg/util/net/dial.go b/pkg/util/net/dial.go
new file mode 100644
index 00000000000..251ebbff7ae
--- /dev/null
+++ b/pkg/util/net/dial.go
@@ -0,0 +1,44 @@
+package net
+
+import (
+	"context"
+	"net"
+	"net/url"
+
+	libdial "github.com/fatedier/golib/net/dial"
+	"golang.org/x/net/websocket"
+)
+
+func DialHookCustomTLSHeadByte(enableTLS bool, disableCustomTLSHeadByte bool) libdial.AfterHookFunc {
+	return func(ctx context.Context, c net.Conn, addr string) (context.Context, net.Conn, error) {
+		if enableTLS && !disableCustomTLSHeadByte {
+			_, err := c.Write([]byte{byte(FRPTLSHeadByte)})
+			if err != nil {
+				return nil, nil, err
+			}
+		}
+		return ctx, c, nil
+	}
+}
+
+func DialHookWebsocket() libdial.AfterHookFunc {
+	return func(ctx context.Context, c net.Conn, addr string) (context.Context, net.Conn, error) {
+		addr = "ws://" + addr + FrpWebsocketPath
+		uri, err := url.Parse(addr)
+		if err != nil {
+			return nil, nil, err
+		}
+
+		origin := "http://" + uri.Host
+		cfg, err := websocket.NewConfig(addr, origin)
+		if err != nil {
+			return nil, nil, err
+		}
+
+		conn, err := websocket.NewClient(cfg, c)
+		if err != nil {
+			return nil, nil, err
+		}
+		return ctx, conn, nil
+	}
+}
diff --git a/pkg/util/net/tls.go b/pkg/util/net/tls.go
index 80a98aaa0ec..1bae196adb8 100644
--- a/pkg/util/net/tls.go
+++ b/pkg/util/net/tls.go
@@ -27,14 +27,6 @@ var (
 	FRPTLSHeadByte = 0x17
 )
 
-func WrapTLSClientConn(c net.Conn, tlsConfig *tls.Config, disableCustomTLSHeadByte bool) (out net.Conn) {
-	if !disableCustomTLSHeadByte {
-		c.Write([]byte{byte(FRPTLSHeadByte)})
-	}
-	out = tls.Client(c, tlsConfig)
-	return
-}
-
 func CheckAndEnableTLSServerConnWithTimeout(
 	c net.Conn, tlsConfig *tls.Config, tlsOnly bool, timeout time.Duration,
 ) (out net.Conn, isTLS bool, custom bool, err error) {
diff --git a/pkg/util/net/websocket.go b/pkg/util/net/websocket.go
index 36b6440c5b5..7030787e700 100644
--- a/pkg/util/net/websocket.go
+++ b/pkg/util/net/websocket.go
@@ -5,8 +5,6 @@ import (
 	"fmt"
 	"net"
 	"net/http"
-	"net/url"
-	"time"
 
 	"golang.org/x/net/websocket"
 )
@@ -77,27 +75,3 @@ func (p *WebsocketListener) Close() error {
 func (p *WebsocketListener) Addr() net.Addr {
 	return p.ln.Addr()
 }
-
-// addr: domain:port
-func ConnectWebsocketServer(addr string) (net.Conn, error) {
-	addr = "ws://" + addr + FrpWebsocketPath
-	uri, err := url.Parse(addr)
-	if err != nil {
-		return nil, err
-	}
-
-	origin := "http://" + uri.Host
-	cfg, err := websocket.NewConfig(addr, origin)
-	if err != nil {
-		return nil, err
-	}
-	cfg.Dialer = &net.Dialer{
-		Timeout: 10 * time.Second,
-	}
-
-	conn, err := websocket.DialConfig(cfg)
-	if err != nil {
-		return nil, err
-	}
-	return conn, nil
-}
diff --git a/pkg/util/version/version.go b/pkg/util/version/version.go
index c3fe1e0a8ea..2ab9be96345 100644
--- a/pkg/util/version/version.go
+++ b/pkg/util/version/version.go
@@ -19,7 +19,7 @@ import (
 	"strings"
 )
 
-var version string = "0.38.0"
+var version string = "0.39.0"
 
 func Full() string {
 	return version
diff --git a/server/control.go b/server/control.go
index 7632bbed0dc..25adc2d2735 100644
--- a/server/control.go
+++ b/server/control.go
@@ -400,12 +400,19 @@ func (ctl *Control) manager() {
 	defer ctl.allShutdown.Start()
 	defer ctl.managerShutdown.Done()
 
-	heartbeat := time.NewTicker(time.Second)
-	defer heartbeat.Stop()
+	var heartbeatCh <-chan time.Time
+	if ctl.serverCfg.TCPMux || ctl.serverCfg.HeartbeatTimeout <= 0 {
+		// Don't need application heartbeat here.
+		// yamux will do same thing.
+	} else {
+		heartbeat := time.NewTicker(time.Second)
+		defer heartbeat.Stop()
+		heartbeatCh = heartbeat.C
+	}
 
 	for {
 		select {
-		case <-heartbeat.C:
+		case <-heartbeatCh:
 			if time.Since(ctl.lastPing) > time.Duration(ctl.serverCfg.HeartbeatTimeout)*time.Second {
 				xl.Warn("heartbeat timeout")
 				return
diff --git a/server/dashboard.go b/server/dashboard.go
index 7defec3cc88..0e06ed8255e 100644
--- a/server/dashboard.go
+++ b/server/dashboard.go
@@ -34,27 +34,29 @@ var (
 func (svr *Service) RunDashboardServer(address string) (err error) {
 	// url router
 	router := mux.NewRouter()
+	router.HandleFunc("/healthz", svr.Healthz)
+
+	subRouter := router.NewRoute().Subrouter()
 
 	user, passwd := svr.cfg.DashboardUser, svr.cfg.DashboardPwd
-	router.Use(frpNet.NewHTTPAuthMiddleware(user, passwd).Middleware)
+	subRouter.Use(frpNet.NewHTTPAuthMiddleware(user, passwd).Middleware)
 
 	// metrics
 	if svr.cfg.EnablePrometheus {
-		router.Handle("/metrics", promhttp.Handler())
+		subRouter.Handle("/metrics", promhttp.Handler())
 	}
 
 	// api, see dashboard_api.go
-	router.HandleFunc("/api/serverinfo", svr.APIServerInfo).Methods("GET")
-	router.HandleFunc("/api/proxy/{type}", svr.APIProxyByType).Methods("GET")
-	router.HandleFunc("/api/proxy/{type}/{name}", svr.APIProxyByTypeAndName).Methods("GET")
-	router.HandleFunc("/api/traffic/{name}", svr.APIProxyTraffic).Methods("GET")
-	router.HandleFunc("/healthz", svr.Healthz)
+	subRouter.HandleFunc("/api/serverinfo", svr.APIServerInfo).Methods("GET")
+	subRouter.HandleFunc("/api/proxy/{type}", svr.APIProxyByType).Methods("GET")
+	subRouter.HandleFunc("/api/proxy/{type}/{name}", svr.APIProxyByTypeAndName).Methods("GET")
+	subRouter.HandleFunc("/api/traffic/{name}", svr.APIProxyTraffic).Methods("GET")
 
 	// view
-	router.Handle("/favicon.ico", http.FileServer(assets.FileSystem)).Methods("GET")
-	router.PathPrefix("/static/").Handler(frpNet.MakeHTTPGzipHandler(http.StripPrefix("/static/", http.FileServer(assets.FileSystem)))).Methods("GET")
+	subRouter.Handle("/favicon.ico", http.FileServer(assets.FileSystem)).Methods("GET")
+	subRouter.PathPrefix("/static/").Handler(frpNet.MakeHTTPGzipHandler(http.StripPrefix("/static/", http.FileServer(assets.FileSystem)))).Methods("GET")
 
-	router.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
+	subRouter.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
 		http.Redirect(w, r, "/static/", http.StatusMovedPermanently)
 	})
 
diff --git a/server/service.go b/server/service.go
index 97a65f80e3b..bc0d48a6f04 100644
--- a/server/service.go
+++ b/server/service.go
@@ -334,7 +334,8 @@ func (svr *Service) handleConnection(ctx context.Context, conn net.Conn) {
 	case *msg.Login:
 		// server plugin hook
 		content := &plugin.LoginContent{
-			Login: *m,
+			Login:         *m,
+			ClientAddress: conn.RemoteAddr().String(),
 		}
 		retContent, err := svr.pluginManager.Login(content)
 		if err == nil {
@@ -405,7 +406,7 @@ func (svr *Service) HandleListener(l net.Listener) {
 		go func(ctx context.Context, frpConn net.Conn) {
 			if svr.cfg.TCPMux {
 				fmuxCfg := fmux.DefaultConfig()
-				fmuxCfg.KeepAliveInterval = 20 * time.Second
+				fmuxCfg.KeepAliveInterval = time.Duration(svr.cfg.TCPMuxKeepaliveInterval) * time.Second
 				fmuxCfg.LogOutput = io.Discard
 				session, err := fmux.Server(frpConn, fmuxCfg)
 				if err != nil {


Test patch:
diff --git a/pkg/config/client_test.go b/pkg/config/client_test.go
index e9e8e363c00..c78e3294045 100644
--- a/pkg/config/client_test.go
+++ b/pkg/config/client_test.go
@@ -259,33 +259,34 @@ func Test_LoadClientCommonConf(t *testing.T) {
 				OidcTokenEndpointURL: "endpoint_url",
 			},
 		},
-		ServerAddr:        "0.0.0.9",
-		ServerPort:        7009,
-		HTTPProxy:         "http://user:passwd@192.168.1.128:8080",
-		LogFile:           "./frpc.log9",
-		LogWay:            "file",
-		LogLevel:          "info9",
-		LogMaxDays:        39,
-		DisableLogColor:   false,
-		AdminAddr:         "127.0.0.9",
-		AdminPort:         7409,
-		AdminUser:         "admin9",
-		AdminPwd:          "admin9",
-		AssetsDir:         "./static9",
-		PoolCount:         59,
-		TCPMux:            true,
-		User:              "your_name",
-		LoginFailExit:     true,
-		Protocol:          "tcp",
-		TLSEnable:         true,
-		TLSCertFile:       "client.crt",
-		TLSKeyFile:        "client.key",
-		TLSTrustedCaFile:  "ca.crt",
-		TLSServerName:     "example.com",
-		DNSServer:         "8.8.8.9",
-		Start:             []string{"ssh", "dns"},
-		HeartbeatInterval: 39,
-		HeartbeatTimeout:  99,
+		ServerAddr:              "0.0.0.9",
+		ServerPort:              7009,
+		HTTPProxy:               "http://user:passwd@192.168.1.128:8080",
+		LogFile:                 "./frpc.log9",
+		LogWay:                  "file",
+		LogLevel:                "info9",
+		LogMaxDays:              39,
+		DisableLogColor:         false,
+		AdminAddr:               "127.0.0.9",
+		AdminPort:               7409,
+		AdminUser:               "admin9",
+		AdminPwd:                "admin9",
+		AssetsDir:               "./static9",
+		PoolCount:               59,
+		TCPMux:                  true,
+		TCPMuxKeepaliveInterval: 60,
+		User:                    "your_name",
+		LoginFailExit:           true,
+		Protocol:                "tcp",
+		TLSEnable:               true,
+		TLSCertFile:             "client.crt",
+		TLSKeyFile:              "client.key",
+		TLSTrustedCaFile:        "ca.crt",
+		TLSServerName:           "example.com",
+		DNSServer:               "8.8.8.9",
+		Start:                   []string{"ssh", "dns"},
+		HeartbeatInterval:       39,
+		HeartbeatTimeout:        99,
 		Metas: map[string]string{
 			"var1": "123",
 			"var2": "234",
diff --git a/pkg/config/server_test.go b/pkg/config/server_test.go
index 18f6d7ae04b..bdf20cb4974 100644
--- a/pkg/config/server_test.go
+++ b/pkg/config/server_test.go
@@ -131,15 +131,16 @@ func Test_LoadServerCommonConf(t *testing.T) {
 					12: struct{}{},
 					99: struct{}{},
 				},
-				MaxPoolCount:      59,
-				MaxPortsPerClient: 9,
-				TLSOnly:           true,
-				TLSCertFile:       "server.crt",
-				TLSKeyFile:        "server.key",
-				TLSTrustedCaFile:  "ca.crt",
-				SubDomainHost:     "frps.com",
-				TCPMux:            true,
-				UDPPacketSize:     1509,
+				MaxPoolCount:            59,
+				MaxPortsPerClient:       9,
+				TLSOnly:                 true,
+				TLSCertFile:             "server.crt",
+				TLSKeyFile:              "server.key",
+				TLSTrustedCaFile:        "ca.crt",
+				SubDomainHost:           "frps.com",
+				TCPMux:                  true,
+				TCPMuxKeepaliveInterval: 60,
+				UDPPacketSize:           1509,
 
 				HTTPPlugins: map[string]plugin.HTTPPluginOptions{
 					"user-manager": {
@@ -174,27 +175,28 @@ func Test_LoadServerCommonConf(t *testing.T) {
 						AuthenticateNewWorkConns: false,
 					},
 				},
-				BindAddr:               "0.0.0.9",
-				BindPort:               7009,
-				BindUDPPort:            7008,
-				ProxyBindAddr:          "0.0.0.9",
-				VhostHTTPTimeout:       60,
-				DashboardAddr:          "0.0.0.0",
-				DashboardUser:          "",
-				DashboardPwd:           "",
-				EnablePrometheus:       false,
-				LogFile:                "console",
-				LogWay:                 "console",
-				LogLevel:               "info",
-				LogMaxDays:             3,
-				DetailedErrorsToClient: true,
-				TCPMux:                 true,
-				AllowPorts:             make(map[int]struct{}),
-				MaxPoolCount:           5,
-				HeartbeatTimeout:       90,
-				UserConnTimeout:        10,
-				HTTPPlugins:            make(map[string]plugin.HTTPPluginOptions),
-				UDPPacketSize:          1500,
+				BindAddr:                "0.0.0.9",
+				BindPort:                7009,
+				BindUDPPort:             7008,
+				ProxyBindAddr:           "0.0.0.9",
+				VhostHTTPTimeout:        60,
+				DashboardAddr:           "0.0.0.0",
+				DashboardUser:           "",
+				DashboardPwd:            "",
+				EnablePrometheus:        false,
+				LogFile:                 "console",
+				LogWay:                  "console",
+				LogLevel:                "info",
+				LogMaxDays:              3,
+				DetailedErrorsToClient:  true,
+				TCPMux:                  true,
+				TCPMuxKeepaliveInterval: 60,
+				AllowPorts:              make(map[int]struct{}),
+				MaxPoolCount:            5,
+				HeartbeatTimeout:        90,
+				UserConnTimeout:         10,
+				HTTPPlugins:             make(map[string]plugin.HTTPPluginOptions),
+				UDPPacketSize:           1500,
 			},
 		},
 	}
diff --git a/test/e2e/basic/client.go b/test/e2e/basic/client.go
index 24363551fce..8d370928c51 100644
--- a/test/e2e/basic/client.go
+++ b/test/e2e/basic/client.go
@@ -8,6 +8,7 @@ import (
 
 	"github.com/fatedier/frp/test/e2e/framework"
 	"github.com/fatedier/frp/test/e2e/framework/consts"
+	"github.com/fatedier/frp/test/e2e/pkg/request"
 	clientsdk "github.com/fatedier/frp/test/e2e/pkg/sdk/client"
 
 	. "github.com/onsi/ginkgo"
@@ -75,4 +76,28 @@ var _ = Describe("[Feature: ClientManage]", func() {
 		framework.NewRequestExpect(f).Port(newP2Port).Explain("new p2 port").Ensure()
 		framework.NewRequestExpect(f).Port(p3Port).Explain("p3 port").ExpectError(true).Ensure()
 	})
+
+	It("healthz", func() {
+		serverConf := consts.DefaultServerConfig
+
+		dashboardPort := f.AllocPort()
+		clientConf := consts.DefaultClientConfig + fmt.Sprintf(`
+		admin_addr = 0.0.0.0
+		admin_port = %d
+		admin_user = admin
+		admin_pwd = admin
+		`, dashboardPort)
+
+		f.RunProcesses([]string{serverConf}, []string{clientConf})
+
+		framework.NewRequestExpect(f).RequestModify(func(r *request.Request) {
+			r.HTTP().HTTPPath("/healthz")
+		}).Port(dashboardPort).ExpectResp([]byte("")).Ensure()
+
+		framework.NewRequestExpect(f).RequestModify(func(r *request.Request) {
+			r.HTTP().HTTPPath("/")
+		}).Port(dashboardPort).
+			Ensure(framework.ExpectResponseCode(401))
+	})
+
 })
diff --git a/test/e2e/basic/server.go b/test/e2e/basic/server.go
index 25fec2a01f7..74b421be5b1 100644
--- a/test/e2e/basic/server.go
+++ b/test/e2e/basic/server.go
@@ -144,4 +144,36 @@ var _ = Describe("[Feature: Server Manager]", func() {
 			r.HTTP().HTTPHost("example.com")
 		}).PortName(consts.PortServerName).Ensure()
 	})
+
+	It("healthz", func() {
+		serverConf := consts.DefaultServerConfig
+		dashboardPort := f.AllocPort()
+
+		// Use same port as PortServer
+		serverConf += fmt.Sprintf(`
+		vhost_http_port = {{ .%s }}
+		dashboard_addr = 0.0.0.0
+		dashboard_port = %d
+		dashboard_user = admin
+		dashboard_pwd = admin
+		`, consts.PortServerName, dashboardPort)
+
+		clientConf := consts.DefaultClientConfig + fmt.Sprintf(`
+		[http]
+		type = http
+		local_port = {{ .%s }}
+		custom_domains = example.com
+		`, framework.HTTPSimpleServerPort)
+
+		f.RunProcesses([]string{serverConf}, []string{clientConf})
+
+		framework.NewRequestExpect(f).RequestModify(func(r *request.Request) {
+			r.HTTP().HTTPPath("/healthz")
+		}).Port(dashboardPort).ExpectResp([]byte("")).Ensure()
+
+		framework.NewRequestExpect(f).RequestModify(func(r *request.Request) {
+			r.HTTP().HTTPPath("/")
+		}).Port(dashboardPort).
+			Ensure(framework.ExpectResponseCode(401))
+	})
 })
diff --git a/test/e2e/features/heartbeat.go b/test/e2e/features/heartbeat.go
new file mode 100644
index 00000000000..b0732c37f00
--- /dev/null
+++ b/test/e2e/features/heartbeat.go
@@ -0,0 +1,48 @@
+package features
+
+import (
+	"fmt"
+	"time"
+
+	"github.com/fatedier/frp/test/e2e/framework"
+
+	. "github.com/onsi/ginkgo"
+)
+
+var _ = Describe("[Feature: Heartbeat]", func() {
+	f := framework.NewDefaultFramework()
+
+	It("disable application layer heartbeat", func() {
+		serverPort := f.AllocPort()
+		serverConf := fmt.Sprintf(`
+		[common]
+		bind_addr = 0.0.0.0
+		bind_port = %d
+		heartbeat_timeout = -1
+		tcp_mux_keepalive_interval = 2
+		`, serverPort)
+
+		remotePort := f.AllocPort()
+		clientConf := fmt.Sprintf(`
+		[common]
+		server_port = %d
+		log_level = trace
+		heartbeat_interval = -1
+		heartbeat_timeout = -1
+		tcp_mux_keepalive_interval = 2
+
+		[tcp]
+		type = tcp
+		local_port = %d
+		remote_port = %d
+		`, serverPort, f.PortByName(framework.TCPEchoServerPort), remotePort)
+
+		// run frps and frpc
+		f.RunProcesses([]string{serverConf}, []string{clientConf})
+
+		framework.NewRequestExpect(f).Protocol("tcp").Port(remotePort).Ensure()
+
+		time.Sleep(5 * time.Second)
+		framework.NewRequestExpect(f).Protocol("tcp").Port(remotePort).Ensure()
+	})
+})
diff --git a/test/e2e/framework/request.go b/test/e2e/framework/request.go
index e20e400109b..5dccd661e4c 100644
--- a/test/e2e/framework/request.go
+++ b/test/e2e/framework/request.go
@@ -113,7 +113,7 @@ func (e *RequestExpect) Ensure(fns ...EnsureFunc) {
 		if !bytes.Equal(e.expectResp, ret.Content) {
 			flog.Trace("Response info: %+v", ret)
 		}
-		ExpectEqualValuesWithOffset(1, e.expectResp, ret.Content, e.explain...)
+		ExpectEqualValuesWithOffset(1, ret.Content, e.expectResp, e.explain...)
 	} else {
 		for _, fn := range fns {
 			ok := fn(ret)
diff --git a/test/e2e/pkg/request/request.go b/test/e2e/pkg/request/request.go
index 5792d530f05..6d264783e94 100644
--- a/test/e2e/pkg/request/request.go
+++ b/test/e2e/pkg/request/request.go
@@ -13,7 +13,7 @@ import (
 	"time"
 
 	"github.com/fatedier/frp/test/e2e/pkg/rpc"
-	libnet "github.com/fatedier/golib/net"
+	libdial "github.com/fatedier/golib/net/dial"
 )
 
 type Request struct {
@@ -141,7 +141,11 @@ func (r *Request) Do() (*Response, error) {
 		if r.protocol != "tcp" {
 			return nil, fmt.Errorf("only tcp protocol is allowed for proxy")
 		}
-		conn, err = libnet.DialTcpByProxy(r.proxyURL, addr)
+		proxyType, proxyAddress, auth, err := libdial.ParseProxyURL(r.proxyURL)
+		if err != nil {
+			return nil, fmt.Errorf("parse ProxyURL error: %v", err)
+		}
+		conn, err = libdial.Dial(addr, libdial.WithProxy(proxyType, proxyAddress), libdial.WithProxyAuth(auth))
 		if err != nil {
 			return nil, err
 		}
diff --git a/test/e2e/plugin/server.go b/test/e2e/plugin/server.go
index 0cd618f93cd..79ecff44b04 100644
--- a/test/e2e/plugin/server.go
+++ b/test/e2e/plugin/server.go
@@ -24,9 +24,14 @@ var _ = Describe("[Feature: Server-Plugins]", func() {
 
 		It("Auth for custom meta token", func() {
 			localPort := f.AllocPort()
+
+			clientAddressGot := false
 			handler := func(req *plugin.Request) *plugin.Response {
 				var ret plugin.Response
 				content := req.Content.(*plugin.LoginContent)
+				if content.ClientAddress != "" {
+					clientAddressGot = true
+				}
 				if content.Metas["token"] == "123" {
 					ret.Unchange = true
 				} else {
@@ -69,6 +74,8 @@ var _ = Describe("[Feature: Server-Plugins]", func() {
 
 			framework.NewRequestExpect(f).Port(remotePort).Ensure()
 			framework.NewRequestExpect(f).Port(remotePort2).ExpectError(true).Ensure()
+
+			framework.ExpectTrue(clientAddressGot)
 		})
 	})