1
0
Эх сурвалжийг харах

Merge pull request #2425 from fatedier/dev

bump version
fatedier 4 жил өмнө
parent
commit
57577ea044
64 өөрчлөгдсөн 1223 нэмэгдсэн , 494 устгасан
  1. 29 5
      README.md
  2. 1 1
      assets/frpc/static/index.html
  3. 1 1
      assets/frpc/static/manifest.js
  4. 0 0
      assets/frpc/static/vendor.js
  5. 0 0
      assets/frpc/statik/statik.go
  6. 1 1
      assets/frps/static/index.html
  7. 1 1
      assets/frps/static/manifest.js
  8. 0 0
      assets/frps/static/vendor.js
  9. 0 0
      assets/frps/statik/statik.go
  10. 4 22
      client/admin_api.go
  11. 1 1
      client/proxy/proxy_wrapper.go
  12. 3 2
      client/visitor.go
  13. 1 1
      cmd/frpc/sub/http.go
  14. 1 1
      cmd/frpc/sub/https.go
  15. 2 8
      cmd/frpc/sub/reload.go
  16. 8 34
      cmd/frpc/sub/root.go
  17. 2 9
      cmd/frpc/sub/status.go
  18. 1 1
      cmd/frpc/sub/stcp.go
  19. 1 1
      cmd/frpc/sub/sudp.go
  20. 1 1
      cmd/frpc/sub/tcp.go
  21. 1 1
      cmd/frpc/sub/tcpmux.go
  22. 1 1
      cmd/frpc/sub/udp.go
  23. 43 0
      cmd/frpc/sub/verify.go
  24. 1 1
      cmd/frpc/sub/xtcp.go
  25. 53 0
      cmd/frps/verify.go
  26. 3 0
      conf/frpc_full.ini
  27. 1 1
      go.mod
  28. 2 2
      go.sum
  29. 47 35
      pkg/config/client.go
  30. 3 3
      pkg/config/client_test.go
  31. 100 0
      pkg/config/parse.go
  32. 1 2
      pkg/config/proxy.go
  33. 5 6
      pkg/config/server.go
  34. 2 2
      pkg/config/server_test.go
  35. 1 1
      pkg/util/version/version.go
  36. 6 8
      server/control.go
  37. 1 1
      server/proxy/udp.go
  38. 101 23
      test/e2e/basic/basic.go
  39. 9 13
      test/e2e/basic/client_server.go
  40. 79 0
      test/e2e/basic/server.go
  41. 0 1
      test/e2e/e2e.go
  42. 4 5
      test/e2e/examples.go
  43. 23 6
      test/e2e/framework/consts/consts.go
  44. 47 7
      test/e2e/framework/framework.go
  45. 11 25
      test/e2e/framework/mockservers.go
  46. 5 2
      test/e2e/framework/process.go
  47. 69 30
      test/e2e/framework/request.go
  48. 0 4
      test/e2e/framework/util.go
  49. 0 111
      test/e2e/mock/echoserver/echoserver.go
  50. 142 0
      test/e2e/mock/server/server.go
  51. 58 2
      test/e2e/pkg/port/port.go
  52. 69 0
      test/e2e/pkg/port/util.go
  53. 107 8
      test/e2e/pkg/request/request.go
  54. 10 10
      test/e2e/plugin/client_plugins.go
  55. 0 2
      test/e2e/suites.go
  56. 0 7
      tests/ci/auto_test_frpc.ini
  57. 0 46
      tests/ci/normal_test.go
  58. 3 0
      web/frpc/src/components/Overview.vue
  59. 1 0
      web/frps/src/App.vue
  60. 3 0
      web/frps/src/components/Overview.vue
  61. 116 0
      web/frps/src/components/ProxiesSudp.vue
  62. 5 0
      web/frps/src/router/index.js
  63. 24 37
      web/frps/src/utils/chart.js
  64. 8 1
      web/frps/src/utils/proxy.js

+ 29 - 5
README.md

@@ -24,12 +24,13 @@ frp also has a P2P connect mode.
     * [Forward DNS query request](#forward-dns-query-request)
     * [Forward Unix domain socket](#forward-unix-domain-socket)
     * [Expose a simple HTTP file server](#expose-a-simple-http-file-server)
-    * [Enable HTTPS for local HTTP service](#enable-https-for-local-http-service)
+    * [Enable HTTPS for local HTTP(S) service](#enable-https-for-local-https-service)
     * [Expose your service privately](#expose-your-service-privately)
     * [P2P Mode](#p2p-mode)
 * [Features](#features)
     * [Configuration Files](#configuration-files)
     * [Using Environment Variables](#using-environment-variables)
+    * [Split Configures Into Different Files](#split-configures-into-different-files)
     * [Dashboard](#dashboard)
     * [Admin UI](#admin-ui)
     * [Monitor](#monitor)
@@ -412,6 +413,27 @@ export FRP_SSH_REMOTE_PORT="6000"
 
 `frpc` will render configuration file template using OS environment variables. Remember to prefix your reference with `.Envs`.
 
+### Split Configures Into Different Files
+
+You can split multiple proxy configs into different files and include them in the main file.
+
+```ini
+# frpc.ini
+[common]
+server_addr = x.x.x.x
+server_port = 7000
+includes=./confd/*.ini
+```
+
+```ini
+# ./confd/test.ini
+[ssh]
+type = tcp
+local_ip = 127.0.0.1
+local_port = 22
+remote_port = 6000
+```
+
 ### Dashboard
 
 Check frp's status and proxies' statistics information by Dashboard.
@@ -421,12 +443,12 @@ Configure a port for dashboard to enable this feature:
 ```ini
 [common]
 dashboard_port = 7500
-# dashboard's username and password are both optional,if not set, default is admin.
+# dashboard's username and password are both optional
 dashboard_user = admin
 dashboard_pwd = admin
 ```
 
-Then visit `http://[server_addr]:7500` to see the dashboard, with username and password both being `admin` by default.
+Then visit `http://[server_addr]:7500` to see the dashboard, with username and password both being `admin`.
 
 ![dashboard](/doc/pic/dashboard.png)
 
@@ -444,7 +466,7 @@ admin_user = admin
 admin_pwd = admin
 ```
 
-Then visit `http://127.0.0.1:7400` to see admin UI, with username and password both being `admin` by default.
+Then visit `http://127.0.0.1:7400` to see admin UI, with username and password both being `admin`.
 
 ### Monitor
 
@@ -624,10 +646,12 @@ admin_addr = 127.0.0.1
 admin_port = 7400
 ```
 
-Then run command `frpc reload -c ./frpc.ini` and wait for about 10 seconds to let `frpc` create or update or delete proxies.
+Then run command `frpc reload -c ./frpc.ini` and wait for about 10 seconds to let `frpc` create or update or remove proxies.
 
 **Note that parameters in [common] section won't be modified except 'start'.**
 
+You can run command `frpc verify -c ./frpc.ini` before reloading to check if there are config errors.
+
 ### Get proxy status from client
 
 Use `frpc status -c ./frpc.ini` to get status of all proxies. The `admin_addr` and `admin_port` fields are required for enabling HTTP API.

+ 1 - 1
assets/frpc/static/index.html

@@ -1 +1 @@
-<!doctype html> <html lang=en> <head> <meta charset=utf-8> <title>frp client admin UI</title> <link rel="shortcut icon" href="favicon.ico"></head> <body> <div id=app></div> <script type="text/javascript" src="manifest.js?f30e0e5ff7dbde4611e0"></script><script type="text/javascript" src="vendor.js?a82aed5fb0b844cbdb29"></script></body> </html> 
+<!doctype html> <html lang=en> <head> <meta charset=utf-8> <title>frp client admin UI</title> <link rel="shortcut icon" href="favicon.ico"></head> <body> <div id=app></div> <script type="text/javascript" src="manifest.js?5a0eb52788515d02ca46"></script><script type="text/javascript" src="vendor.js?3e5221f064f1295497bf"></script></body> </html> 

+ 1 - 1
assets/frpc/static/manifest.js

@@ -1 +1 @@
-!function(e){function n(r){if(t[r])return t[r].exports;var o=t[r]={i:r,l:!1,exports:{}};return e[r].call(o.exports,o,o.exports,n),o.l=!0,o.exports}var r=window.webpackJsonp;window.webpackJsonp=function(t,c,u){for(var i,a,f,l=0,s=[];l<t.length;l++)a=t[l],o[a]&&s.push(o[a][0]),o[a]=0;for(i in c)Object.prototype.hasOwnProperty.call(c,i)&&(e[i]=c[i]);for(r&&r(t,c,u);s.length;)s.shift()();if(u)for(l=0;l<u.length;l++)f=n(n.s=u[l]);return f};var t={},o={1:0};n.e=function(e){function r(){i.onerror=i.onload=null,clearTimeout(a);var n=o[e];0!==n&&(n&&n[1](new Error("Loading chunk "+e+" failed.")),o[e]=void 0)}var t=o[e];if(0===t)return new Promise(function(e){e()});if(t)return t[2];var c=new Promise(function(n,r){t=o[e]=[n,r]});t[2]=c;var u=document.getElementsByTagName("head")[0],i=document.createElement("script");i.type="text/javascript",i.charset="utf-8",i.async=!0,i.timeout=12e4,n.nc&&i.setAttribute("nonce",n.nc),i.src=n.p+""+e+".js?"+{0:"a82aed5fb0b844cbdb29"}[e];var a=setTimeout(r,12e4);return i.onerror=i.onload=r,u.appendChild(i),c},n.m=e,n.c=t,n.i=function(e){return e},n.d=function(e,r,t){n.o(e,r)||Object.defineProperty(e,r,{configurable:!1,enumerable:!0,get:t})},n.n=function(e){var r=e&&e.__esModule?function(){return e.default}:function(){return e};return n.d(r,"a",r),r},n.o=function(e,n){return Object.prototype.hasOwnProperty.call(e,n)},n.p="",n.oe=function(e){throw console.error(e),e}}([]);
+!function(e){function n(r){if(t[r])return t[r].exports;var o=t[r]={i:r,l:!1,exports:{}};return e[r].call(o.exports,o,o.exports,n),o.l=!0,o.exports}var r=window.webpackJsonp;window.webpackJsonp=function(t,u,c){for(var i,a,f,l=0,s=[];l<t.length;l++)a=t[l],o[a]&&s.push(o[a][0]),o[a]=0;for(i in u)Object.prototype.hasOwnProperty.call(u,i)&&(e[i]=u[i]);for(r&&r(t,u,c);s.length;)s.shift()();if(c)for(l=0;l<c.length;l++)f=n(n.s=c[l]);return f};var t={},o={1:0};n.e=function(e){function r(){i.onerror=i.onload=null,clearTimeout(a);var n=o[e];0!==n&&(n&&n[1](new Error("Loading chunk "+e+" failed.")),o[e]=void 0)}var t=o[e];if(0===t)return new Promise(function(e){e()});if(t)return t[2];var u=new Promise(function(n,r){t=o[e]=[n,r]});t[2]=u;var c=document.getElementsByTagName("head")[0],i=document.createElement("script");i.type="text/javascript",i.charset="utf-8",i.async=!0,i.timeout=12e4,n.nc&&i.setAttribute("nonce",n.nc),i.src=n.p+""+e+".js?"+{0:"3e5221f064f1295497bf"}[e];var a=setTimeout(r,12e4);return i.onerror=i.onload=r,c.appendChild(i),u},n.m=e,n.c=t,n.i=function(e){return e},n.d=function(e,r,t){n.o(e,r)||Object.defineProperty(e,r,{configurable:!1,enumerable:!0,get:t})},n.n=function(e){var r=e&&e.__esModule?function(){return e.default}:function(){return e};return n.d(r,"a",r),r},n.o=function(e,n){return Object.prototype.hasOwnProperty.call(e,n)},n.p="",n.oe=function(e){throw console.error(e),e}}([]);

Файлын зөрүү хэтэрхий том тул дарагдсан байна
+ 0 - 0
assets/frpc/static/vendor.js


Файлын зөрүү хэтэрхий том тул дарагдсан байна
+ 0 - 0
assets/frpc/statik/statik.go


+ 1 - 1
assets/frps/static/index.html

@@ -1 +1 @@
-<!doctype html> <html lang=en> <head> <meta charset=utf-8> <title>frps dashboard</title> <link rel="shortcut icon" href="favicon.ico"></head> <body> <div id=app></div> <script type="text/javascript" src="manifest.js?782d7b1b910e824ac986"></script><script type="text/javascript" src="vendor.js?7f899297af075fb3b085"></script></body> </html> 
+<!doctype html> <html lang=en> <head> <meta charset=utf-8> <title>frps dashboard</title> <link rel="shortcut icon" href="favicon.ico"></head> <body> <div id=app></div> <script type="text/javascript" src="manifest.js?41dbccdbc87e6bcbd79c"></script><script type="text/javascript" src="vendor.js?d7109b07f8f86bab2eeb"></script></body> </html> 

+ 1 - 1
assets/frps/static/manifest.js

@@ -1 +1 @@
-!function(e){function n(r){if(t[r])return t[r].exports;var o=t[r]={i:r,l:!1,exports:{}};return e[r].call(o.exports,o,o.exports,n),o.l=!0,o.exports}var r=window.webpackJsonp;window.webpackJsonp=function(t,u,c){for(var i,a,f,l=0,s=[];l<t.length;l++)a=t[l],o[a]&&s.push(o[a][0]),o[a]=0;for(i in u)Object.prototype.hasOwnProperty.call(u,i)&&(e[i]=u[i]);for(r&&r(t,u,c);s.length;)s.shift()();if(c)for(l=0;l<c.length;l++)f=n(n.s=c[l]);return f};var t={},o={1:0};n.e=function(e){function r(){i.onerror=i.onload=null,clearTimeout(a);var n=o[e];0!==n&&(n&&n[1](new Error("Loading chunk "+e+" failed.")),o[e]=void 0)}var t=o[e];if(0===t)return new Promise(function(e){e()});if(t)return t[2];var u=new Promise(function(n,r){t=o[e]=[n,r]});t[2]=u;var c=document.getElementsByTagName("head")[0],i=document.createElement("script");i.type="text/javascript",i.charset="utf-8",i.async=!0,i.timeout=12e4,n.nc&&i.setAttribute("nonce",n.nc),i.src=n.p+""+e+".js?"+{0:"7f899297af075fb3b085"}[e];var a=setTimeout(r,12e4);return i.onerror=i.onload=r,c.appendChild(i),u},n.m=e,n.c=t,n.i=function(e){return e},n.d=function(e,r,t){n.o(e,r)||Object.defineProperty(e,r,{configurable:!1,enumerable:!0,get:t})},n.n=function(e){var r=e&&e.__esModule?function(){return e.default}:function(){return e};return n.d(r,"a",r),r},n.o=function(e,n){return Object.prototype.hasOwnProperty.call(e,n)},n.p="",n.oe=function(e){throw console.error(e),e}}([]);
+!function(e){function n(r){if(t[r])return t[r].exports;var o=t[r]={i:r,l:!1,exports:{}};return e[r].call(o.exports,o,o.exports,n),o.l=!0,o.exports}var r=window.webpackJsonp;window.webpackJsonp=function(t,u,c){for(var i,a,f,l=0,s=[];l<t.length;l++)a=t[l],o[a]&&s.push(o[a][0]),o[a]=0;for(i in u)Object.prototype.hasOwnProperty.call(u,i)&&(e[i]=u[i]);for(r&&r(t,u,c);s.length;)s.shift()();if(c)for(l=0;l<c.length;l++)f=n(n.s=c[l]);return f};var t={},o={1:0};n.e=function(e){function r(){i.onerror=i.onload=null,clearTimeout(a);var n=o[e];0!==n&&(n&&n[1](new Error("Loading chunk "+e+" failed.")),o[e]=void 0)}var t=o[e];if(0===t)return new Promise(function(e){e()});if(t)return t[2];var u=new Promise(function(n,r){t=o[e]=[n,r]});t[2]=u;var c=document.getElementsByTagName("head")[0],i=document.createElement("script");i.type="text/javascript",i.charset="utf-8",i.async=!0,i.timeout=12e4,n.nc&&i.setAttribute("nonce",n.nc),i.src=n.p+""+e+".js?"+{0:"d7109b07f8f86bab2eeb"}[e];var a=setTimeout(r,12e4);return i.onerror=i.onload=r,c.appendChild(i),u},n.m=e,n.c=t,n.i=function(e){return e},n.d=function(e,r,t){n.o(e,r)||Object.defineProperty(e,r,{configurable:!1,enumerable:!0,get:t})},n.n=function(e){var r=e&&e.__esModule?function(){return e.default}:function(){return e};return n.d(r,"a",r),r},n.o=function(e,n){return Object.prototype.hasOwnProperty.call(e,n)},n.p="",n.oe=function(e){throw console.error(e),e}}([]);

Файлын зөрүү хэтэрхий том тул дарагдсан байна
+ 0 - 0
assets/frps/static/vendor.js


Файлын зөрүү хэтэрхий том тул дарагдсан байна
+ 0 - 0
assets/frps/statik/statik.go


+ 4 - 22
client/admin_api.go

@@ -33,36 +33,19 @@ type GeneralResponse struct {
 }
 
 // GET api/reload
-
 func (svr *Service) apiReload(w http.ResponseWriter, r *http.Request) {
 	res := GeneralResponse{Code: 200}
 
-	log.Info("Http request [/api/reload]")
+	log.Info("api request [/api/reload]")
 	defer func() {
-		log.Info("Http response [/api/reload], code [%d]", res.Code)
+		log.Info("api response [/api/reload], code [%d]", res.Code)
 		w.WriteHeader(res.Code)
 		if len(res.Msg) > 0 {
 			w.Write([]byte(res.Msg))
 		}
 	}()
 
-	content, err := config.GetRenderedConfFromFile(svr.cfgFile)
-	if err != nil {
-		res.Code = 400
-		res.Msg = err.Error()
-		log.Warn("reload frpc config file error: %s", res.Msg)
-		return
-	}
-
-	newCommonCfg, err := config.UnmarshalClientConfFromIni(content)
-	if err != nil {
-		res.Code = 400
-		res.Msg = err.Error()
-		log.Warn("reload frpc common section error: %s", res.Msg)
-		return
-	}
-
-	pxyCfgs, visitorCfgs, err := config.LoadAllProxyConfsFromIni(svr.cfg.User, content, newCommonCfg.Start)
+	_, pxyCfgs, visitorCfgs, err := config.ParseClientConfig(svr.cfgFile)
 	if err != nil {
 		res.Code = 400
 		res.Msg = err.Error()
@@ -70,8 +53,7 @@ func (svr *Service) apiReload(w http.ResponseWriter, r *http.Request) {
 		return
 	}
 
-	err = svr.ReloadConf(pxyCfgs, visitorCfgs)
-	if err != nil {
+	if err = svr.ReloadConf(pxyCfgs, visitorCfgs); err != nil {
 		res.Code = 500
 		res.Msg = err.Error()
 		log.Warn("reload frpc proxy config error: %s", res.Msg)

+ 1 - 1
client/proxy/proxy_wrapper.go

@@ -227,7 +227,7 @@ func (pw *Wrapper) InWorkConn(workConn net.Conn, m *msg.StartWorkConn) {
 	pw.mu.RLock()
 	pxy := pw.pxy
 	pw.mu.RUnlock()
-	if pxy != nil {
+	if pxy != nil && pw.Phase == ProxyPhaseRunning {
 		xl.Debug("start a new work connection, localAddr: %s remoteAddr: %s", workConn.LocalAddr().String(), workConn.RemoteAddr().String())
 		go pxy.InWorkConn(workConn, m)
 	} else {

+ 3 - 2
client/visitor.go

@@ -366,7 +366,7 @@ func (sv *SUDPVisitor) Run() (err error) {
 	sv.sendCh = make(chan *msg.UDPPacket, 1024)
 	sv.readCh = make(chan *msg.UDPPacket, 1024)
 
-	xl.Info("sudp start to work")
+	xl.Info("sudp start to work, listen on %s", addr)
 
 	go sv.dispatcher()
 	go udp.ForwardUserConn(sv.udpConn, sv.readCh, sv.sendCh, int(sv.ctl.clientCfg.UDPPacketSize))
@@ -446,7 +446,7 @@ func (sv *SUDPVisitor) worker(workConn net.Conn) {
 			case *msg.UDPPacket:
 				if errRet := errors.PanicToError(func() {
 					sv.readCh <- m
-					xl.Trace("frpc visitor get udp packet from frpc")
+					xl.Trace("frpc visitor get udp packet from workConn: %s", m.Content)
 				}); errRet != nil {
 					xl.Info("reader goroutine for udp work connection closed")
 					return
@@ -475,6 +475,7 @@ func (sv *SUDPVisitor) worker(workConn net.Conn) {
 					xl.Warn("sender goroutine for udp work connection closed: %v", errRet)
 					return
 				}
+				xl.Trace("send udp package to workConn: %s", udpMsg.Content)
 			case <-closeCh:
 				return
 			}

+ 1 - 1
cmd/frpc/sub/http.go

@@ -47,7 +47,7 @@ var httpCmd = &cobra.Command{
 	Use:   "http",
 	Short: "Run frpc with a single http proxy",
 	RunE: func(cmd *cobra.Command, args []string) error {
-		clientCfg, err := parseClientCommonCfg(CfgFileTypeCmd, nil)
+		clientCfg, err := parseClientCommonCfgFromCmd()
 		if err != nil {
 			fmt.Println(err)
 			os.Exit(1)

+ 1 - 1
cmd/frpc/sub/https.go

@@ -43,7 +43,7 @@ var httpsCmd = &cobra.Command{
 	Use:   "https",
 	Short: "Run frpc with a single https proxy",
 	RunE: func(cmd *cobra.Command, args []string) error {
-		clientCfg, err := parseClientCommonCfg(CfgFileTypeCmd, nil)
+		clientCfg, err := parseClientCommonCfgFromCmd()
 		if err != nil {
 			fmt.Println(err)
 			os.Exit(1)

+ 2 - 8
cmd/frpc/sub/reload.go

@@ -35,19 +35,13 @@ var reloadCmd = &cobra.Command{
 	Use:   "reload",
 	Short: "Hot-Reload frpc configuration",
 	RunE: func(cmd *cobra.Command, args []string) error {
-		iniContent, err := config.GetRenderedConfFromFile(cfgFile)
+		cfg, _, _, err := config.ParseClientConfig(cfgFile)
 		if err != nil {
 			fmt.Println(err)
 			os.Exit(1)
 		}
 
-		clientCfg, err := parseClientCommonCfg(CfgFileTypeIni, iniContent)
-		if err != nil {
-			fmt.Println(err)
-			os.Exit(1)
-		}
-
-		err = reload(clientCfg)
+		err = reload(cfg)
 		if err != nil {
 			fmt.Printf("frpc reload error: %v\n", err)
 			os.Exit(1)

+ 8 - 34
cmd/frpc/sub/root.go

@@ -129,25 +129,6 @@ func handleSignal(svr *client.Service) {
 	close(kcpDoneCh)
 }
 
-func parseClientCommonCfg(fileType int, source []byte) (cfg config.ClientCommonConf, err error) {
-	if fileType == CfgFileTypeIni {
-		cfg, err = config.UnmarshalClientConfFromIni(source)
-	} else if fileType == CfgFileTypeCmd {
-		cfg, err = parseClientCommonCfgFromCmd()
-	}
-	if err != nil {
-		return
-	}
-
-	cfg.Complete()
-	err = cfg.Validate()
-	if err != nil {
-		err = fmt.Errorf("Parse config error: %v", err)
-		return
-	}
-	return
-}
-
 func parseClientCommonCfgFromCmd() (cfg config.ClientCommonConf, err error) {
 	cfg = config.GetDefaultClientConf()
 
@@ -176,26 +157,19 @@ func parseClientCommonCfgFromCmd() (cfg config.ClientCommonConf, err error) {
 	cfg.Token = token
 	cfg.TLSEnable = tlsEnable
 
+	cfg.Complete()
+	if err = cfg.Validate(); err != nil {
+		err = fmt.Errorf("Parse config error: %v", err)
+		return
+	}
 	return
 }
 
-func runClient(cfgFilePath string) (err error) {
-	var content []byte
-	content, err = config.GetRenderedConfFromFile(cfgFilePath)
-	if err != nil {
-		return err
-	}
-
-	cfg, err := parseClientCommonCfg(CfgFileTypeIni, content)
+func runClient(cfgFilePath string) error {
+	cfg, pxyCfgs, visitorCfgs, err := config.ParseClientConfig(cfgFilePath)
 	if err != nil {
 		return err
 	}
-
-	pxyCfgs, visitorCfgs, err := config.LoadAllProxyConfsFromIni(cfg.User, content, cfg.Start)
-	if err != nil {
-		return err
-	}
-
 	return startService(cfg, pxyCfgs, visitorCfgs, cfgFilePath)
 }
 
@@ -234,7 +208,7 @@ func startService(
 	}
 
 	err = svr.Run()
-	if cfg.Protocol == "kcp" {
+	if err == nil && cfg.Protocol == "kcp" {
 		<-kcpDoneCh
 	}
 	return

+ 2 - 9
cmd/frpc/sub/status.go

@@ -38,20 +38,13 @@ var statusCmd = &cobra.Command{
 	Use:   "status",
 	Short: "Overview of all proxies status",
 	RunE: func(cmd *cobra.Command, args []string) error {
-		iniContent, err := config.GetRenderedConfFromFile(cfgFile)
+		cfg, _, _, err := config.ParseClientConfig(cfgFile)
 		if err != nil {
 			fmt.Println(err)
 			os.Exit(1)
 		}
 
-		clientCfg, err := parseClientCommonCfg(CfgFileTypeIni, iniContent)
-		if err != nil {
-			fmt.Println(err)
-			os.Exit(1)
-		}
-
-		err = status(clientCfg)
-		if err != nil {
+		if err = status(cfg); err != nil {
 			fmt.Printf("frpc get status error: %v\n", err)
 			os.Exit(1)
 		}

+ 1 - 1
cmd/frpc/sub/stcp.go

@@ -45,7 +45,7 @@ var stcpCmd = &cobra.Command{
 	Use:   "stcp",
 	Short: "Run frpc with a single stcp proxy",
 	RunE: func(cmd *cobra.Command, args []string) error {
-		clientCfg, err := parseClientCommonCfg(CfgFileTypeCmd, nil)
+		clientCfg, err := parseClientCommonCfgFromCmd()
 		if err != nil {
 			fmt.Println(err)
 			os.Exit(1)

+ 1 - 1
cmd/frpc/sub/sudp.go

@@ -45,7 +45,7 @@ var sudpCmd = &cobra.Command{
 	Use:   "sudp",
 	Short: "Run frpc with a single sudp proxy",
 	RunE: func(cmd *cobra.Command, args []string) error {
-		clientCfg, err := parseClientCommonCfg(CfgFileTypeCmd, nil)
+		clientCfg, err := parseClientCommonCfgFromCmd()
 		if err != nil {
 			fmt.Println(err)
 			os.Exit(1)

+ 1 - 1
cmd/frpc/sub/tcp.go

@@ -41,7 +41,7 @@ var tcpCmd = &cobra.Command{
 	Use:   "tcp",
 	Short: "Run frpc with a single tcp proxy",
 	RunE: func(cmd *cobra.Command, args []string) error {
-		clientCfg, err := parseClientCommonCfg(CfgFileTypeCmd, nil)
+		clientCfg, err := parseClientCommonCfgFromCmd()
 		if err != nil {
 			fmt.Println(err)
 			os.Exit(1)

+ 1 - 1
cmd/frpc/sub/tcpmux.go

@@ -44,7 +44,7 @@ var tcpMuxCmd = &cobra.Command{
 	Use:   "tcpmux",
 	Short: "Run frpc with a single tcpmux proxy",
 	RunE: func(cmd *cobra.Command, args []string) error {
-		clientCfg, err := parseClientCommonCfg(CfgFileTypeCmd, nil)
+		clientCfg, err := parseClientCommonCfgFromCmd()
 		if err != nil {
 			fmt.Println(err)
 			os.Exit(1)

+ 1 - 1
cmd/frpc/sub/udp.go

@@ -41,7 +41,7 @@ var udpCmd = &cobra.Command{
 	Use:   "udp",
 	Short: "Run frpc with a single udp proxy",
 	RunE: func(cmd *cobra.Command, args []string) error {
-		clientCfg, err := parseClientCommonCfg(CfgFileTypeCmd, nil)
+		clientCfg, err := parseClientCommonCfgFromCmd()
 		if err != nil {
 			fmt.Println(err)
 			os.Exit(1)

+ 43 - 0
cmd/frpc/sub/verify.go

@@ -0,0 +1,43 @@
+// Copyright 2021 The frp Authors
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package sub
+
+import (
+	"fmt"
+	"os"
+
+	"github.com/fatedier/frp/pkg/config"
+
+	"github.com/spf13/cobra"
+)
+
+func init() {
+	rootCmd.AddCommand(verifyCmd)
+}
+
+var verifyCmd = &cobra.Command{
+	Use:   "verify",
+	Short: "Verify that the configures is valid",
+	RunE: func(cmd *cobra.Command, args []string) error {
+		_, _, _, err := config.ParseClientConfig(cfgFile)
+		if err != nil {
+			fmt.Println(err)
+			os.Exit(1)
+		}
+
+		fmt.Printf("frpc: the configuration file %s syntax is ok\n", cfgFile)
+		return nil
+	},
+}

+ 1 - 1
cmd/frpc/sub/xtcp.go

@@ -45,7 +45,7 @@ var xtcpCmd = &cobra.Command{
 	Use:   "xtcp",
 	Short: "Run frpc with a single xtcp proxy",
 	RunE: func(cmd *cobra.Command, args []string) error {
-		clientCfg, err := parseClientCommonCfg(CfgFileTypeCmd, nil)
+		clientCfg, err := parseClientCommonCfgFromCmd()
 		if err != nil {
 			fmt.Println(err)
 			os.Exit(1)

+ 53 - 0
cmd/frps/verify.go

@@ -0,0 +1,53 @@
+// Copyright 2021 The frp Authors
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package main
+
+import (
+	"fmt"
+	"os"
+
+	"github.com/fatedier/frp/pkg/config"
+
+	"github.com/spf13/cobra"
+)
+
+func init() {
+	rootCmd.AddCommand(verifyCmd)
+}
+
+var verifyCmd = &cobra.Command{
+	Use:   "verify",
+	Short: "Verify that the configures is valid",
+	RunE: func(cmd *cobra.Command, args []string) error {
+		if cfgFile == "" {
+			fmt.Println("no config file is specified")
+			return nil
+		}
+		iniContent, err := config.GetRenderedConfFromFile(cfgFile)
+		if err != nil {
+			fmt.Println(err)
+			os.Exit(1)
+		}
+
+		_, err = parseServerCommonCfg(CfgFileTypeIni, iniContent)
+		if err != nil {
+			fmt.Println(err)
+			os.Exit(1)
+		}
+
+		fmt.Printf("frps: the configuration file %s syntax is ok\n", cfgFile)
+		return nil
+	},
+}

+ 3 - 0
conf/frpc_full.ini

@@ -102,6 +102,9 @@ meta_var2 = 234
 # It affects the udp and sudp proxy.
 udp_packet_size = 1500
 
+# include other config files for proxies.
+# includes = ./confd/*.ini
+
 # 'ssh' is the unique proxy name
 # if user in [common] section is not empty, it will be changed to {user}.{proxy} such as 'your_name.ssh'
 [ssh]

+ 1 - 1
go.mod

@@ -11,7 +11,7 @@ require (
 	github.com/google/uuid v1.1.1
 	github.com/gorilla/mux v1.7.3
 	github.com/gorilla/websocket v1.4.0
-	github.com/hashicorp/yamux v0.0.0-20181012175058-2f1d1f20f75d
+	github.com/hashicorp/yamux v0.0.0-20210316155119-a95892c5f864
 	github.com/inconshreveable/mousetrap v1.0.0 // indirect
 	github.com/klauspost/cpuid v1.2.0 // indirect
 	github.com/klauspost/reedsolomon v1.9.1 // indirect

+ 2 - 2
go.sum

@@ -78,8 +78,8 @@ github.com/gorilla/mux v1.7.3/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2z
 github.com/gorilla/websocket v1.4.0 h1:WDFjx/TMzVgy9VdMMQi2K2Emtwi2QcUQsztZ/zLaH/Q=
 github.com/gorilla/websocket v1.4.0/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ=
 github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
-github.com/hashicorp/yamux v0.0.0-20181012175058-2f1d1f20f75d h1:kJCB4vdITiW1eC1vq2e6IsrXKrZit1bv/TDYFGMp4BQ=
-github.com/hashicorp/yamux v0.0.0-20181012175058-2f1d1f20f75d/go.mod h1:+NfK9FKeTrX5uv1uIXGdwYDTeHna2qgaIlx54MXqjAM=
+github.com/hashicorp/yamux v0.0.0-20210316155119-a95892c5f864 h1:Y4V+SFe7d3iH+9pJCoeWIOS5/xBJIFsltS7E+KJSsJY=
+github.com/hashicorp/yamux v0.0.0-20210316155119-a95892c5f864/go.mod h1:CtWFDAQgb7dxtzFs4tWbplKIe2jSi3+5vKbgIO0SLnQ=
 github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
 github.com/inconshreveable/mousetrap v1.0.0 h1:Z8tu5sraLXCXIcARxBp/8cbvlwVa7Z1NHg9XEKhtSvM=
 github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8=

+ 47 - 35
pkg/config/client.go

@@ -17,6 +17,7 @@ package config
 import (
 	"fmt"
 	"os"
+	"path/filepath"
 	"strings"
 
 	"github.com/fatedier/frp/pkg/auth"
@@ -68,10 +69,10 @@ type ClientCommonConf struct {
 	// is 0.
 	AdminPort int `ini:"admin_port" json:"admin_port"`
 	// AdminUser specifies the username that the admin server will use for
-	// login. By default, this value is "admin".
+	// login.
 	AdminUser string `ini:"admin_user" json:"admin_user"`
 	// AdminPwd specifies the password that the admin server will use for
-	// login. By default, this value is "admin".
+	// login.
 	AdminPwd string `ini:"admin_pwd" json:"admin_pwd"`
 	// AssetsDir specifies the local directory that the admin server will load
 	// resources from. If this value is "", assets will be loaded from the
@@ -136,40 +137,43 @@ type ClientCommonConf struct {
 	// UDPPacketSize specifies the udp packet size
 	// By default, this value is 1500
 	UDPPacketSize int64 `ini:"udp_packet_size" json:"udp_packet_size"`
+	// Include other config files for proxies.
+	IncludeConfigFiles []string `ini:"includes" json:"includes"`
 }
 
 // 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,
+		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),
 	}
 }
 
@@ -208,6 +212,15 @@ func (cfg *ClientCommonConf) Validate() error {
 		return fmt.Errorf("invalid protocol")
 	}
 
+	for _, f := range cfg.IncludeConfigFiles {
+		absDir, err := filepath.Abs(filepath.Dir(f))
+		if err != nil {
+			return fmt.Errorf("include: parse directory of %s failed: %v", f, absDir)
+		}
+		if _, err := os.Stat(absDir); os.IsNotExist(err) {
+			return fmt.Errorf("include: directory of %s not exist", f)
+		}
+	}
 	return nil
 }
 
@@ -236,7 +249,6 @@ func UnmarshalClientConfFromIni(source interface{}) (ClientCommonConf, error) {
 	}
 
 	common.Metas = GetMapWithoutPrefix(s.KeysHash(), "meta_")
-
 	return common, nil
 }
 
@@ -290,7 +302,7 @@ func LoadAllProxyConfsFromIni(
 	for _, section := range rangeSections {
 		err = renderRangeProxyTemplates(f, section)
 		if err != nil {
-			return nil, nil, fmt.Errorf("fail to render range-section[%s] with error: %v", section.Name(), err)
+			return nil, nil, fmt.Errorf("failed to render template for proxy %s: %v", section.Name(), err)
 		}
 	}
 
@@ -315,7 +327,7 @@ func LoadAllProxyConfsFromIni(
 		case "server":
 			newConf, newErr := NewProxyConfFromIni(prefix, name, section)
 			if newErr != nil {
-				return nil, nil, fmt.Errorf("fail to parse section[%s], err: %v", name, newErr)
+				return nil, nil, fmt.Errorf("failed to parse proxy %s, err: %v", name, newErr)
 			}
 			proxyConfs[prefix+name] = newConf
 		case "visitor":
@@ -325,7 +337,7 @@ func LoadAllProxyConfsFromIni(
 			}
 			visitorConfs[prefix+name] = newConf
 		default:
-			return nil, nil, fmt.Errorf("section[%s] role should be 'server' or 'visitor'", name)
+			return nil, nil, fmt.Errorf("proxy %s role should be 'server' or 'visitor'", name)
 		}
 	}
 	return proxyConfs, visitorConfs, nil

+ 3 - 3
pkg/config/client_test.go

@@ -290,12 +290,13 @@ func Test_LoadClientCommonConf(t *testing.T) {
 			"var1": "123",
 			"var2": "234",
 		},
-		UDPPacketSize: 1509,
+		UDPPacketSize:      1509,
+		IncludeConfigFiles: []string{},
 	}
 
 	common, err := UnmarshalClientConfFromIni(testClientBytesWithFull)
 	assert.NoError(err)
-	assert.Equal(expected, common)
+	assert.EqualValues(expected, common)
 }
 
 func Test_LoadClientBasicConf(t *testing.T) {
@@ -641,5 +642,4 @@ func Test_LoadClientBasicConf(t *testing.T) {
 	assert.NoError(err)
 	assert.Equal(proxyExpected, proxyActual)
 	assert.Equal(visitorExpected, visitorActual)
-
 }

+ 100 - 0
pkg/config/parse.go

@@ -0,0 +1,100 @@
+// Copyright 2021 The frp Authors
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package config
+
+import (
+	"bytes"
+	"fmt"
+	"io/ioutil"
+	"os"
+	"path/filepath"
+)
+
+func ParseClientConfig(filePath string) (
+	cfg ClientCommonConf,
+	pxyCfgs map[string]ProxyConf,
+	visitorCfgs map[string]VisitorConf,
+	err error,
+) {
+	var content []byte
+	content, err = GetRenderedConfFromFile(filePath)
+	if err != nil {
+		return
+	}
+	configBuffer := bytes.NewBuffer(nil)
+	configBuffer.Write(content)
+
+	// Parse common section.
+	cfg, err = UnmarshalClientConfFromIni(content)
+	if err != nil {
+		return
+	}
+	cfg.Complete()
+	if err = cfg.Validate(); err != nil {
+		err = fmt.Errorf("Parse config error: %v", err)
+		return
+	}
+
+	// Aggregate proxy configs from include files.
+	var buf []byte
+	buf, err = getIncludeContents(cfg.IncludeConfigFiles)
+	if err != nil {
+		err = fmt.Errorf("getIncludeContents error: %v", err)
+		return
+	}
+	configBuffer.WriteString("\n")
+	configBuffer.Write(buf)
+
+	// Parse all proxy and visitor configs.
+	pxyCfgs, visitorCfgs, err = LoadAllProxyConfsFromIni(cfg.User, configBuffer.Bytes(), cfg.Start)
+	if err != nil {
+		return
+	}
+	return
+}
+
+// getIncludeContents renders all configs from paths.
+// files format can be a single file path or directory or regex path.
+func getIncludeContents(paths []string) ([]byte, error) {
+	out := bytes.NewBuffer(nil)
+	for _, path := range paths {
+		absDir, err := filepath.Abs(filepath.Dir(path))
+		if err != nil {
+			return nil, err
+		}
+		if _, err := os.Stat(absDir); os.IsNotExist(err) {
+			return nil, err
+		}
+		files, err := ioutil.ReadDir(absDir)
+		if err != nil {
+			return nil, err
+		}
+		for _, fi := range files {
+			if fi.IsDir() {
+				continue
+			}
+			absFile := filepath.Join(absDir, fi.Name())
+			if matched, _ := filepath.Match(filepath.Join(absDir, filepath.Base(path)), absFile); matched {
+				tmpContent, err := GetRenderedConfFromFile(absFile)
+				if err != nil {
+					return nil, fmt.Errorf("render extra config %s error: %v", absFile, err)
+				}
+				out.Write(tmpContent)
+				out.WriteString("\n")
+			}
+		}
+	}
+	return out.Bytes(), nil
+}

+ 1 - 2
pkg/config/proxy.go

@@ -143,7 +143,6 @@ type BaseProxyConf struct {
 	// meta info for each proxy
 	Metas map[string]string `ini:"-" json:"metas"`
 
-	// TODO: LocalSvrConf => LocalAppConf
 	LocalSvrConf    `ini:",extends"`
 	HealthCheckConf `ini:",extends"`
 }
@@ -274,7 +273,7 @@ func NewProxyConfFromIni(prefix, name string, section *ini.Section) (ProxyConf,
 
 	conf := DefaultProxyConf(proxyType)
 	if conf == nil {
-		return nil, fmt.Errorf("proxy [%s] type [%s] error", name, proxyType)
+		return nil, fmt.Errorf("proxy %s has invalid type [%s]", name, proxyType)
 	}
 
 	if err := conf.UnmarshalFromIni(prefix, name, section); err != nil {

+ 5 - 6
pkg/config/server.go

@@ -72,10 +72,10 @@ type ServerCommonConf struct {
 	// 0.
 	DashboardPort int `ini:"dashboard_port" json:"dashboard_port"`
 	// DashboardUser specifies the username that the dashboard will use for
-	// login. By default, this value is "admin".
+	// login.
 	DashboardUser string `ini:"dashboard_user" json:"dashboard_user"`
-	// DashboardUser specifies the password that the dashboard will use for
-	// login. By default, this value is "admin".
+	// DashboardPwd specifies the password that the dashboard will use for
+	// login.
 	DashboardPwd string `ini:"dashboard_pwd" json:"dashboard_pwd"`
 	// EnablePrometheus will export prometheus metrics on {dashboard_addr}:{dashboard_port}
 	// in /metrics api.
@@ -181,8 +181,8 @@ func GetDefaultServerConf() ServerCommonConf {
 		VhostHTTPTimeout:       60,
 		DashboardAddr:          "0.0.0.0",
 		DashboardPort:          0,
-		DashboardUser:          "admin",
-		DashboardPwd:           "admin",
+		DashboardUser:          "",
+		DashboardPwd:           "",
 		EnablePrometheus:       false,
 		AssetsDir:              "",
 		LogFile:                "console",
@@ -223,7 +223,6 @@ func UnmarshalServerConfFromIni(source interface{}) (ServerCommonConf, error) {
 
 	s, err := f.GetSection("common")
 	if err != nil {
-		// TODO: add error info
 		return ServerCommonConf{}, err
 	}
 

+ 2 - 2
pkg/config/server_test.go

@@ -180,8 +180,8 @@ func Test_LoadServerCommonConf(t *testing.T) {
 				ProxyBindAddr:          "0.0.0.9",
 				VhostHTTPTimeout:       60,
 				DashboardAddr:          "0.0.0.0",
-				DashboardUser:          "admin",
-				DashboardPwd:           "admin",
+				DashboardUser:          "",
+				DashboardPwd:           "",
 				EnablePrometheus:       false,
 				LogFile:                "console",
 				LogWay:                 "console",

+ 1 - 1
pkg/util/version/version.go

@@ -19,7 +19,7 @@ import (
 	"strings"
 )
 
-var version string = "0.36.2"
+var version string = "0.37.0"
 
 func Full() string {
 	return version

+ 6 - 8
server/control.go

@@ -248,12 +248,10 @@ func (ctl *Control) GetWorkConn() (workConn net.Conn, err error) {
 		xl.Debug("get work connection from pool")
 	default:
 		// no work connections available in the poll, send message to frpc to get more
-		err = errors.PanicToError(func() {
+		if err = errors.PanicToError(func() {
 			ctl.sendCh <- &msg.ReqWorkConn{}
-		})
-		if err != nil {
-			xl.Error("%v", err)
-			return
+		}); err != nil {
+			return nil, fmt.Errorf("control is already closed")
 		}
 
 		select {
@@ -357,15 +355,15 @@ func (ctl *Control) stoper() {
 
 	ctl.allShutdown.WaitStart()
 
+	ctl.conn.Close()
+	ctl.readerShutdown.WaitDone()
+
 	close(ctl.readCh)
 	ctl.managerShutdown.WaitDone()
 
 	close(ctl.sendCh)
 	ctl.writerShutdown.WaitDone()
 
-	ctl.conn.Close()
-	ctl.readerShutdown.WaitDone()
-
 	ctl.mu.Lock()
 	defer ctl.mu.Unlock()
 

+ 1 - 1
server/proxy/udp.go

@@ -60,7 +60,7 @@ func (pxy *UDPProxy) Run() (remoteAddr string, err error) {
 	xl := pxy.xl
 	pxy.realPort, err = pxy.rc.UDPPortManager.Acquire(pxy.name, pxy.cfg.RemotePort)
 	if err != nil {
-		return
+		return "", fmt.Errorf("acquire port %d error: %v", pxy.cfg.RemotePort, err)
 	}
 	defer func() {
 		if err != nil {

+ 101 - 23
test/e2e/basic/basic.go

@@ -3,16 +3,16 @@ package basic
 import (
 	"fmt"
 	"strings"
-	"time"
 
 	"github.com/fatedier/frp/test/e2e/framework"
 	"github.com/fatedier/frp/test/e2e/framework/consts"
+	"github.com/fatedier/frp/test/e2e/mock/server"
+	"github.com/fatedier/frp/test/e2e/pkg/port"
+	"github.com/fatedier/frp/test/e2e/pkg/request"
 
 	. "github.com/onsi/ginkgo"
 )
 
-var connTimeout = 2 * time.Second
-
 var _ = Describe("[Feature: Basic]", func() {
 	f := framework.NewDefaultFramework()
 
@@ -50,21 +50,21 @@ var _ = Describe("[Feature: Basic]", func() {
 				}{
 					{
 						proxyName: "normal",
-						portName:  framework.GenPortName("Normal"),
+						portName:  port.GenName("Normal"),
 					},
 					{
 						proxyName:   "with-encryption",
-						portName:    framework.GenPortName("WithEncryption"),
+						portName:    port.GenName("WithEncryption"),
 						extraConfig: "use_encryption = true",
 					},
 					{
 						proxyName:   "with-compression",
-						portName:    framework.GenPortName("WithCompression"),
+						portName:    port.GenName("WithCompression"),
 						extraConfig: "use_compression = true",
 					},
 					{
 						proxyName: "with-encryption-and-compression",
-						portName:  framework.GenPortName("WithEncryptionAndCompression"),
+						portName:  port.GenName("WithEncryptionAndCompression"),
 						extraConfig: `
 						use_encryption = true
 						use_compression = true
@@ -80,8 +80,11 @@ var _ = Describe("[Feature: Basic]", func() {
 				f.RunProcesses([]string{serverConf}, []string{clientConf})
 
 				for _, test := range tests {
-					framework.ExpectRequest(protocol, f.UsedPorts[test.portName],
-						[]byte(consts.TestString), []byte(consts.TestString), connTimeout, test.proxyName)
+					framework.NewRequestExpect(f).
+						RequestModify(framework.SetRequestProtocol(protocol)).
+						PortName(test.portName).
+						Explain(test.proxyName).
+						Ensure()
 				}
 			})
 		}
@@ -139,24 +142,24 @@ var _ = Describe("[Feature: Basic]", func() {
 				}{
 					{
 						proxyName:    "normal",
-						bindPortName: framework.GenPortName("Normal"),
+						bindPortName: port.GenName("Normal"),
 						visitorSK:    correctSK,
 					},
 					{
 						proxyName:    "with-encryption",
-						bindPortName: framework.GenPortName("WithEncryption"),
+						bindPortName: port.GenName("WithEncryption"),
 						visitorSK:    correctSK,
 						extraConfig:  "use_encryption = true",
 					},
 					{
 						proxyName:    "with-compression",
-						bindPortName: framework.GenPortName("WithCompression"),
+						bindPortName: port.GenName("WithCompression"),
 						visitorSK:    correctSK,
 						extraConfig:  "use_compression = true",
 					},
 					{
 						proxyName:    "with-encryption-and-compression",
-						bindPortName: framework.GenPortName("WithEncryptionAndCompression"),
+						bindPortName: port.GenName("WithEncryptionAndCompression"),
 						visitorSK:    correctSK,
 						extraConfig: `
 						use_encryption = true
@@ -165,7 +168,7 @@ var _ = Describe("[Feature: Basic]", func() {
 					},
 					{
 						proxyName:    "with-error-sk",
-						bindPortName: framework.GenPortName("WithErrorSK"),
+						bindPortName: port.GenName("WithErrorSK"),
 						visitorSK:    wrongSK,
 						expectError:  true,
 					},
@@ -182,17 +185,92 @@ var _ = Describe("[Feature: Basic]", func() {
 				f.RunProcesses([]string{serverConf}, []string{clientServerConf, clientVisitorConf})
 
 				for _, test := range tests {
-					expectResp := []byte(consts.TestString)
-					if test.expectError {
-						framework.ExpectRequestError(protocol, f.UsedPorts[test.bindPortName],
-							[]byte(consts.TestString), connTimeout, test.proxyName)
-						continue
-					}
-
-					framework.ExpectRequest(protocol, f.UsedPorts[test.bindPortName],
-						[]byte(consts.TestString), expectResp, connTimeout, test.proxyName)
+					framework.NewRequestExpect(f).
+						RequestModify(framework.SetRequestProtocol(protocol)).
+						PortName(test.bindPortName).
+						Explain(test.proxyName).
+						ExpectError(test.expectError).
+						Ensure()
+
 				}
 			})
 		}
 	})
+
+	Describe("TCPMUX", func() {
+		It("Type tcpmux", func() {
+			serverConf := consts.DefaultServerConfig
+			clientConf := consts.DefaultClientConfig
+
+			tcpmuxHTTPConnectPortName := port.GenName("TCPMUX")
+			serverConf += fmt.Sprintf(`
+			tcpmux_httpconnect_port = {{ .%s }}
+			`, tcpmuxHTTPConnectPortName)
+
+			getProxyConf := func(proxyName string, extra string) string {
+				return fmt.Sprintf(`
+				[%s]
+				type = tcpmux
+				multiplexer = httpconnect
+				local_port = {{ .%s }}
+				custom_domains = %s
+				`+extra, proxyName, port.GenName(proxyName), proxyName)
+			}
+
+			tests := []struct {
+				proxyName   string
+				extraConfig string
+			}{
+				{
+					proxyName: "normal",
+				},
+				{
+					proxyName:   "with-encryption",
+					extraConfig: "use_encryption = true",
+				},
+				{
+					proxyName:   "with-compression",
+					extraConfig: "use_compression = true",
+				},
+				{
+					proxyName: "with-encryption-and-compression",
+					extraConfig: `
+						use_encryption = true
+						use_compression = true
+					`,
+				},
+			}
+
+			// build all client config
+			for _, test := range tests {
+				clientConf += getProxyConf(test.proxyName, test.extraConfig) + "\n"
+
+				localServer := server.New(server.TCP, server.WithBindPort(f.AllocPort()), server.WithRespContent([]byte(test.proxyName)))
+				f.RunServer(port.GenName(test.proxyName), localServer)
+			}
+
+			// run frps and frpc
+			f.RunProcesses([]string{serverConf}, []string{clientConf})
+
+			// Request without HTTP connect should get error
+			framework.NewRequestExpect(f).
+				PortName(tcpmuxHTTPConnectPortName).
+				ExpectError(true).
+				Explain("request without HTTP connect expect error").
+				Ensure()
+
+			proxyURL := fmt.Sprintf("http://127.0.0.1:%d", f.PortByName(tcpmuxHTTPConnectPortName))
+			// Request with incorrect connect hostname
+			framework.NewRequestExpect(f).RequestModify(func(r *request.Request) {
+				r.Proxy(proxyURL, "invalid")
+			}).ExpectError(true).Explain("request without HTTP connect expect error").Ensure()
+
+			// Request with correct connect hostname
+			for _, test := range tests {
+				framework.NewRequestExpect(f).RequestModify(func(r *request.Request) {
+					r.Proxy(proxyURL, test.proxyName)
+				}).ExpectResp([]byte(test.proxyName)).Explain(test.proxyName).Ensure()
+			}
+		})
+	})
 })

+ 9 - 13
test/e2e/basic/client_server.go

@@ -6,6 +6,7 @@ import (
 
 	"github.com/fatedier/frp/test/e2e/framework"
 	"github.com/fatedier/frp/test/e2e/framework/consts"
+	"github.com/fatedier/frp/test/e2e/pkg/port"
 
 	. "github.com/onsi/ginkgo"
 )
@@ -16,6 +17,7 @@ type generalTestConfigures struct {
 	expectError bool
 }
 
+// defineClientServerTest test a normal tcp and udp proxy with specified TestConfigures.
 func defineClientServerTest(desc string, f *framework.Framework, configures *generalTestConfigures) {
 	It(desc, func() {
 		serverConf := consts.DefaultServerConfig
@@ -25,6 +27,8 @@ func defineClientServerTest(desc string, f *framework.Framework, configures *gen
 				%s
 				`, configures.server)
 
+		tcpPortName := port.GenName("TCP")
+		udpPortName := port.GenName("UDP")
 		clientConf += fmt.Sprintf(`
 				%s
 
@@ -38,23 +42,15 @@ func defineClientServerTest(desc string, f *framework.Framework, configures *gen
 				local_port = {{ .%s }}
 				remote_port = {{ .%s }}
 				`, configures.client,
-			framework.TCPEchoServerPort, framework.GenPortName("TCP"),
-			framework.UDPEchoServerPort, framework.GenPortName("UDP"),
+			framework.TCPEchoServerPort, tcpPortName,
+			framework.UDPEchoServerPort, udpPortName,
 		)
 
 		f.RunProcesses([]string{serverConf}, []string{clientConf})
 
-		if !configures.expectError {
-			framework.ExpectTCPRequest(f.UsedPorts[framework.GenPortName("TCP")],
-				[]byte(consts.TestString), []byte(consts.TestString), connTimeout, "tcp proxy")
-			framework.ExpectUDPRequest(f.UsedPorts[framework.GenPortName("UDP")],
-				[]byte(consts.TestString), []byte(consts.TestString), connTimeout, "udp proxy")
-		} else {
-			framework.ExpectTCPRequestError(f.UsedPorts[framework.GenPortName("TCP")],
-				[]byte(consts.TestString), connTimeout, "tcp proxy")
-			framework.ExpectUDPRequestError(f.UsedPorts[framework.GenPortName("UDP")],
-				[]byte(consts.TestString), connTimeout, "udp proxy")
-		}
+		framework.NewRequestExpect(f).PortName(tcpPortName).ExpectError(configures.expectError).Explain("tcp proxy").Ensure()
+		framework.NewRequestExpect(f).RequestModify(framework.SetRequestProtocol("udp")).
+			PortName(udpPortName).ExpectError(configures.expectError).Explain("udp proxy").Ensure()
 	})
 }
 

+ 79 - 0
test/e2e/basic/server.go

@@ -0,0 +1,79 @@
+package basic
+
+import (
+	"fmt"
+
+	"github.com/fatedier/frp/test/e2e/framework"
+	"github.com/fatedier/frp/test/e2e/framework/consts"
+	"github.com/fatedier/frp/test/e2e/pkg/port"
+	"github.com/fatedier/frp/test/e2e/pkg/request"
+
+	. "github.com/onsi/ginkgo"
+)
+
+var _ = Describe("[Feature: Server Manager]", func() {
+	f := framework.NewDefaultFramework()
+
+	It("Ports Whitelist", func() {
+		serverConf := consts.DefaultServerConfig
+		clientConf := consts.DefaultClientConfig
+
+		serverConf += `
+			allow_ports = 10000-20000,20002,30000-50000
+		`
+
+		tcpPortName := port.GenName("TCP", port.WithRangePorts(10000, 20000))
+		udpPortName := port.GenName("UDP", port.WithRangePorts(30000, 50000))
+		clientConf += fmt.Sprintf(`
+			[tcp-allowded-in-range]
+			type = tcp
+			local_port = {{ .%s }}
+			remote_port = {{ .%s }}
+			`, framework.TCPEchoServerPort, tcpPortName)
+		clientConf += fmt.Sprintf(`
+			[tcp-port-not-allowed]
+			type = tcp
+			local_port = {{ .%s }}
+			remote_port = 20001
+			`, framework.TCPEchoServerPort)
+		clientConf += fmt.Sprintf(`
+			[tcp-port-unavailable]
+			type = tcp
+			local_port = {{ .%s }}
+			remote_port = {{ .%s }}
+			`, framework.TCPEchoServerPort, consts.PortServerName)
+		clientConf += fmt.Sprintf(`
+			[udp-allowed-in-range]
+			type = udp
+			local_port = {{ .%s }}
+			remote_port = {{ .%s }}
+			`, framework.UDPEchoServerPort, udpPortName)
+		clientConf += fmt.Sprintf(`
+			[udp-port-not-allowed]
+			type = udp
+			local_port = {{ .%s }}
+			remote_port = 20003
+			`, framework.UDPEchoServerPort)
+
+		f.RunProcesses([]string{serverConf}, []string{clientConf})
+
+		// TCP
+		// Allowed in range
+		framework.NewRequestExpect(f).PortName(tcpPortName).Ensure()
+
+		// Not Allowed
+		framework.NewRequestExpect(f).RequestModify(framework.SetRequestPort(20001)).ExpectError(true).Ensure()
+
+		// Unavailable, already bind by frps
+		framework.NewRequestExpect(f).PortName(consts.PortServerName).ExpectError(true).Ensure()
+
+		// UDP
+		// Allowed in range
+		framework.NewRequestExpect(f).RequestModify(framework.SetRequestProtocol("udp")).PortName(udpPortName).Ensure()
+
+		// Not Allowed
+		framework.NewRequestExpect(f).RequestModify(func(r *request.Request) {
+			r.UDP().Port(20003)
+		}).ExpectError(true).Ensure()
+	})
+})

+ 0 - 1
test/e2e/e2e.go

@@ -49,7 +49,6 @@ func RunE2ETests(t *testing.T) {
 // accepting the byte array.
 func setupSuite() {
 	// Run only on Ginkgo node 1
-	// TODO
 }
 
 // setupSuitePerGinkgoNode is the boilerplate that can be used to setup ginkgo test suites, on the SynchronizedBeforeSuite step.

+ 4 - 5
test/e2e/examples.go

@@ -2,16 +2,14 @@ package e2e
 
 import (
 	"fmt"
-	"time"
 
 	"github.com/fatedier/frp/test/e2e/framework"
 	"github.com/fatedier/frp/test/e2e/framework/consts"
+	"github.com/fatedier/frp/test/e2e/pkg/port"
 
 	. "github.com/onsi/ginkgo"
 )
 
-var connTimeout = 2 * time.Second
-
 var _ = Describe("[Feature: Example]", func() {
 	f := framework.NewDefaultFramework()
 
@@ -20,16 +18,17 @@ var _ = Describe("[Feature: Example]", func() {
 			serverConf := consts.DefaultServerConfig
 			clientConf := consts.DefaultClientConfig
 
+			portName := port.GenName("TCP")
 			clientConf += fmt.Sprintf(`
 			[tcp]
 			type = tcp
 			local_port = {{ .%s }}
 			remote_port = {{ .%s }}
-			`, framework.TCPEchoServerPort, framework.GenPortName("TCP"))
+			`, framework.TCPEchoServerPort, portName)
 
 			f.RunProcesses([]string{serverConf}, []string{clientConf})
 
-			framework.ExpectTCPRequest(f.UsedPorts[framework.GenPortName("TCP")], []byte(consts.TestString), []byte(consts.TestString), connTimeout)
+			framework.NewRequestExpect(f).PortName(portName).Ensure()
 		})
 	})
 })

+ 23 - 6
test/e2e/framework/consts/consts.go

@@ -1,21 +1,38 @@
 package consts
 
-const (
-	TestString = "frp is a fast reverse proxy to help you expose a local server behind a NAT or firewall to the internet."
+import (
+	"fmt"
+	"time"
+
+	"github.com/fatedier/frp/test/e2e/pkg/port"
 )
 
 const (
-	PortServerName = "PortServer"
+	TestString = "frp is a fast reverse proxy to help you expose a local server behind a NAT or firewall to the internet."
+
+	DefaultTimeout = 2 * time.Second
 )
 
-const (
+var (
+	PortServerName  string
+	PortClientAdmin string
+
 	DefaultServerConfig = `
 	[common]
-	bind_port = {{ .PortServer }}
+	bind_port = {{ .%s }}
+	log_level = trace
 	`
 
 	DefaultClientConfig = `
 	[common]
-	server_port = {{ .PortServer }}
+	server_port = {{ .%s }}
+	log_level = trace
 	`
 )
+
+func init() {
+	PortServerName = port.GenName("Server")
+	PortClientAdmin = port.GenName("ClientAdmin")
+	DefaultServerConfig = fmt.Sprintf(DefaultServerConfig, port.GenName("Server"))
+	DefaultClientConfig = fmt.Sprintf(DefaultClientConfig, port.GenName("Server"))
+}

+ 47 - 7
test/e2e/framework/framework.go

@@ -9,6 +9,7 @@ import (
 	"strings"
 	"text/template"
 
+	"github.com/fatedier/frp/test/e2e/mock/server"
 	"github.com/fatedier/frp/test/e2e/pkg/port"
 	"github.com/fatedier/frp/test/e2e/pkg/process"
 
@@ -25,11 +26,14 @@ type Options struct {
 
 type Framework struct {
 	TempDirectory string
-	UsedPorts     map[string]int
 
+	// ports used in this framework indexed by port name.
+	usedPorts map[string]int
+
+	// portAllocator to alloc port for this test case.
 	portAllocator *port.Allocator
 
-	// Multiple mock servers used for e2e testing.
+	// Multiple default mock servers used for e2e testing.
 	mockServers *MockServers
 
 	// To make sure that this framework cleans up after itself, no matter what,
@@ -44,6 +48,9 @@ type Framework struct {
 	serverProcesses []*process.Process
 	clientConfPaths []string
 	clientProcesses []*process.Process
+
+	// Manual registered mock servers.
+	servers []*server.Server
 }
 
 func NewDefaultFramework() *Framework {
@@ -59,6 +66,7 @@ func NewDefaultFramework() *Framework {
 func NewFramework(opt Options) *Framework {
 	f := &Framework{
 		portAllocator: port.NewAllocator(opt.FromPortIndex, opt.ToPortIndex, opt.TotalParallelNode, opt.CurrentNodeIndex-1),
+		usedPorts:     make(map[string]int),
 	}
 
 	ginkgo.BeforeEach(f.BeforeEach)
@@ -107,9 +115,14 @@ func (f *Framework) AfterEach() {
 	f.serverProcesses = nil
 	f.clientProcesses = nil
 
-	// close mock servers
+	// close default mock servers
 	f.mockServers.Close()
 
+	// close manual registered mock servers
+	for _, s := range f.servers {
+		s.Close()
+	}
+
 	// clean directory
 	os.RemoveAll(f.TempDirectory)
 	f.TempDirectory = ""
@@ -117,10 +130,10 @@ func (f *Framework) AfterEach() {
 	f.clientConfPaths = nil
 
 	// release used ports
-	for _, port := range f.UsedPorts {
+	for _, port := range f.usedPorts {
 		f.portAllocator.Release(port)
 	}
-	f.UsedPorts = nil
+	f.usedPorts = make(map[string]int)
 }
 
 var portRegex = regexp.MustCompile(`{{ \.Port.*? }}`)
@@ -151,16 +164,16 @@ func (f *Framework) genPortsFromTemplates(templates []string) (ports map[string]
 	}()
 
 	for name := range ports {
-		port := f.portAllocator.Get()
+		port := f.portAllocator.GetByName(name)
 		if port <= 0 {
 			return nil, fmt.Errorf("can't allocate port")
 		}
 		ports[name] = port
 	}
 	return
-
 }
 
+// RenderTemplates alloc all ports for port names placeholder.
 func (f *Framework) RenderTemplates(templates []string) (outs []string, ports map[string]int, err error) {
 	ports, err = f.genPortsFromTemplates(templates)
 	if err != nil {
@@ -172,6 +185,10 @@ func (f *Framework) RenderTemplates(templates []string) (outs []string, ports ma
 		params[name] = port
 	}
 
+	for name, port := range f.usedPorts {
+		params[name] = port
+	}
+
 	for _, t := range templates {
 		tmpl, err := template.New("").Parse(t)
 		if err != nil {
@@ -185,3 +202,26 @@ func (f *Framework) RenderTemplates(templates []string) (outs []string, ports ma
 	}
 	return
 }
+
+func (f *Framework) PortByName(name string) int {
+	return f.usedPorts[name]
+}
+
+func (f *Framework) AllocPort() int {
+	port := f.portAllocator.Get()
+	ExpectTrue(port > 0, "alloc port failed")
+	return port
+}
+
+func (f *Framework) ReleasePort(port int) {
+	f.portAllocator.Release(port)
+}
+
+func (f *Framework) RunServer(portName string, s *server.Server) {
+	f.servers = append(f.servers, s)
+	if s.BindPort() > 0 {
+		f.usedPorts[portName] = s.BindPort()
+	}
+	err := s.Run()
+	ExpectNoError(err, portName)
+}

+ 11 - 25
test/e2e/framework/mockservers.go

@@ -4,7 +4,7 @@ import (
 	"fmt"
 	"os"
 
-	"github.com/fatedier/frp/test/e2e/mock/echoserver"
+	"github.com/fatedier/frp/test/e2e/mock/server"
 	"github.com/fatedier/frp/test/e2e/pkg/port"
 )
 
@@ -15,36 +15,22 @@ const (
 )
 
 type MockServers struct {
-	tcpEchoServer *echoserver.Server
-	udpEchoServer *echoserver.Server
-	udsEchoServer *echoserver.Server
+	tcpEchoServer *server.Server
+	udpEchoServer *server.Server
+	udsEchoServer *server.Server
 }
 
 func NewMockServers(portAllocator *port.Allocator) *MockServers {
 	s := &MockServers{}
 	tcpPort := portAllocator.Get()
 	udpPort := portAllocator.Get()
-	s.tcpEchoServer = echoserver.New(echoserver.Options{
-		Type:      echoserver.TCP,
-		BindAddr:  "127.0.0.1",
-		BindPort:  int32(tcpPort),
-		RepeatNum: 1,
-	})
-	s.udpEchoServer = echoserver.New(echoserver.Options{
-		Type:      echoserver.UDP,
-		BindAddr:  "127.0.0.1",
-		BindPort:  int32(udpPort),
-		RepeatNum: 1,
-	})
+	s.tcpEchoServer = server.New(server.TCP, server.WithBindPort(tcpPort), server.WithEchoMode(true))
+	s.udpEchoServer = server.New(server.UDP, server.WithBindPort(udpPort), server.WithEchoMode(true))
 
 	udsIndex := portAllocator.Get()
 	udsAddr := fmt.Sprintf("%s/frp_echo_server_%d.sock", os.TempDir(), udsIndex)
 	os.Remove(udsAddr)
-	s.udsEchoServer = echoserver.New(echoserver.Options{
-		Type:      echoserver.Unix,
-		BindAddr:  udsAddr,
-		RepeatNum: 1,
-	})
+	s.udsEchoServer = server.New(server.Unix, server.WithBindAddr(udsAddr), server.WithEchoMode(true))
 	return s
 }
 
@@ -65,14 +51,14 @@ func (m *MockServers) Close() {
 	m.tcpEchoServer.Close()
 	m.udpEchoServer.Close()
 	m.udsEchoServer.Close()
-	os.Remove(m.udsEchoServer.GetOptions().BindAddr)
+	os.Remove(m.udsEchoServer.BindAddr())
 }
 
 func (m *MockServers) GetTemplateParams() map[string]interface{} {
 	ret := make(map[string]interface{})
-	ret[TCPEchoServerPort] = m.tcpEchoServer.GetOptions().BindPort
-	ret[UDPEchoServerPort] = m.udpEchoServer.GetOptions().BindPort
-	ret[UDSEchoServerAddr] = m.udsEchoServer.GetOptions().BindAddr
+	ret[TCPEchoServerPort] = m.tcpEchoServer.BindPort()
+	ret[UDPEchoServerPort] = m.udpEchoServer.BindPort()
+	ret[UDSEchoServerAddr] = m.udsEchoServer.BindAddr()
 	return ret
 }
 

+ 5 - 2
test/e2e/framework/process.go

@@ -28,7 +28,9 @@ func (f *Framework) RunProcesses(serverTemplates []string, clientTemplates []str
 	ExpectNoError(err)
 	ExpectTrue(len(templates) > 0)
 
-	f.UsedPorts = ports
+	for name, port := range ports {
+		f.usedPorts[name] = port
+	}
 
 	for i := range serverTemplates {
 		path := filepath.Join(f.TempDirectory, fmt.Sprintf("frp-e2e-server-%d", i))
@@ -40,8 +42,8 @@ func (f *Framework) RunProcesses(serverTemplates []string, clientTemplates []str
 		f.serverProcesses = append(f.serverProcesses, p)
 		err = p.Start()
 		ExpectNoError(err)
-		time.Sleep(500 * time.Millisecond)
 	}
+	time.Sleep(time.Second)
 
 	for i := range clientTemplates {
 		index := i + len(serverTemplates)
@@ -56,4 +58,5 @@ func (f *Framework) RunProcesses(serverTemplates []string, clientTemplates []str
 		ExpectNoError(err)
 		time.Sleep(500 * time.Millisecond)
 	}
+	time.Sleep(500 * time.Millisecond)
 }

+ 69 - 30
test/e2e/framework/request.go

@@ -1,51 +1,90 @@
 package framework
 
 import (
-	"time"
-
+	"github.com/fatedier/frp/test/e2e/framework/consts"
 	"github.com/fatedier/frp/test/e2e/pkg/request"
 )
 
-func ExpectRequest(protocol string, port int, in, out []byte, timeout time.Duration, explain ...interface{}) {
-	switch protocol {
-	case "tcp":
-		ExpectTCPRequest(port, in, out, timeout, explain...)
-	case "udp":
-		ExpectUDPRequest(port, in, out, timeout, explain...)
-	default:
-		Failf("ExpectRequest not support protocol: %s", protocol)
+func SetRequestProtocol(protocol string) func(*request.Request) {
+	return func(r *request.Request) {
+		r.Protocol(protocol)
 	}
 }
 
-func ExpectRequestError(protocol string, port int, in []byte, timeout time.Duration, explain ...interface{}) {
-	switch protocol {
-	case "tcp":
-		ExpectTCPRequestError(port, in, timeout, explain...)
-	case "udp":
-		ExpectUDPRequestError(port, in, timeout, explain...)
-	default:
-		Failf("ExpectRequestError not support protocol: %s", protocol)
+func SetRequestPort(port int) func(*request.Request) {
+	return func(r *request.Request) {
+		r.Port(port)
 	}
 }
 
-func ExpectTCPRequest(port int, in, out []byte, timeout time.Duration, explain ...interface{}) {
-	res, err := request.SendTCPRequest(port, in, timeout)
+// NewRequest return a default TCP request with default timeout and content.
+func NewRequest() *request.Request {
+	return request.New().
+		Timeout(consts.DefaultTimeout).
+		Body([]byte(consts.TestString))
+}
+
+func ExpectResponse(req *request.Request, expectResp []byte, explain ...interface{}) {
+	ret, err := req.Do()
 	ExpectNoError(err, explain...)
-	ExpectEqual(string(out), res, explain...)
+	ExpectEqualValues(expectResp, ret, explain...)
 }
 
-func ExpectTCPRequestError(port int, in []byte, timeout time.Duration, explain ...interface{}) {
-	_, err := request.SendTCPRequest(port, in, timeout)
+func ExpectResponseError(req *request.Request, explain ...interface{}) {
+	_, err := req.Do()
 	ExpectError(err, explain...)
 }
 
-func ExpectUDPRequest(port int, in, out []byte, timeout time.Duration, explain ...interface{}) {
-	res, err := request.SendUDPRequest(port, in, timeout)
-	ExpectNoError(err, explain...)
-	ExpectEqual(string(out), res, explain...)
+type RequestExpect struct {
+	req *request.Request
+
+	f           *Framework
+	expectResp  []byte
+	expectError bool
+	explain     []interface{}
 }
 
-func ExpectUDPRequestError(port int, in []byte, timeout time.Duration, explain ...interface{}) {
-	_, err := request.SendUDPRequest(port, in, timeout)
-	ExpectError(err, explain...)
+func NewRequestExpect(f *Framework) *RequestExpect {
+	return &RequestExpect{
+		req:         NewRequest(),
+		f:           f,
+		expectResp:  []byte(consts.TestString),
+		expectError: false,
+		explain:     make([]interface{}, 0),
+	}
+}
+
+func (e *RequestExpect) RequestModify(f func(r *request.Request)) *RequestExpect {
+	f(e.req)
+	return e
+}
+
+func (e *RequestExpect) PortName(name string) *RequestExpect {
+	if e.f != nil {
+		e.req.Port(e.f.PortByName(name))
+	}
+	return e
+}
+
+func (e *RequestExpect) ExpectResp(resp []byte) *RequestExpect {
+	e.expectResp = resp
+	return e
+}
+
+func (e *RequestExpect) ExpectError(expectErr bool) *RequestExpect {
+	e.expectError = expectErr
+	return e
+}
+
+func (e *RequestExpect) Explain(explain ...interface{}) *RequestExpect {
+	e.explain = explain
+	return e
+}
+
+func (e *RequestExpect) Ensure() {
+	if e.expectError {
+		ExpectResponseError(e.req, e.explain...)
+	} else {
+		ExpectResponse(e.req, e.expectResp, e.explain...)
+	}
 }

+ 0 - 4
test/e2e/framework/util.go

@@ -12,7 +12,3 @@ func init() {
 	uuid, _ := uuid.NewUUID()
 	RunID = uuid.String()
 }
-
-func GenPortName(name string) string {
-	return "Port" + name
-}

+ 0 - 111
test/e2e/mock/echoserver/echoserver.go

@@ -1,111 +0,0 @@
-package echoserver
-
-import (
-	"fmt"
-	"net"
-	"strings"
-
-	fnet "github.com/fatedier/frp/pkg/util/net"
-)
-
-type ServerType string
-
-const (
-	TCP  ServerType = "tcp"
-	UDP  ServerType = "udp"
-	Unix ServerType = "unix"
-)
-
-type Options struct {
-	Type              ServerType
-	BindAddr          string
-	BindPort          int32
-	RepeatNum         int
-	SpecifiedResponse string
-}
-
-type Server struct {
-	opt Options
-
-	l net.Listener
-}
-
-func New(opt Options) *Server {
-	if opt.Type == "" {
-		opt.Type = TCP
-	}
-	if opt.BindAddr == "" {
-		opt.BindAddr = "127.0.0.1"
-	}
-	if opt.RepeatNum <= 0 {
-		opt.RepeatNum = 1
-	}
-	return &Server{
-		opt: opt,
-	}
-}
-
-func (s *Server) GetOptions() Options {
-	return s.opt
-}
-
-func (s *Server) Run() error {
-	if err := s.initListener(); err != nil {
-		return err
-	}
-
-	go func() {
-		for {
-			c, err := s.l.Accept()
-			if err != nil {
-				return
-			}
-			go s.handle(c)
-		}
-	}()
-	return nil
-}
-
-func (s *Server) Close() error {
-	if s.l != nil {
-		return s.l.Close()
-	}
-	return nil
-}
-
-func (s *Server) initListener() (err error) {
-	switch s.opt.Type {
-	case TCP:
-		s.l, err = net.Listen("tcp", fmt.Sprintf("%s:%d", s.opt.BindAddr, s.opt.BindPort))
-	case UDP:
-		s.l, err = fnet.ListenUDP(s.opt.BindAddr, int(s.opt.BindPort))
-	case Unix:
-		s.l, err = net.Listen("unix", s.opt.BindAddr)
-	default:
-		return fmt.Errorf("unknown server type: %s", s.opt.Type)
-	}
-	if err != nil {
-		return
-	}
-	return nil
-}
-
-func (s *Server) handle(c net.Conn) {
-	defer c.Close()
-
-	buf := make([]byte, 2048)
-	for {
-		n, err := c.Read(buf)
-		if err != nil {
-			return
-		}
-
-		var response string
-		if len(s.opt.SpecifiedResponse) > 0 {
-			response = s.opt.SpecifiedResponse
-		} else {
-			response = strings.Repeat(string(buf[:n]), s.opt.RepeatNum)
-		}
-		c.Write([]byte(response))
-	}
-}

+ 142 - 0
test/e2e/mock/server/server.go

@@ -0,0 +1,142 @@
+package server
+
+import (
+	"fmt"
+	"net"
+
+	libnet "github.com/fatedier/frp/pkg/util/net"
+)
+
+type ServerType string
+
+const (
+	TCP  ServerType = "tcp"
+	UDP  ServerType = "udp"
+	Unix ServerType = "unix"
+)
+
+type Server struct {
+	netType     ServerType
+	bindAddr    string
+	bindPort    int
+	respContent []byte
+	bufSize     int64
+
+	echoMode bool
+
+	l net.Listener
+}
+
+type Option func(*Server) *Server
+
+func New(netType ServerType, options ...Option) *Server {
+	s := &Server{
+		netType:  netType,
+		bindAddr: "127.0.0.1",
+		bufSize:  2048,
+	}
+
+	for _, option := range options {
+		s = option(s)
+	}
+	return s
+}
+
+func WithBindAddr(addr string) Option {
+	return func(s *Server) *Server {
+		s.bindAddr = addr
+		return s
+	}
+}
+
+func WithBindPort(port int) Option {
+	return func(s *Server) *Server {
+		s.bindPort = port
+		return s
+	}
+}
+
+func WithRespContent(content []byte) Option {
+	return func(s *Server) *Server {
+		s.respContent = content
+		return s
+	}
+}
+
+func WithBufSize(bufSize int64) Option {
+	return func(s *Server) *Server {
+		s.bufSize = bufSize
+		return s
+	}
+}
+
+func WithEchoMode(echoMode bool) Option {
+	return func(s *Server) *Server {
+		s.echoMode = echoMode
+		return s
+	}
+}
+
+func (s *Server) Run() error {
+	if err := s.initListener(); err != nil {
+		return err
+	}
+
+	go func() {
+		for {
+			c, err := s.l.Accept()
+			if err != nil {
+				return
+			}
+			go s.handle(c)
+		}
+	}()
+	return nil
+}
+
+func (s *Server) Close() error {
+	if s.l != nil {
+		return s.l.Close()
+	}
+	return nil
+}
+
+func (s *Server) initListener() (err error) {
+	switch s.netType {
+	case TCP:
+		s.l, err = net.Listen("tcp", fmt.Sprintf("%s:%d", s.bindAddr, s.bindPort))
+	case UDP:
+		s.l, err = libnet.ListenUDP(s.bindAddr, s.bindPort)
+	case Unix:
+		s.l, err = net.Listen("unix", s.bindAddr)
+	default:
+		return fmt.Errorf("unknown server type: %s", s.netType)
+	}
+	return err
+}
+
+func (s *Server) handle(c net.Conn) {
+	defer c.Close()
+
+	buf := make([]byte, s.bufSize)
+	for {
+		n, err := c.Read(buf)
+		if err != nil {
+			return
+		}
+
+		if s.echoMode {
+			c.Write(buf[:n])
+		} else {
+			c.Write(s.respContent)
+		}
+	}
+}
+
+func (s *Server) BindAddr() string {
+	return s.bindAddr
+}
+
+func (s *Server) BindPort() int {
+	return s.bindPort
+}

+ 58 - 2
test/e2e/pkg/port/port.go

@@ -3,6 +3,7 @@ package port
 import (
 	"fmt"
 	"net"
+	"sync"
 
 	"k8s.io/apimachinery/pkg/util/sets"
 )
@@ -10,6 +11,7 @@ import (
 type Allocator struct {
 	reserved sets.Int
 	used     sets.Int
+	mu       sync.Mutex
 }
 
 // NewAllocator return a port allocator for testing.
@@ -29,13 +31,31 @@ func NewAllocator(from int, to int, mod int, index int) *Allocator {
 }
 
 func (pa *Allocator) Get() int {
+	return pa.GetByName("")
+}
+
+func (pa *Allocator) GetByName(portName string) int {
+	var builder *nameBuilder
+	if portName == "" {
+		builder = &nameBuilder{}
+	} else {
+		var err error
+		builder, err = unmarshalFromName(portName)
+		if err != nil {
+			fmt.Println(err, portName)
+			return 0
+		}
+	}
+
+	pa.mu.Lock()
+	defer pa.mu.Unlock()
+
 	for i := 0; i < 10; i++ {
-		port, _ := pa.reserved.PopAny()
+		port := pa.getByRange(builder.rangePortFrom, builder.rangePortTo)
 		if port == 0 {
 			return 0
 		}
 
-		// TODO: Distinguish between TCP and UDP
 		l, err := net.Listen("tcp", fmt.Sprintf("127.0.0.1:%d", port))
 		if err != nil {
 			// Maybe not controlled by us, mark it used.
@@ -43,13 +63,49 @@ func (pa *Allocator) Get() int {
 			continue
 		}
 		l.Close()
+
+		udpAddr, err := net.ResolveUDPAddr("udp", fmt.Sprintf("127.0.0.1:%d", port))
+		if err != nil {
+			continue
+		}
+		udpConn, err := net.ListenUDP("udp", udpAddr)
+		if err != nil {
+			// Maybe not controlled by us, mark it used.
+			pa.used.Insert(port)
+			continue
+		}
+		udpConn.Close()
+
 		pa.used.Insert(port)
 		return port
 	}
 	return 0
 }
 
+func (pa *Allocator) getByRange(from, to int) int {
+	if from <= 0 {
+		port, _ := pa.reserved.PopAny()
+		return port
+	}
+
+	// choose a random port between from - to
+	ports := pa.reserved.UnsortedList()
+	for _, port := range ports {
+		if port >= from && port <= to {
+			return port
+		}
+	}
+	return 0
+}
+
 func (pa *Allocator) Release(port int) {
+	if port <= 0 {
+		return
+	}
+
+	pa.mu.Lock()
+	defer pa.mu.Unlock()
+
 	if pa.used.Has(port) {
 		pa.used.Delete(port)
 		pa.reserved.Insert(port)

+ 69 - 0
test/e2e/pkg/port/util.go

@@ -0,0 +1,69 @@
+package port
+
+import (
+	"fmt"
+	"strconv"
+	"strings"
+)
+
+const (
+	NameDelimiter = "_"
+)
+
+type NameOption func(*nameBuilder) *nameBuilder
+
+type nameBuilder struct {
+	name          string
+	rangePortFrom int
+	rangePortTo   int
+}
+
+func unmarshalFromName(name string) (*nameBuilder, error) {
+	var builder nameBuilder
+	arrs := strings.Split(name, NameDelimiter)
+	switch len(arrs) {
+	case 2:
+		builder.name = arrs[1]
+	case 4:
+		builder.name = arrs[1]
+		if fromPort, err := strconv.Atoi(arrs[2]); err != nil {
+			return nil, fmt.Errorf("error range port from")
+		} else {
+			builder.rangePortFrom = fromPort
+		}
+		if toPort, err := strconv.Atoi(arrs[3]); err != nil {
+			return nil, fmt.Errorf("error range port to")
+		} else {
+			builder.rangePortTo = toPort
+		}
+	default:
+		return nil, fmt.Errorf("error port name format")
+	}
+	return &builder, nil
+}
+
+func (builder *nameBuilder) String() string {
+	name := fmt.Sprintf("Port%s%s", NameDelimiter, builder.name)
+	if builder.rangePortFrom > 0 && builder.rangePortTo > 0 && builder.rangePortTo > builder.rangePortFrom {
+		name += fmt.Sprintf("%s%d%s%d", NameDelimiter, builder.rangePortFrom, NameDelimiter, builder.rangePortTo)
+	}
+	return name
+}
+
+func WithRangePorts(from, to int) NameOption {
+	return func(builder *nameBuilder) *nameBuilder {
+		builder.rangePortFrom = from
+		builder.rangePortTo = to
+		return builder
+	}
+}
+
+func GenName(name string, options ...NameOption) string {
+	name = strings.ReplaceAll(name, "-", "")
+	name = strings.ReplaceAll(name, "_", "")
+	builder := &nameBuilder{name: name}
+	for _, option := range options {
+		builder = option(builder)
+	}
+	return builder.String()
+}

+ 107 - 8
test/e2e/pkg/request/request.go

@@ -4,12 +4,108 @@ import (
 	"fmt"
 	"net"
 	"time"
+
+	libnet "github.com/fatedier/golib/net"
 )
 
-func SendTCPRequest(port int, content []byte, timeout time.Duration) (string, error) {
+type Request struct {
+	protocol  string
+	addr      string
+	port      int
+	body      []byte
+	timeout   time.Duration
+	proxyURL  string
+	proxyHost string
+}
+
+func New() *Request {
+	return &Request{
+		protocol: "tcp",
+	}
+}
+
+func (r *Request) Protocol(protocol string) *Request {
+	r.protocol = protocol
+	return r
+}
+
+func (r *Request) TCP() *Request {
+	r.protocol = "tcp"
+	return r
+}
+
+func (r *Request) UDP() *Request {
+	r.protocol = "udp"
+	return r
+}
+
+func (r *Request) Proxy(url, host string) *Request {
+	r.proxyURL = url
+	r.proxyHost = host
+	return r
+}
+
+func (r *Request) Addr(addr string) *Request {
+	r.addr = addr
+	return r
+}
+
+func (r *Request) Port(port int) *Request {
+	r.port = port
+	return r
+}
+
+func (r *Request) Timeout(timeout time.Duration) *Request {
+	r.timeout = timeout
+	return r
+}
+
+func (r *Request) Body(content []byte) *Request {
+	r.body = content
+	return r
+}
+
+func (r *Request) Do() ([]byte, error) {
+	var (
+		conn net.Conn
+		err  error
+	)
+	if len(r.proxyURL) > 0 {
+		if r.protocol != "tcp" {
+			return nil, fmt.Errorf("only tcp protocol is allowed for proxy")
+		}
+		conn, err = libnet.DialTcpByProxy(r.proxyURL, r.proxyHost)
+		if err != nil {
+			return nil, err
+		}
+	} else {
+		if r.addr == "" {
+			r.addr = fmt.Sprintf("127.0.0.1:%d", r.port)
+		}
+		switch r.protocol {
+		case "tcp":
+			conn, err = net.Dial("tcp", r.addr)
+		case "udp":
+			conn, err = net.Dial("udp", r.addr)
+		default:
+			return nil, fmt.Errorf("invalid protocol")
+		}
+		if err != nil {
+			return nil, err
+		}
+	}
+
+	defer conn.Close()
+	if r.timeout > 0 {
+		conn.SetDeadline(time.Now().Add(r.timeout))
+	}
+	return sendRequestByConn(conn, r.body)
+}
+
+func SendTCPRequest(port int, content []byte, timeout time.Duration) ([]byte, error) {
 	c, err := net.Dial("tcp", fmt.Sprintf("127.0.0.1:%d", port))
 	if err != nil {
-		return "", fmt.Errorf("connect to tcp server error: %v", err)
+		return nil, fmt.Errorf("connect to tcp server error: %v", err)
 	}
 	defer c.Close()
 
@@ -17,10 +113,10 @@ func SendTCPRequest(port int, content []byte, timeout time.Duration) (string, er
 	return sendRequestByConn(c, content)
 }
 
-func SendUDPRequest(port int, content []byte, timeout time.Duration) (string, error) {
+func SendUDPRequest(port int, content []byte, timeout time.Duration) ([]byte, error) {
 	c, err := net.Dial("udp", fmt.Sprintf("127.0.0.1:%d", port))
 	if err != nil {
-		return "", fmt.Errorf("connect to udp server error:  %v", err)
+		return nil, fmt.Errorf("connect to udp server error:  %v", err)
 	}
 	defer c.Close()
 
@@ -28,13 +124,16 @@ func SendUDPRequest(port int, content []byte, timeout time.Duration) (string, er
 	return sendRequestByConn(c, content)
 }
 
-func sendRequestByConn(c net.Conn, content []byte) (string, error) {
-	c.Write(content)
+func sendRequestByConn(c net.Conn, content []byte) ([]byte, error) {
+	_, err := c.Write(content)
+	if err != nil {
+		return nil, fmt.Errorf("write error: %v", err)
+	}
 
 	buf := make([]byte, 2048)
 	n, err := c.Read(buf)
 	if err != nil {
-		return "", fmt.Errorf("read error: %v", err)
+		return nil, fmt.Errorf("read error: %v", err)
 	}
-	return string(buf[:n]), nil
+	return buf[:n], nil
 }

+ 10 - 10
test/e2e/plugin/client_plugins.go

@@ -2,16 +2,14 @@ package plugin
 
 import (
 	"fmt"
-	"time"
 
 	"github.com/fatedier/frp/test/e2e/framework"
 	"github.com/fatedier/frp/test/e2e/framework/consts"
+	"github.com/fatedier/frp/test/e2e/pkg/port"
 
 	. "github.com/onsi/ginkgo"
 )
 
-var connTimeout = 2 * time.Second
-
 var _ = Describe("[Feature: Client-Plugins]", func() {
 	f := framework.NewDefaultFramework()
 
@@ -37,21 +35,21 @@ var _ = Describe("[Feature: Client-Plugins]", func() {
 			}{
 				{
 					proxyName: "normal",
-					portName:  framework.GenPortName("Normal"),
+					portName:  port.GenName("Normal"),
 				},
 				{
 					proxyName:   "with-encryption",
-					portName:    framework.GenPortName("WithEncryption"),
+					portName:    port.GenName("WithEncryption"),
 					extraConfig: "use_encryption = true",
 				},
 				{
 					proxyName:   "with-compression",
-					portName:    framework.GenPortName("WithCompression"),
+					portName:    port.GenName("WithCompression"),
 					extraConfig: "use_compression = true",
 				},
 				{
 					proxyName: "with-encryption-and-compression",
-					portName:  framework.GenPortName("WithEncryptionAndCompression"),
+					portName:  port.GenName("WithEncryptionAndCompression"),
 					extraConfig: `
 					use_encryption = true
 					use_compression = true
@@ -67,9 +65,11 @@ var _ = Describe("[Feature: Client-Plugins]", func() {
 			f.RunProcesses([]string{serverConf}, []string{clientConf})
 
 			for _, test := range tests {
-				framework.ExpectTCPRequest(f.UsedPorts[test.portName],
-					[]byte(consts.TestString), []byte(consts.TestString),
-					connTimeout, test.proxyName)
+				framework.ExpectResponse(
+					framework.NewRequest().Port(f.PortByName(test.portName)),
+					[]byte(consts.TestString),
+					test.proxyName,
+				)
 			}
 		})
 	})

+ 0 - 2
test/e2e/suites.go

@@ -6,11 +6,9 @@ package e2e
 // and then the function that only runs on the first Ginkgo node.
 func CleanupSuite() {
 	// Run on all Ginkgo nodes
-	// TODO
 }
 
 // AfterSuiteActions are actions that are run on ginkgo's SynchronizedAfterSuite
 func AfterSuiteActions() {
 	// Run only Ginkgo on node 1
-	// TODO
 }

+ 0 - 7
tests/ci/auto_test_frpc.ini

@@ -132,13 +132,6 @@ custom_domains = test6.frp.com
 host_header_rewrite = test6.frp.com
 header_X-From-Where = frp
 
-[tcpmuxhttpconnect]
-type = tcpmux
-multiplexer = httpconnect
-local_ip = 127.0.0.1
-local_port = 10701
-custom_domains = tunnel1
-
 [wildcard_http]
 type = http
 local_ip = 127.0.0.1

+ 0 - 46
tests/ci/normal_test.go

@@ -5,7 +5,6 @@ import (
 	"net/http"
 	"net/url"
 	"os"
-	"strings"
 	"testing"
 	"time"
 
@@ -13,7 +12,6 @@ import (
 	"github.com/stretchr/testify/assert"
 
 	"github.com/fatedier/frp/client/proxy"
-	"github.com/fatedier/frp/server/ports"
 	"github.com/fatedier/frp/tests/consts"
 	"github.com/fatedier/frp/tests/mock"
 	"github.com/fatedier/frp/tests/util"
@@ -155,17 +153,6 @@ func TestHTTP(t *testing.T) {
 	}
 }
 
-func TestTCPMux(t *testing.T) {
-	assert := assert.New(t)
-
-	conn, err := gnet.DialTcpByProxy(fmt.Sprintf("http://%s:%d", "127.0.0.1", consts.TEST_TCP_MUX_FRP_PORT), "tunnel1")
-	if assert.NoError(err) {
-		res, err := util.SendTCPMsgByConn(conn, consts.TEST_TCP_ECHO_STR)
-		assert.NoError(err)
-		assert.Equal(consts.TEST_TCP_ECHO_STR, res)
-	}
-}
-
 func TestWebSocket(t *testing.T) {
 	assert := assert.New(t)
 
@@ -182,39 +169,6 @@ func TestWebSocket(t *testing.T) {
 	assert.Equal(consts.TEST_HTTP_NORMAL_STR, string(msg))
 }
 
-func TestAllowPorts(t *testing.T) {
-	assert := assert.New(t)
-	// Port not allowed
-	status, err := util.GetProxyStatus(consts.ADMIN_ADDR, consts.ADMIN_USER, consts.ADMIN_PWD, consts.ProxyTCPPortNotAllowed)
-	if assert.NoError(err) {
-		assert.Equal(proxy.ProxyPhaseStartErr, status.Status)
-		assert.True(strings.Contains(status.Err, ports.ErrPortNotAllowed.Error()))
-	}
-
-	status, err = util.GetProxyStatus(consts.ADMIN_ADDR, consts.ADMIN_USER, consts.ADMIN_PWD, consts.ProxyUDPPortNotAllowed)
-	if assert.NoError(err) {
-		assert.Equal(proxy.ProxyPhaseStartErr, status.Status)
-		assert.True(strings.Contains(status.Err, ports.ErrPortNotAllowed.Error()))
-	}
-
-	status, err = util.GetProxyStatus(consts.ADMIN_ADDR, consts.ADMIN_USER, consts.ADMIN_PWD, consts.ProxyTCPPortUnavailable)
-	if assert.NoError(err) {
-		assert.Equal(proxy.ProxyPhaseStartErr, status.Status)
-		assert.True(strings.Contains(status.Err, ports.ErrPortUnAvailable.Error()))
-	}
-
-	// Port normal
-	status, err = util.GetProxyStatus(consts.ADMIN_ADDR, consts.ADMIN_USER, consts.ADMIN_PWD, consts.ProxyTCPPortNormal)
-	if assert.NoError(err) {
-		assert.Equal(proxy.ProxyPhaseRunning, status.Status)
-	}
-
-	status, err = util.GetProxyStatus(consts.ADMIN_ADDR, consts.ADMIN_USER, consts.ADMIN_PWD, consts.ProxyUDPPortNormal)
-	if assert.NoError(err) {
-		assert.Equal(proxy.ProxyPhaseRunning, status.Status)
-	}
-}
-
 func TestRandomPort(t *testing.T) {
 	assert := assert.New(t)
 	// tcp

+ 3 - 0
web/frpc/src/components/Overview.vue

@@ -53,6 +53,9 @@
                 for (let s of json.stcp) {
                     this.status.push(s)
                 }
+                for (let s of json.sudp) {
+                    this.status.push(s)
+                }
                 for (let s of json.xtcp) {
                     this.status.push(s)
                 }

+ 1 - 0
web/frps/src/App.vue

@@ -17,6 +17,7 @@
                             <el-menu-item index="/proxies/http">HTTP</el-menu-item>
                             <el-menu-item index="/proxies/https">HTTPS</el-menu-item>
                             <el-menu-item index="/proxies/stcp">STCP</el-menu-item>
+                            <el-menu-item index="/proxies/sudp">SUDP</el-menu-item>
                         </el-submenu>
                         <el-menu-item index="">Help</el-menu-item>
                     </el-menu>

+ 3 - 0
web/frps/src/components/Overview.vue

@@ -122,6 +122,9 @@
                     if (json.proxy_type_count.stcp != null) {
                         this.proxy_counts += json.proxy_type_count.stcp
                     }
+                    if (json.proxy_type_count.sudp != null) {
+                        this.proxy_counts += json.proxy_type_count.sudp
+                    }
                     if (json.proxy_type_count.xtcp != null) {
                         this.proxy_counts += json.proxy_type_count.xtcp
                     }

+ 116 - 0
web/frps/src/components/ProxiesSudp.vue

@@ -0,0 +1,116 @@
+<template>
+  <div>
+    <el-table :data="proxies" :default-sort="{prop: 'name', order: 'ascending'}" style="width: 100%">
+      <el-table-column type="expand">
+        <template slot-scope="props">
+          <el-popover
+            ref="popover4"
+            placement="right"
+            width="600"
+  		  style="margin-left:0px"
+            trigger="click">
+            <my-traffic-chart :proxy_name="props.row.name"></my-traffic-chart>
+          </el-popover>
+  
+          <el-button v-popover:popover4 type="primary" size="small" icon="view" :name="props.row.name" style="margin-bottom:10px" @click="fetchData2">Traffic Statistics</el-button>
+  
+          <el-form label-position="left" inline class="demo-table-expand">
+            <el-form-item label="Name">
+              <span>{{ props.row.name }}</span>
+            </el-form-item>
+            <el-form-item label="Type">
+              <span>{{ props.row.type }}</span>
+            </el-form-item>
+            <el-form-item label="Encryption">
+              <span>{{ props.row.encryption }}</span>
+            </el-form-item>
+            <el-form-item label="Compression">
+              <span>{{ props.row.compression }}</span>
+            </el-form-item>
+            <el-form-item label="Last Start">
+              <span>{{ props.row.last_start_time }}</span>
+            </el-form-item>
+            <el-form-item label="Last Close">
+              <span>{{ props.row.last_close_time }}</span>
+            </el-form-item>
+        </el-form>
+        </template>
+    </el-table-column>
+    <el-table-column
+      label="Name"
+      prop="name"
+      sortable>
+    </el-table-column>
+    <el-table-column
+      label="Connections"
+      prop="conns"
+      sortable>
+    </el-table-column>
+    <el-table-column
+      label="Traffic In"
+      prop="traffic_in"
+      :formatter="formatTrafficIn"
+      sortable>
+    </el-table-column>
+    <el-table-column
+      label="Traffic Out"
+      prop="traffic_out"
+      :formatter="formatTrafficOut"
+      sortable>
+    </el-table-column>
+    <el-table-column
+      label="status"
+      prop="status"
+      sortable>
+      <template slot-scope="scope">
+        <el-tag type="success" v-if="scope.row.status === 'online'">{{ scope.row.status }}</el-tag>
+        <el-tag type="danger" v-else>{{ scope.row.status }}</el-tag>
+      </template>
+    </el-table-column>
+  </el-table>
+</div>
+</template>
+
+<script>
+  import Humanize from 'humanize-plus'
+  import Traffic from './Traffic.vue'
+  import { SudpProxy } from '../utils/proxy.js'
+  export default {
+    data() {
+      return {
+        proxies: null
+      }
+    },
+    created() {
+      this.fetchData()
+    },
+    watch: {
+      '$route': 'fetchData'
+    },
+    methods: {
+      formatTrafficIn(row, column) {
+        return Humanize.fileSize(row.traffic_in)
+      },
+      formatTrafficOut(row, column) {
+        return Humanize.fileSize(row.traffic_out)
+      },
+      fetchData() {
+        fetch('../api/proxy/sudp', {credentials: 'include'})
+          .then(res => {
+            return res.json()
+          }).then(json => {
+            this.proxies = new Array()
+            for (let proxyStats of json.proxies) {
+              this.proxies.push(new SudpProxy(proxyStats))
+            }
+          })
+      }
+    },
+    components: {
+        'my-traffic-chart': Traffic
+    }
+  }
+</script>
+
+<style>
+</style>

+ 5 - 0
web/frps/src/router/index.js

@@ -6,6 +6,7 @@ import ProxiesUdp from '../components/ProxiesUdp.vue'
 import ProxiesHttp from '../components/ProxiesHttp.vue'
 import ProxiesHttps from '../components/ProxiesHttps.vue'
 import ProxiesStcp from '../components/ProxiesStcp.vue'
+import ProxiesSudp from '../components/ProxiesSudp.vue'
 
 Vue.use(Router)
 
@@ -34,5 +35,9 @@ export default new Router({
         path: '/proxies/stcp',
         name: 'ProxiesStcp',
         component: ProxiesStcp
+    }, {
+        path: '/proxies/sudp',
+        name: 'ProxiesSudp',
+        component: ProxiesSudp
     }]
 })

+ 24 - 37
web/frps/src/utils/chart.js

@@ -48,24 +48,6 @@ function DrawTrafficChart(elementId, trafficIn, trafficOut) {
 }
 
 function DrawProxyChart(elementId, serverInfo) {
-    if (serverInfo.proxy_type_count.tcp == null) {
-        serverInfo.proxy_type_count.tcp = 0
-    }
-    if (serverInfo.proxy_type_count.udp == null) {
-        serverInfo.proxy_type_count.udp = 0
-    }
-    if (serverInfo.proxy_type_count.http == null) {
-        serverInfo.proxy_type_count.http = 0
-    }
-    if (serverInfo.proxy_type_count.https == null) {
-        serverInfo.proxy_type_count.https = 0
-    }
-    if (serverInfo.proxy_type_count.stcp == null) {
-        serverInfo.proxy_type_count.stcp = 0
-    }
-    if (serverInfo.proxy_type_count.xtcp == null) {
-        serverInfo.proxy_type_count.xtcp = 0
-    }
     let myChart = echarts.init(document.getElementById(elementId), 'macarons')
     myChart.showLoading()
 
@@ -85,25 +67,7 @@ function DrawProxyChart(elementId, serverInfo) {
             type: 'pie',
             radius: '55%',
             center: ['50%', '60%'],
-            data: [{
-                value: serverInfo.proxy_type_count.tcp,
-                name: 'TCP'
-            }, {
-                value: serverInfo.proxy_type_count.udp,
-                name: 'UDP'
-            }, {
-                value: serverInfo.proxy_type_count.http,
-                name: 'HTTP'
-            }, {
-                value: serverInfo.proxy_type_count.https,
-                name: 'HTTPS'
-            }, {
-                value: serverInfo.proxy_type_count.stcp,
-                name: 'STCP'
-            }, {
-                value: serverInfo.proxy_type_count.xtcp,
-                name: 'XTCP'
-            }],
+            data: [],
             itemStyle: {
                 emphasis: {
                     shadowBlur: 10,
@@ -113,6 +77,29 @@ function DrawProxyChart(elementId, serverInfo) {
             }
         }]
     };
+
+    if (serverInfo.proxy_type_count.tcp != null && serverInfo.proxy_type_count.tcp != 0) {
+        option.series[0].data.push({value: serverInfo.proxy_type_count.tcp, name: 'TCP'})
+    }
+    if (serverInfo.proxy_type_count.udp != null && serverInfo.proxy_type_count.udp != 0) {
+        option.series[0].data.push({value: serverInfo.proxy_type_count.udp, name: 'UDP'})
+    }
+    if (serverInfo.proxy_type_count.http != null && serverInfo.proxy_type_count.http != 0) {
+        option.series[0].data.push({value: serverInfo.proxy_type_count.http, name: 'HTTP'})
+    }
+    if (serverInfo.proxy_type_count.https != null && serverInfo.proxy_type_count.https != 0) {
+        option.series[0].data.push({value: serverInfo.proxy_type_count.https, name: 'HTTPS'})
+    }
+    if (serverInfo.proxy_type_count.stcp != null && serverInfo.proxy_type_count.stcp != 0) {
+        option.series[0].data.push({value: serverInfo.proxy_type_count.stcp, name: 'STCP'})
+    }
+    if (serverInfo.proxy_type_count.sudp != null && serverInfo.proxy_type_count.sudp != 0) {
+        option.series[0].data.push({value: serverInfo.proxy_type_count.sudp, name: 'SUDP'})
+    }
+    if (serverInfo.proxy_type_count.xtcp != null && serverInfo.proxy_type_count.xtcp != 0) {
+        option.series[0].data.push({value: serverInfo.proxy_type_count.xtcp, name: 'XTCP'})
+    }
+
     myChart.setOption(option);
     myChart.hideLoading()
 }

+ 8 - 1
web/frps/src/utils/proxy.js

@@ -94,4 +94,11 @@ class StcpProxy extends BaseProxy {
     }
 }
 
-export {BaseProxy, TcpProxy, UdpProxy, HttpProxy, HttpsProxy, StcpProxy}
+class SudpProxy extends BaseProxy {
+    constructor(proxyStats) {
+        super(proxyStats)
+        this.type = "sudp"
+    }
+}
+
+export {BaseProxy, TcpProxy, UdpProxy, HttpProxy, HttpsProxy, StcpProxy, SudpProxy}

Энэ ялгаанд хэт олон файл өөрчлөгдсөн тул зарим файлыг харуулаагүй болно