Browse Source

Merge pull request #629 from fatedier/new

new feature
fatedier 7 years ago
parent
commit
5d239127bb

+ 0 - 3
Makefile

@@ -53,6 +53,3 @@ clean:
 	rm -f ./bin/frpc
 	rm -f ./bin/frps
 	cd ./tests && ./clean_test.sh && cd -
-
-save:
-	godep save ./...

+ 43 - 5
README.md

@@ -20,6 +20,7 @@ frp is a fast reverse proxy to help you expose a local server behind a NAT or fi
     * [Visit your web service in LAN by custom domains](#visit-your-web-service-in-lan-by-custom-domains)
     * [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)
     * [Expose your service in security](#expose-your-service-in-security)
     * [P2P Mode](#p2p-mode)
     * [Connect website through frpc's network](#connect-website-through-frpcs-network)
@@ -214,6 +215,32 @@ Configure frps same as above.
 
   `curl http://x.x.x.x:6000/version`
 
+### Expose a simple http file server
+
+A simple way to visit files in the LAN.
+
+Configure frps same as above.
+
+1. Start frpc with configurations:
+
+  ```ini
+  # frpc.ini
+  [common]
+  server_addr = x.x.x.x
+  server_port = 7000
+
+  [test_static_file]
+  type = tcp
+  remote_port = 6000
+  plugin = static_file
+  plugin_local_path = /tmp/file
+  plugin_strip_prefix = static
+  plugin_http_user = abc
+  plugin_http_passwd = abc
+  ```
+
+2. Visit `http://x.x.x.x:6000/static/` by your browser, set correct user and password, so you can see files in `/tmp/file`.
+
 ### Expose your service in security
 
 For some services, if expose them to the public network directly will be a security risk.
@@ -576,11 +603,26 @@ server_port = 7000
 http_proxy = http://user:pwd@192.168.1.128:8080
 ```
 
+### Range ports mapping
+
+Proxy name has prefix `range:` will support mapping range ports.
+
+```ini
+# frpc.ini
+[range:test_tcp]
+type = tcp
+local_ip = 127.0.0.1
+local_port = 6000-6006,6007
+remote_port = 6000-6006,6007
+```
+
+frpc will generate 6 proxies like `test_tcp_0, test_tcp_1 ... test_tcp_5`.
+
 ### Plugin
 
 frpc only forward request to local tcp or udp port by default.
 
-Plugin is used for providing rich features. There are built-in plugins such as **unix_domain_socket**, **http_proxy**, **socks5** and you can see [example usage](#example-usage).
+Plugin is used for providing rich features. There are built-in plugins such as `unix_domain_socket`, `http_proxy`, `socks5`, `static_file` and you can see [example usage](#example-usage).
 
 Specify which plugin to use by `plugin` parameter. Configuration parameters of plugin should be started with `plugin_`. `local_ip` and `local_port` is useless for plugin.
 
@@ -598,17 +640,13 @@ plugin_http_passwd = abc
 
 `plugin_http_user` and `plugin_http_passwd` are configuration parameters used in `http_proxy` plugin.
 
-
 ## Development Plan
 
 * Log http request information in frps.
 * Direct reverse proxy, like haproxy.
 * Load balance to different service in frpc.
-* Frpc can directly be a webserver for static files.
-* P2p communicate by making udp hole to penetrate NAT.
 * kubernetes ingress support.
 
-
 ## Contributing
 
 Interested in getting involved? We would like to help you!

+ 52 - 5
README_zh.md

@@ -18,6 +18,7 @@ frp 是一个可用于内网穿透的高性能的反向代理应用,支持 tcp
     * [通过自定义域名访问部署于内网的 web 服务](#通过自定义域名访问部署于内网的-web-服务)
     * [转发 DNS 查询请求](#转发-dns-查询请求)
     * [转发 Unix域套接字](#转发-unix域套接字)
+    * [对外提供简单的文件访问服务](#对外提供简单的文件访问服务)
     * [安全地暴露内网服务](#安全地暴露内网服务)
     * [点对点内网穿透](#点对点内网穿透)
     * [通过 frpc 所在机器访问外网](#通过-frpc-所在机器访问外网)
@@ -39,6 +40,7 @@ frp 是一个可用于内网穿透的高性能的反向代理应用,支持 tcp
     * [自定义二级域名](#自定义二级域名)
     * [URL 路由](#url-路由)
     * [通过代理连接 frps](#通过代理连接-frps)
+    * [范围端口映射](#范围端口映射)
     * [插件](#插件)
 * [开发计划](#开发计划)
 * [为 frp 做贡献](#为-frp-做贡献)
@@ -192,11 +194,11 @@ DNS 查询请求通常使用 UDP 协议,frp 支持对内网 UDP 服务的穿
 
 ### 转发 Unix域套接字
 
-通过 tcp 端口访问内网的 unix域套接字(和 docker daemon 通信)。
+通过 tcp 端口访问内网的 unix域套接字(例如和 docker daemon 通信)。
 
 frps 的部署步骤同上。
 
-1. 启动 frpc,启用 unix_domain_socket 插件,配置如下:
+1. 启动 frpc,启用 `unix_domain_socket` 插件,配置如下:
 
   ```ini
   # frpc.ini
@@ -215,6 +217,34 @@ frps 的部署步骤同上。
 
   `curl http://x.x.x.x:6000/version`
 
+### 对外提供简单的文件访问服务
+
+通过 `static_file` 插件可以对外提供一个简单的基于 HTTP 的文件访问服务。
+
+frps 的部署步骤同上。
+
+1. 启动 frpc,启用 `static_file` 插件,配置如下:
+
+  ```ini
+  # frpc.ini
+  [common]
+  server_addr = x.x.x.x
+  server_port = 7000
+
+  [test_static_file]
+  type = tcp
+  remote_port = 6000
+  plugin = static_file
+  # 要对外暴露的文件目录
+  plugin_local_path = /tmp/file
+  # 访问 url 中会被去除的前缀,保留的内容即为要访问的文件路径
+  plugin_strip_prefix = static
+  plugin_http_user = abc
+  plugin_http_passwd = abc
+  ```
+
+2. 通过浏览器访问 `http://x.x.x.x:6000/static/` 来查看位于 `/tmp/file` 目录下的文件,会要求输入已设置好的用户名和密码。
+
 ### 安全地暴露内网服务
 
 对于某些服务来说如果直接暴露于公网上将会存在安全隐患。
@@ -609,11 +639,30 @@ server_port = 7000
 http_proxy = http://user:pwd@192.168.1.128:8080
 ```
 
+### 范围端口映射
+
+在 frpc 的配置文件中可以指定映射多个端口,目前只支持 tcp 和 udp 的类型。
+
+这一功能通过 `range:` 段落标记来实现,客户端会解析这个标记中的配置,将其拆分成多个 proxy,每一个 proxy 以数字为后缀命名。
+
+例如要映射本地 6000-6005, 6007 这6个端口,主要配置如下:
+
+```ini
+# frpc.ini
+[range:test_tcp]
+type = tcp
+local_ip = 127.0.0.1
+local_port = 6000-6006,6007
+remote_port = 6000-6006,6007
+```
+
+实际连接成功后会创建 6 个 proxy,命名为 `test_tcp_0, test_tcp_1 ... test_tcp_5`。
+
 ### 插件
 
 默认情况下,frpc 只会转发请求到本地 tcp 或 udp 端口。
 
-插件模式是为了在客户端提供更加丰富的功能,目前内置的插件有 **unix_domain_socket**、**http_proxy**、**socks5**。具体使用方式请查看[使用示例](#使用示例)。
+插件模式是为了在客户端提供更加丰富的功能,目前内置的插件有 `unix_domain_socket`、`http_proxy`、`socks5`、`static_file`。具体使用方式请查看[使用示例](#使用示例)。
 
 通过 `plugin` 指定需要使用的插件,插件的配置参数都以 `plugin_` 开头。使用插件后 `local_ip` 和 `local_port` 不再需要配置。
 
@@ -638,8 +687,6 @@ plugin_http_passwd = abc
 * frps 记录 http 请求日志。
 * frps 支持直接反向代理,类似 haproxy。
 * frpc 支持负载均衡到后端不同服务。
-* frpc 支持直接作为 webserver 访问指定静态页面。
-* 支持 udp 打洞的方式,提供两边内网机器直接通信,流量不经过服务器转发。
 * 集成对 k8s 等平台的支持。
 
 ## 为 frp 做贡献

+ 12 - 12
client/control.go

@@ -89,8 +89,8 @@ func NewControl(svr *Service, pxyCfgs map[string]config.ProxyConf, visitorCfgs m
 	ctl := &Control{
 		svr:                svr,
 		loginMsg:           loginMsg,
-		sendCh:             make(chan msg.Message, 10),
-		readCh:             make(chan msg.Message, 10),
+		sendCh:             make(chan msg.Message, 100),
+		readCh:             make(chan msg.Message, 100),
 		closedCh:           make(chan int),
 		readerShutdown:     shutdown.New(),
 		writerShutdown:     shutdown.New(),
@@ -98,7 +98,7 @@ func NewControl(svr *Service, pxyCfgs map[string]config.ProxyConf, visitorCfgs m
 		Logger:             log.NewPrefixLogger(""),
 	}
 	ctl.pm = NewProxyManager(ctl, ctl.sendCh, "")
-	ctl.pm.Reload(pxyCfgs, visitorCfgs)
+	ctl.pm.Reload(pxyCfgs, visitorCfgs, false)
 	return ctl
 }
 
@@ -124,7 +124,7 @@ func (ctl *Control) Run() (err error) {
 
 	// start all local visitors and send NewProxy message for all configured proxies
 	ctl.pm.Reset(ctl.sendCh, ctl.runId)
-	ctl.pm.CheckAndStartProxy()
+	ctl.pm.CheckAndStartProxy([]string{ProxyStatusNew})
 	return nil
 }
 
@@ -360,20 +360,20 @@ func (ctl *Control) msgHandler() {
 // If controler is notified by closedCh, reader and writer and handler will exit, then recall these functions.
 func (ctl *Control) worker() {
 	go ctl.msgHandler()
-	go ctl.writer()
 	go ctl.reader()
+	go ctl.writer()
 
 	var err error
 	maxDelayTime := 20 * time.Second
 	delayTime := time.Second
 
-	checkInterval := 10 * time.Second
+	checkInterval := 60 * time.Second
 	checkProxyTicker := time.NewTicker(checkInterval)
 	for {
 		select {
 		case <-checkProxyTicker.C:
-			// every 10 seconds, check which proxy registered failed and reregister it to server
-			ctl.pm.CheckAndStartProxy()
+			// check which proxy registered failed and reregister it to server
+			ctl.pm.CheckAndStartProxy([]string{ProxyStatusStartErr, ProxyStatusClosed})
 		case _, ok := <-ctl.closedCh:
 			// we won't get any variable from this channel
 			if !ok {
@@ -413,8 +413,8 @@ func (ctl *Control) worker() {
 				}
 
 				// init related channels and variables
-				ctl.sendCh = make(chan msg.Message, 10)
-				ctl.readCh = make(chan msg.Message, 10)
+				ctl.sendCh = make(chan msg.Message, 100)
+				ctl.readCh = make(chan msg.Message, 100)
 				ctl.closedCh = make(chan int)
 				ctl.readerShutdown = shutdown.New()
 				ctl.writerShutdown = shutdown.New()
@@ -427,7 +427,7 @@ func (ctl *Control) worker() {
 				go ctl.reader()
 
 				// start all configured proxies
-				ctl.pm.CheckAndStartProxy()
+				ctl.pm.CheckAndStartProxy([]string{ProxyStatusNew, ProxyStatusClosed})
 
 				checkProxyTicker.Stop()
 				checkProxyTicker = time.NewTicker(checkInterval)
@@ -437,6 +437,6 @@ func (ctl *Control) worker() {
 }
 
 func (ctl *Control) reloadConf(pxyCfgs map[string]config.ProxyConf, visitorCfgs map[string]config.ProxyConf) error {
-	err := ctl.pm.Reload(pxyCfgs, visitorCfgs)
+	err := ctl.pm.Reload(pxyCfgs, visitorCfgs, true)
 	return err
 }

+ 34 - 20
client/proxy_manager.go

@@ -12,10 +12,11 @@ import (
 )
 
 const (
-	ProxyStatusNew      = "new"
-	ProxyStatusStartErr = "start error"
-	ProxyStatusRunning  = "running"
-	ProxyStatusClosed   = "closed"
+	ProxyStatusNew       = "new"
+	ProxyStatusStartErr  = "start error"
+	ProxyStatusWaitStart = "wait start"
+	ProxyStatusRunning   = "running"
+	ProxyStatusClosed    = "closed"
 )
 
 type ProxyManager struct {
@@ -69,14 +70,10 @@ func NewProxyWrapper(cfg config.ProxyConf) *ProxyWrapper {
 	}
 }
 
-func (pw *ProxyWrapper) IsRunning() bool {
+func (pw *ProxyWrapper) GetStatusStr() string {
 	pw.mu.RLock()
 	defer pw.mu.RUnlock()
-	if pw.Status == ProxyStatusRunning {
-		return true
-	} else {
-		return false
-	}
+	return pw.Status
 }
 
 func (pw *ProxyWrapper) GetStatus() *ProxyStatus {
@@ -93,6 +90,12 @@ func (pw *ProxyWrapper) GetStatus() *ProxyStatus {
 	return ps
 }
 
+func (pw *ProxyWrapper) WaitStart() {
+	pw.mu.Lock()
+	defer pw.mu.Unlock()
+	pw.Status = ProxyStatusWaitStart
+}
+
 func (pw *ProxyWrapper) Start(remoteAddr string, serverRespErr string) error {
 	if pw.pxy != nil {
 		pw.pxy.Close()
@@ -210,7 +213,8 @@ func (pm *ProxyManager) CloseProxies() {
 	}
 }
 
-func (pm *ProxyManager) CheckAndStartProxy() {
+// pxyStatus: check and start proxies in which status
+func (pm *ProxyManager) CheckAndStartProxy(pxyStatus []string) {
 	pm.mu.RLock()
 	defer pm.mu.RUnlock()
 	if pm.closed {
@@ -219,13 +223,18 @@ func (pm *ProxyManager) CheckAndStartProxy() {
 	}
 
 	for _, pxy := range pm.proxies {
-		if !pxy.IsRunning() {
-			var newProxyMsg msg.NewProxy
-			pxy.Cfg.UnMarshalToMsg(&newProxyMsg)
-			err := pm.sendMsg(&newProxyMsg)
-			if err != nil {
-				pm.Warn("[%s] proxy send NewProxy message error")
-				return
+		status := pxy.GetStatusStr()
+		for _, s := range pxyStatus {
+			if status == s {
+				var newProxyMsg msg.NewProxy
+				pxy.Cfg.UnMarshalToMsg(&newProxyMsg)
+				err := pm.sendMsg(&newProxyMsg)
+				if err != nil {
+					pm.Warn("[%s] proxy send NewProxy message error")
+					return
+				}
+				pxy.WaitStart()
+				break
 			}
 		}
 	}
@@ -245,9 +254,14 @@ func (pm *ProxyManager) CheckAndStartProxy() {
 	}
 }
 
-func (pm *ProxyManager) Reload(pxyCfgs map[string]config.ProxyConf, visitorCfgs map[string]config.ProxyConf) error {
+func (pm *ProxyManager) Reload(pxyCfgs map[string]config.ProxyConf, visitorCfgs map[string]config.ProxyConf, startNow bool) error {
 	pm.mu.Lock()
-	defer pm.mu.Unlock()
+	defer func() {
+		pm.mu.Unlock()
+		if startNow {
+			go pm.CheckAndStartProxy([]string{ProxyStatusNew})
+		}
+	}()
 	if pm.closed {
 		err := fmt.Errorf("Reload error: ProxyManager is closed now")
 		pm.Warn(err.Error())

+ 1 - 1
cmd/frpc/main.go

@@ -99,7 +99,7 @@ func main() {
 	if args["status"] != nil {
 		if args["status"].(bool) {
 			if err = CmdStatus(); err != nil {
-				fmt.Printf("frps get status error: %v\n", err)
+				fmt.Printf("frpc get status error: %v\n", err)
 				os.Exit(1)
 			} else {
 				os.Exit(0)

+ 34 - 0
conf/frpc_full.ini

@@ -73,6 +73,16 @@ local_port = 22
 # if remote_port is 0, frps will assgin a random port for you
 remote_port = 0
 
+# if you want tp expose multiple ports, add 'range:' prefix to the section name
+# frpc will generate multiple proxies such as 'tcp_port_6010', 'tcp_port_6011' and so on.
+[range:tcp_port]
+type = tcp
+local_ip = 127.0.0.1
+local_port = 6010-6020,6022,6024-6028
+remote_port = 6010-6020,6022,6024-6028
+use_encryption = false
+use_compression = false
+
 [dns]
 type = udp
 local_ip = 114.114.114.114
@@ -81,6 +91,14 @@ remote_port = 6002
 use_encryption = false
 use_compression = false
 
+[range:udp_port]
+type = udp
+local_ip = 127.0.0.1
+local_port = 6010-6020
+remote_port = 6010-6020
+use_encryption = false
+use_compression = false
+
 # Resolve your domain names to [server_addr] so you can use http://web01.yourdomain.com to browse web01 and http://web02.yourdomain.com to browse web02
 [web01]
 type = http
@@ -124,6 +142,22 @@ plugin = http_proxy
 plugin_http_user = abc
 plugin_http_passwd = abc
 
+[plugin_socks5]
+type = tcp
+remote_port = 6005
+plugin = socks5
+plugin_user = abc
+plugin_passwd = abc
+
+[plugin_static_file]
+type = tcp
+remote_port = 6006
+plugin = static_file
+plugin_local_path = /var/www/blog
+plugin_strip_prefix = static
+plugin_http_user = abc
+plugin_http_passwd = abc
+
 [secret_tcp]
 # If the type is secret tcp, remote_port is useless
 # Who want to connect local port should deploy another frpc with stcp proxy and role is visitor

+ 3 - 0
conf/frps_full.ini

@@ -52,6 +52,9 @@ privilege_allow_ports = 2000-3000,3001,3003,4000-50000
 # pool_count in each proxy will change to max_pool_count if they exceed the maximum value
 max_pool_count = 5
 
+# max ports can be used for each client, default value is 0 means no limit
+max_ports_per_client = 0
+
 # authentication_timeout means the timeout interval (seconds) when the frpc connects frps
 # if authentication_timeout is zero, the time is not verified, default is 900s
 authentication_timeout = 900

+ 68 - 6
models/config/proxy.go

@@ -22,6 +22,7 @@ import (
 
 	"github.com/fatedier/frp/models/consts"
 	"github.com/fatedier/frp/models/msg"
+	"github.com/fatedier/frp/utils/util"
 
 	ini "github.com/vaughan0/go-ini"
 )
@@ -770,6 +771,38 @@ func (cfg *XtcpProxyConf) Check() (err error) {
 	return
 }
 
+func ParseRangeSection(name string, section ini.Section) (sections map[string]ini.Section, err error) {
+	localPorts, errRet := util.ParseRangeNumbers(section["local_port"])
+	if errRet != nil {
+		err = fmt.Errorf("Parse conf error: range section [%s] local_port invalid, %v", name, errRet)
+		return
+	}
+
+	remotePorts, errRet := util.ParseRangeNumbers(section["remote_port"])
+	if errRet != nil {
+		err = fmt.Errorf("Parse conf error: range section [%s] remote_port invalid, %v", name, errRet)
+		return
+	}
+	if len(localPorts) != len(remotePorts) {
+		err = fmt.Errorf("Parse conf error: range section [%s] local ports number should be same with remote ports number", name)
+		return
+	}
+	if len(localPorts) == 0 {
+		err = fmt.Errorf("Parse conf error: range section [%s] local_port and remote_port is necessary")
+		return
+	}
+
+	sections = make(map[string]ini.Section)
+	for i, port := range localPorts {
+		subName := fmt.Sprintf("%s_%d", name, i)
+		subSection := copySection(section)
+		subSection["local_port"] = fmt.Sprintf("%d", port)
+		subSection["remote_port"] = fmt.Sprintf("%d", remotePorts[i])
+		sections[subName] = subSection
+	}
+	return
+}
+
 // if len(startProxy) is 0, start all
 // otherwise just start proxies in startProxy map
 func LoadProxyConfFromFile(prefix string, conf ini.File, startProxy map[string]struct{}) (
@@ -786,22 +819,51 @@ func LoadProxyConfFromFile(prefix string, conf ini.File, startProxy map[string]s
 	proxyConfs = make(map[string]ProxyConf)
 	visitorConfs = make(map[string]ProxyConf)
 	for name, section := range conf {
+		if name == "common" {
+			continue
+		}
+
 		_, shouldStart := startProxy[name]
-		if name != "common" && (startAll || shouldStart) {
+		if !startAll && !shouldStart {
+			continue
+		}
+
+		subSections := make(map[string]ini.Section)
+
+		if strings.HasPrefix(name, "range:") {
+			// range section
+			rangePrefix := strings.TrimSpace(strings.TrimPrefix(name, "range:"))
+			subSections, err = ParseRangeSection(rangePrefix, section)
+			if err != nil {
+				return
+			}
+		} else {
+			subSections[name] = section
+		}
+
+		for subName, subSection := range subSections {
 			// some proxy or visotr configure may be used this prefix
-			section["prefix"] = prefix
-			cfg, err := NewProxyConfFromFile(name, section)
+			subSection["prefix"] = prefix
+			cfg, err := NewProxyConfFromFile(subName, subSection)
 			if err != nil {
 				return proxyConfs, visitorConfs, err
 			}
 
-			role := section["role"]
+			role := subSection["role"]
 			if role == "visitor" {
-				visitorConfs[prefix+name] = cfg
+				visitorConfs[prefix+subName] = cfg
 			} else {
-				proxyConfs[prefix+name] = cfg
+				proxyConfs[prefix+subName] = cfg
 			}
 		}
 	}
 	return
 }
+
+func copySection(section ini.Section) (out ini.Section) {
+	out = make(ini.Section)
+	for k, v := range section {
+		out[k] = v
+	}
+	return
+}

+ 34 - 39
models/config/server_common.go

@@ -20,6 +20,8 @@ import (
 	"strings"
 
 	ini "github.com/vaughan0/go-ini"
+
+	"github.com/fatedier/frp/utils/util"
 )
 
 var ServerCommonCfg *ServerCommonConf
@@ -57,6 +59,7 @@ type ServerCommonConf struct {
 
 	PrivilegeAllowPorts map[int]struct{}
 	MaxPoolCount        int64
+	MaxPortsPerClient   int64
 	HeartBeatTimeout    int64
 	UserConnTimeout     int64
 }
@@ -87,6 +90,7 @@ func GetDefaultServerCommonConf() *ServerCommonConf {
 		TcpMux:              true,
 		PrivilegeAllowPorts: make(map[int]struct{}),
 		MaxPoolCount:        5,
+		MaxPortsPerClient:   0,
 		HeartBeatTimeout:    90,
 		UserConnTimeout:     10,
 	}
@@ -238,55 +242,46 @@ func LoadServerCommonConf(conf ini.File) (cfg *ServerCommonConf, err error) {
 		allowPortsStr, ok := conf.Get("common", "privilege_allow_ports")
 		if ok {
 			// e.g. 1000-2000,2001,2002,3000-4000
-			portRanges := strings.Split(allowPortsStr, ",")
-			for _, portRangeStr := range portRanges {
-				// 1000-2000 or 2001
-				portArray := strings.Split(portRangeStr, "-")
-				// length: only 1 or 2 is correct
-				rangeType := len(portArray)
-				if rangeType == 1 {
-					// single port
-					singlePort, errRet := strconv.ParseInt(portArray[0], 10, 64)
-					if errRet != nil {
-						err = fmt.Errorf("Parse conf error: privilege_allow_ports is incorrect, %v", errRet)
-						return
-					}
-					cfg.PrivilegeAllowPorts[int(singlePort)] = struct{}{}
-				} else if rangeType == 2 {
-					// range ports
-					min, errRet := strconv.ParseInt(portArray[0], 10, 64)
-					if errRet != nil {
-						err = fmt.Errorf("Parse conf error: privilege_allow_ports is incorrect, %v", errRet)
-						return
-					}
-					max, errRet := strconv.ParseInt(portArray[1], 10, 64)
-					if errRet != nil {
-						err = fmt.Errorf("Parse conf error: privilege_allow_ports is incorrect, %v", errRet)
-						return
-					}
-					if max < min {
-						err = fmt.Errorf("Parse conf error: privilege_allow_ports range incorrect")
-						return
-					}
-					for i := min; i <= max; i++ {
-						cfg.PrivilegeAllowPorts[int(i)] = struct{}{}
-					}
-				} else {
-					err = fmt.Errorf("Parse conf error: privilege_allow_ports is incorrect")
-					return
-				}
+			ports, errRet := util.ParseRangeNumbers(allowPortsStr)
+			if errRet != nil {
+				err = fmt.Errorf("Parse conf error: privilege_allow_ports: %v", errRet)
+				return
+			}
+
+			for _, port := range ports {
+				cfg.PrivilegeAllowPorts[int(port)] = struct{}{}
 			}
 		}
 	}
 
 	tmpStr, ok = conf.Get("common", "max_pool_count")
 	if ok {
-		v, err = strconv.ParseInt(tmpStr, 10, 64)
-		if err == nil && v >= 0 {
+		if v, err = strconv.ParseInt(tmpStr, 10, 64); err != nil {
+			err = fmt.Errorf("Parse conf error: invalid max_pool_count")
+			return
+		} else {
+			if v < 0 {
+				err = fmt.Errorf("Parse conf error: invalid max_pool_count")
+				return
+			}
 			cfg.MaxPoolCount = v
 		}
 	}
 
+	tmpStr, ok = conf.Get("common", "max_ports_per_client")
+	if ok {
+		if v, err = strconv.ParseInt(tmpStr, 10, 64); err != nil {
+			err = fmt.Errorf("Parse conf error: invalid max_ports_per_client")
+			return
+		} else {
+			if v < 0 {
+				err = fmt.Errorf("Parse conf error: invalid max_ports_per_client")
+				return
+			}
+			cfg.MaxPortsPerClient = v
+		}
+	}
+
 	tmpStr, ok = conf.Get("common", "authentication_timeout")
 	if ok {
 		v, errRet := strconv.ParseInt(tmpStr, 10, 64)

+ 0 - 44
models/plugin/http_proxy.go

@@ -17,14 +17,11 @@ package plugin
 import (
 	"bufio"
 	"encoding/base64"
-	"fmt"
 	"io"
 	"net"
 	"net/http"
 	"strings"
-	"sync"
 
-	"github.com/fatedier/frp/utils/errors"
 	frpIo "github.com/fatedier/frp/utils/io"
 	frpNet "github.com/fatedier/frp/utils/net"
 )
@@ -35,47 +32,6 @@ func init() {
 	Register(PluginHttpProxy, NewHttpProxyPlugin)
 }
 
-type Listener struct {
-	conns  chan net.Conn
-	closed bool
-	mu     sync.Mutex
-}
-
-func NewProxyListener() *Listener {
-	return &Listener{
-		conns: make(chan net.Conn, 64),
-	}
-}
-
-func (l *Listener) Accept() (net.Conn, error) {
-	conn, ok := <-l.conns
-	if !ok {
-		return nil, fmt.Errorf("listener closed")
-	}
-	return conn, nil
-}
-
-func (l *Listener) PutConn(conn net.Conn) error {
-	err := errors.PanicToError(func() {
-		l.conns <- conn
-	})
-	return err
-}
-
-func (l *Listener) Close() error {
-	l.mu.Lock()
-	defer l.mu.Unlock()
-	if !l.closed {
-		close(l.conns)
-		l.closed = true
-	}
-	return nil
-}
-
-func (l *Listener) Addr() net.Addr {
-	return (*net.TCPAddr)(nil)
-}
-
 type HttpProxy struct {
 	l          *Listener
 	s          *http.Server

+ 44 - 0
models/plugin/plugin.go

@@ -17,7 +17,10 @@ package plugin
 import (
 	"fmt"
 	"io"
+	"net"
+	"sync"
 
+	"github.com/fatedier/frp/utils/errors"
 	frpNet "github.com/fatedier/frp/utils/net"
 )
 
@@ -45,3 +48,44 @@ type Plugin interface {
 	Handle(conn io.ReadWriteCloser, realConn frpNet.Conn)
 	Close() error
 }
+
+type Listener struct {
+	conns  chan net.Conn
+	closed bool
+	mu     sync.Mutex
+}
+
+func NewProxyListener() *Listener {
+	return &Listener{
+		conns: make(chan net.Conn, 64),
+	}
+}
+
+func (l *Listener) Accept() (net.Conn, error) {
+	conn, ok := <-l.conns
+	if !ok {
+		return nil, fmt.Errorf("listener closed")
+	}
+	return conn, nil
+}
+
+func (l *Listener) PutConn(conn net.Conn) error {
+	err := errors.PanicToError(func() {
+		l.conns <- conn
+	})
+	return err
+}
+
+func (l *Listener) Close() error {
+	l.mu.Lock()
+	defer l.mu.Unlock()
+	if !l.closed {
+		close(l.conns)
+		l.closed = true
+	}
+	return nil
+}
+
+func (l *Listener) Addr() net.Addr {
+	return (*net.TCPAddr)(nil)
+}

+ 13 - 3
models/plugin/socks5.go

@@ -32,13 +32,23 @@ func init() {
 
 type Socks5Plugin struct {
 	Server *gosocks5.Server
+
+	user   string
+	passwd string
 }
 
 func NewSocks5Plugin(params map[string]string) (p Plugin, err error) {
-	sp := &Socks5Plugin{}
-	sp.Server, err = gosocks5.New(&gosocks5.Config{
+	user := params["plugin_user"]
+	passwd := params["plugin_passwd"]
+
+	cfg := &gosocks5.Config{
 		Logger: log.New(ioutil.Discard, "", log.LstdFlags),
-	})
+	}
+	if user != "" || passwd != "" {
+		cfg.Credentials = gosocks5.StaticCredentials(map[string]string{user: passwd})
+	}
+	sp := &Socks5Plugin{}
+	sp.Server, err = gosocks5.New(cfg)
 	p = sp
 	return
 }

+ 87 - 0
models/plugin/static_file.go

@@ -0,0 +1,87 @@
+// Copyright 2018 fatedier, fatedier@gmail.com
+//
+// 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 plugin
+
+import (
+	"io"
+	"net/http"
+
+	"github.com/julienschmidt/httprouter"
+
+	frpNet "github.com/fatedier/frp/utils/net"
+)
+
+const PluginStaticFile = "static_file"
+
+func init() {
+	Register(PluginStaticFile, NewStaticFilePlugin)
+}
+
+type StaticFilePlugin struct {
+	localPath   string
+	stripPrefix string
+	httpUser    string
+	httpPasswd  string
+
+	l *Listener
+	s *http.Server
+}
+
+func NewStaticFilePlugin(params map[string]string) (Plugin, error) {
+	localPath := params["plugin_local_path"]
+	stripPrefix := params["plugin_strip_prefix"]
+	httpUser := params["plugin_http_user"]
+	httpPasswd := params["plugin_http_passwd"]
+
+	listener := NewProxyListener()
+
+	sp := &StaticFilePlugin{
+		localPath:   localPath,
+		stripPrefix: stripPrefix,
+		httpUser:    httpUser,
+		httpPasswd:  httpPasswd,
+
+		l: listener,
+	}
+	var prefix string
+	if stripPrefix != "" {
+		prefix = "/" + stripPrefix + "/"
+	} else {
+		prefix = "/"
+	}
+	router := httprouter.New()
+	router.Handler("GET", prefix+"*filepath", frpNet.MakeHttpGzipHandler(
+		frpNet.NewHttpBasicAuthWraper(http.StripPrefix(prefix, http.FileServer(http.Dir(localPath))), httpUser, httpPasswd)))
+	sp.s = &http.Server{
+		Handler: router,
+	}
+	go sp.s.Serve(listener)
+	return sp, nil
+}
+
+func (sp *StaticFilePlugin) Handle(conn io.ReadWriteCloser, realConn frpNet.Conn) {
+	wrapConn := frpNet.WrapReadWriteCloserToConn(conn, realConn)
+	sp.l.PutConn(wrapConn)
+}
+
+func (sp *StaticFilePlugin) Name() string {
+	return PluginStaticFile
+}
+
+func (sp *StaticFilePlugin) Close() error {
+	sp.s.Close()
+	sp.l.Close()
+	return nil
+}

+ 30 - 1
server/control.go

@@ -55,6 +55,9 @@ type Control struct {
 	// pool count
 	poolCount int
 
+	// ports used, for limitations
+	portsUsedNum int
+
 	// last time got the Ping message
 	lastPing time.Time
 
@@ -84,6 +87,7 @@ func NewControl(svr *Service, ctlConn net.Conn, loginMsg *msg.Login) *Control {
 		workConnCh:      make(chan net.Conn, loginMsg.PoolCount+10),
 		proxies:         make(map[string]Proxy),
 		poolCount:       loginMsg.PoolCount,
+		portsUsedNum:    0,
 		lastPing:        time.Now(),
 		runId:           loginMsg.RunId,
 		status:          consts.Working,
@@ -348,6 +352,26 @@ func (ctl *Control) RegisterProxy(pxyMsg *msg.NewProxy) (remoteAddr string, err
 		return remoteAddr, err
 	}
 
+	// Check ports used number in each client
+	if config.ServerCommonCfg.MaxPortsPerClient > 0 {
+		ctl.mu.Lock()
+		if ctl.portsUsedNum+pxy.GetUsedPortsNum() > int(config.ServerCommonCfg.MaxPortsPerClient) {
+			ctl.mu.Unlock()
+			err = fmt.Errorf("exceed the max_ports_per_client")
+			return
+		}
+		ctl.portsUsedNum = ctl.portsUsedNum + pxy.GetUsedPortsNum()
+		ctl.mu.Unlock()
+
+		defer func() {
+			if err != nil {
+				ctl.mu.Lock()
+				ctl.portsUsedNum = ctl.portsUsedNum - pxy.GetUsedPortsNum()
+				ctl.mu.Unlock()
+			}
+		}()
+	}
+
 	remoteAddr, err = pxy.Run()
 	if err != nil {
 		return
@@ -371,16 +395,21 @@ func (ctl *Control) RegisterProxy(pxyMsg *msg.NewProxy) (remoteAddr string, err
 
 func (ctl *Control) CloseProxy(closeMsg *msg.CloseProxy) (err error) {
 	ctl.mu.Lock()
-	defer ctl.mu.Unlock()
 
 	pxy, ok := ctl.proxies[closeMsg.ProxyName]
 	if !ok {
+		ctl.mu.Unlock()
 		return
 	}
 
+	if config.ServerCommonCfg.MaxPortsPerClient > 0 {
+		ctl.portsUsedNum = ctl.portsUsedNum - pxy.GetUsedPortsNum()
+	}
 	pxy.Close()
 	ctl.svr.DelProxy(pxy.GetName())
 	delete(ctl.proxies, closeMsg.ProxyName)
+	ctl.mu.Unlock()
+
 	StatsCloseProxy(pxy.GetName(), pxy.GetConf().GetBaseInfo().ProxyType)
 	return
 }

+ 13 - 4
server/proxy.go

@@ -40,15 +40,18 @@ type Proxy interface {
 	GetName() string
 	GetConf() config.ProxyConf
 	GetWorkConnFromPool() (workConn frpNet.Conn, err error)
+	GetUsedPortsNum() int
 	Close()
 	log.Logger
 }
 
 type BaseProxy struct {
-	name      string
-	ctl       *Control
-	listeners []frpNet.Listener
-	mu        sync.RWMutex
+	name         string
+	ctl          *Control
+	listeners    []frpNet.Listener
+	usedPortsNum int
+
+	mu sync.RWMutex
 	log.Logger
 }
 
@@ -60,6 +63,10 @@ func (pxy *BaseProxy) GetControl() *Control {
 	return pxy.ctl
 }
 
+func (pxy *BaseProxy) GetUsedPortsNum() int {
+	return pxy.usedPortsNum
+}
+
 func (pxy *BaseProxy) Close() {
 	pxy.Info("proxy closing")
 	for _, l := range pxy.listeners {
@@ -126,6 +133,7 @@ func NewProxy(ctl *Control, pxyConf config.ProxyConf) (pxy Proxy, err error) {
 	}
 	switch cfg := pxyConf.(type) {
 	case *config.TcpProxyConf:
+		basePxy.usedPortsNum = 1
 		pxy = &TcpProxy{
 			BaseProxy: basePxy,
 			cfg:       cfg,
@@ -141,6 +149,7 @@ func NewProxy(ctl *Control, pxyConf config.ProxyConf) (pxy Proxy, err error) {
 			cfg:       cfg,
 		}
 	case *config.UdpProxyConf:
+		basePxy.usedPortsNum = 1
 		pxy = &UdpProxy{
 			BaseProxy: basePxy,
 			cfg:       cfg,

+ 6 - 0
tests/conf/auto_test_frpc.ini

@@ -161,3 +161,9 @@ remote_port = 0
 type = tcp
 plugin = http_proxy
 remote_port = 0
+
+[range:range_tcp]
+type = tcp
+local_ip = 127.0.0.1
+local_port = 30000-30001,30003
+remote_port = 30000-30001,30003

+ 1 - 1
tests/conf/auto_test_frps.ini

@@ -5,5 +5,5 @@ vhost_http_port = 10804
 log_file = ./frps.log
 log_level = debug
 privilege_token = 123456
-privilege_allow_ports = 10000-20000,20002,30000-40000
+privilege_allow_ports = 10000-20000,20002,30000-50000
 subdomain_host = sub.com

+ 14 - 1
tests/func_test.go

@@ -53,8 +53,9 @@ var (
 	ProxyUdpPortNotAllowed  string = "udp_port_not_allowed"
 	ProxyUdpPortNormal      string = "udp_port_normal"
 	ProxyUdpRandomPort      string = "udp_random_port"
+	ProxyHttpProxy          string = "http_proxy"
 
-	ProxyHttpProxy string = "http_proxy"
+	ProxyRangeTcpPrefix string = "range_tcp"
 )
 
 func init() {
@@ -286,3 +287,15 @@ func TestPluginHttpProxy(t *testing.T) {
 		}
 	}
 }
+
+func TestRangePortsMapping(t *testing.T) {
+	assert := assert.New(t)
+
+	for i := 0; i < 3; i++ {
+		name := fmt.Sprintf("%s_%d", ProxyRangeTcpPrefix, i)
+		status, err := getProxyStatus(name)
+		if assert.NoError(err) {
+			assert.Equal(client.ProxyStatusRunning, status.Status)
+		}
+	}
+}

+ 47 - 0
utils/util/util.go

@@ -19,6 +19,8 @@ import (
 	"crypto/rand"
 	"encoding/hex"
 	"fmt"
+	"strconv"
+	"strings"
 )
 
 // RandId return a rand string used in frp.
@@ -54,3 +56,48 @@ func CanonicalAddr(host string, port int) (addr string) {
 	}
 	return
 }
+
+func ParseRangeNumbers(rangeStr string) (numbers []int64, err error) {
+	rangeStr = strings.TrimSpace(rangeStr)
+	numbers = make([]int64, 0)
+	// e.g. 1000-2000,2001,2002,3000-4000
+	numRanges := strings.Split(rangeStr, ",")
+	for _, numRangeStr := range numRanges {
+		// 1000-2000 or 2001
+		numArray := strings.Split(numRangeStr, "-")
+		// length: only 1 or 2 is correct
+		rangeType := len(numArray)
+		if rangeType == 1 {
+			// single number
+			singleNum, errRet := strconv.ParseInt(strings.TrimSpace(numArray[0]), 10, 64)
+			if errRet != nil {
+				err = fmt.Errorf("range number is invalid, %v", errRet)
+				return
+			}
+			numbers = append(numbers, singleNum)
+		} else if rangeType == 2 {
+			// range numbers
+			min, errRet := strconv.ParseInt(strings.TrimSpace(numArray[0]), 10, 64)
+			if errRet != nil {
+				err = fmt.Errorf("range number is invalid, %v", errRet)
+				return
+			}
+			max, errRet := strconv.ParseInt(strings.TrimSpace(numArray[1]), 10, 64)
+			if errRet != nil {
+				err = fmt.Errorf("range number is invalid, %v", errRet)
+				return
+			}
+			if max < min {
+				err = fmt.Errorf("range number is invalid")
+				return
+			}
+			for i := min; i <= max; i++ {
+				numbers = append(numbers, i)
+			}
+		} else {
+			err = fmt.Errorf("range number is invalid")
+			return
+		}
+	}
+	return
+}

+ 26 - 0
utils/util/util_test.go

@@ -20,3 +20,29 @@ func TestGetAuthKey(t *testing.T) {
 	t.Log(key)
 	assert.Equal("6df41a43725f0c770fd56379e12acf8c", key)
 }
+
+func TestParseRangeNumbers(t *testing.T) {
+	assert := assert.New(t)
+	numbers, err := ParseRangeNumbers("2-5")
+	if assert.NoError(err) {
+		assert.Equal([]int64{2, 3, 4, 5}, numbers)
+	}
+
+	numbers, err = ParseRangeNumbers("1")
+	if assert.NoError(err) {
+		assert.Equal([]int64{1}, numbers)
+	}
+
+	numbers, err = ParseRangeNumbers("3-5,8")
+	if assert.NoError(err) {
+		assert.Equal([]int64{3, 4, 5, 8}, numbers)
+	}
+
+	numbers, err = ParseRangeNumbers(" 3-5,8, 10-12 ")
+	if assert.NoError(err) {
+		assert.Equal([]int64{3, 4, 5, 8, 10, 11, 12}, numbers)
+	}
+
+	_, err = ParseRangeNumbers("3-a")
+	assert.Error(err)
+}