From 68dfc89bce16cf6cd08ca94893d384fc8e5e4c70 Mon Sep 17 00:00:00 2001 From: fatedier Date: Wed, 24 Jan 2018 17:49:13 +0800 Subject: [PATCH 1/9] plugin: new plugin static_file for getting files by http protocol --- conf/frpc_full.ini | 27 +++++++++++ models/plugin/http_proxy.go | 44 ------------------ models/plugin/plugin.go | 44 ++++++++++++++++++ models/plugin/static_file.go | 87 ++++++++++++++++++++++++++++++++++++ 4 files changed, 158 insertions(+), 44 deletions(-) create mode 100644 models/plugin/static_file.go diff --git a/conf/frpc_full.ini b/conf/frpc_full.ini index 78992c8f..1b152ed8 100644 --- a/conf/frpc_full.ini +++ b/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,15 @@ plugin = http_proxy plugin_http_user = abc plugin_http_passwd = abc +[plugin_static_file] +type = tcp +remote_port = 6005 +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 diff --git a/models/plugin/http_proxy.go b/models/plugin/http_proxy.go index e5aefa8f..41e6bdcd 100644 --- a/models/plugin/http_proxy.go +++ b/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 diff --git a/models/plugin/plugin.go b/models/plugin/plugin.go index a0ae8e1b..bd976d1e 100644 --- a/models/plugin/plugin.go +++ b/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) +} diff --git a/models/plugin/static_file.go b/models/plugin/static_file.go new file mode 100644 index 00000000..87290b67 --- /dev/null +++ b/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 +} From 41c41789b6f7098e043f6043f2aeeac16426bf15 Mon Sep 17 00:00:00 2001 From: fatedier Date: Wed, 24 Jan 2018 23:04:55 +0800 Subject: [PATCH 2/9] plugin: socks5 support user password auth, close #484 --- conf/frpc_full.ini | 9 ++++++++- models/plugin/socks5.go | 16 +++++++++++++--- 2 files changed, 21 insertions(+), 4 deletions(-) diff --git a/conf/frpc_full.ini b/conf/frpc_full.ini index 1b152ed8..1204b266 100644 --- a/conf/frpc_full.ini +++ b/conf/frpc_full.ini @@ -142,9 +142,16 @@ plugin = http_proxy plugin_http_user = abc plugin_http_passwd = abc -[plugin_static_file] +[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 diff --git a/models/plugin/socks5.go b/models/plugin/socks5.go index d7804885..fba9f5df 100644 --- a/models/plugin/socks5.go +++ b/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 } From eede31c0645e5f5d76aa4eb4093f28758df2f4ba Mon Sep 17 00:00:00 2001 From: fatedier Date: Wed, 24 Jan 2018 23:27:03 +0800 Subject: [PATCH 3/9] doc: about static_file plugin --- README.md | 27 +++++++++++++++++++++++++++ README_zh.md | 33 +++++++++++++++++++++++++++++++-- 2 files changed, 58 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 93ee895f..070224d1 100644 --- a/README.md +++ b/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. diff --git a/README_zh.md b/README_zh.md index e119a4f7..ef71a608 100644 --- a/README_zh.md +++ b/README_zh.md @@ -18,6 +18,7 @@ frp 是一个可用于内网穿透的高性能的反向代理应用,支持 tcp * [通过自定义域名访问部署于内网的 web 服务](#通过自定义域名访问部署于内网的-web-服务) * [转发 DNS 查询请求](#转发-dns-查询请求) * [转发 Unix域套接字](#转发-unix域套接字) + * [对外提供简单的文件访问服务](#对外提供简单的文件访问服务) * [安全地暴露内网服务](#安全地暴露内网服务) * [点对点内网穿透](#点对点内网穿透) * [通过 frpc 所在机器访问外网](#通过-frpc-所在机器访问外网) @@ -192,11 +193,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 +216,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` 目录下的文件,会要求输入已设置好的用户名和密码。 + ### 安全地暴露内网服务 对于某些服务来说如果直接暴露于公网上将会存在安全隐患。 From ce8fde793cdc794c7e3bcd3f7a20427acd288f4a Mon Sep 17 00:00:00 2001 From: fatedier Date: Thu, 25 Jan 2018 23:05:07 +0800 Subject: [PATCH 4/9] new feature: range section for mapping range ports --- models/config/proxy.go | 74 +++++++++++++++++++++++++++++++--- models/config/server_common.go | 47 +++++---------------- utils/util/util.go | 47 +++++++++++++++++++++ utils/util/util_test.go | 26 ++++++++++++ 4 files changed, 151 insertions(+), 43 deletions(-) diff --git a/models/config/proxy.go b/models/config/proxy.go index 022e64f4..b506c435 100644 --- a/models/config/proxy.go +++ b/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 +} diff --git a/models/config/server_common.go b/models/config/server_common.go index 37892b4e..614b95cb 100644 --- a/models/config/server_common.go +++ b/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 @@ -238,43 +240,14 @@ 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{}{} } } } diff --git a/utils/util/util.go b/utils/util/util.go index 4439f1aa..7ea4e83c 100644 --- a/utils/util/util.go +++ b/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 +} diff --git a/utils/util/util_test.go b/utils/util/util_test.go index 8210c613..a7518f6f 100644 --- a/utils/util/util_test.go +++ b/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) +} From 637ddbce1f23bae495a2aba0b3d6b51dbc3fbbaa Mon Sep 17 00:00:00 2001 From: fatedier Date: Fri, 26 Jan 2018 00:23:48 +0800 Subject: [PATCH 5/9] frpc: udpate proxies check and start logic --- client/control.go | 24 ++++++++++++------------ client/proxy_manager.go | 38 ++++++++++++++++++++++---------------- cmd/frpc/main.go | 2 +- 3 files changed, 35 insertions(+), 29 deletions(-) diff --git a/client/control.go b/client/control.go index a4bb9e09..a9c4511b 100644 --- a/client/control.go +++ b/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}) 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 } diff --git a/client/proxy_manager.go b/client/proxy_manager.go index 77823986..6f1e32c9 100644 --- a/client/proxy_manager.go +++ b/client/proxy_manager.go @@ -69,14 +69,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 { @@ -210,7 +206,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 +216,17 @@ 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 + } + break } } } @@ -245,9 +246,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()) diff --git a/cmd/frpc/main.go b/cmd/frpc/main.go index 234d9e79..51eb53ca 100644 --- a/cmd/frpc/main.go +++ b/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) From 8e719ff0ff16289ad4f487283fe094d402a937ef Mon Sep 17 00:00:00 2001 From: fatedier Date: Fri, 26 Jan 2018 14:56:55 +0800 Subject: [PATCH 6/9] frps: new params max_ports_per_client --- client/control.go | 2 +- conf/frps_full.ini | 3 +++ models/config/server_common.go | 26 ++++++++++++++++++++++++-- server/control.go | 31 ++++++++++++++++++++++++++++++- server/proxy.go | 17 +++++++++++++---- 5 files changed, 71 insertions(+), 8 deletions(-) diff --git a/client/control.go b/client/control.go index a9c4511b..1b31ace7 100644 --- a/client/control.go +++ b/client/control.go @@ -427,7 +427,7 @@ func (ctl *Control) worker() { go ctl.reader() // start all configured proxies - ctl.pm.CheckAndStartProxy([]string{ProxyStatusNew}) + ctl.pm.CheckAndStartProxy([]string{ProxyStatusNew, ProxyStatusClosed}) checkProxyTicker.Stop() checkProxyTicker = time.NewTicker(checkInterval) diff --git a/conf/frps_full.ini b/conf/frps_full.ini index eac88269..e2a7c3f0 100644 --- a/conf/frps_full.ini +++ b/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 diff --git a/models/config/server_common.go b/models/config/server_common.go index 614b95cb..51880a70 100644 --- a/models/config/server_common.go +++ b/models/config/server_common.go @@ -59,6 +59,7 @@ type ServerCommonConf struct { PrivilegeAllowPorts map[int]struct{} MaxPoolCount int64 + MaxPortsPerClient int64 HeartBeatTimeout int64 UserConnTimeout int64 } @@ -89,6 +90,7 @@ func GetDefaultServerCommonConf() *ServerCommonConf { TcpMux: true, PrivilegeAllowPorts: make(map[int]struct{}), MaxPoolCount: 5, + MaxPortsPerClient: 0, HeartBeatTimeout: 90, UserConnTimeout: 10, } @@ -254,12 +256,32 @@ func LoadServerCommonConf(conf ini.File) (cfg *ServerCommonConf, err error) { 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) diff --git a/server/control.go b/server/control.go index 7492ce4b..dbb99ad9 100644 --- a/server/control.go +++ b/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 } diff --git a/server/proxy.go b/server/proxy.go index 554e8181..715bf0c0 100644 --- a/server/proxy.go +++ b/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, From 1c35e9a0c6a3a99935813ccd473ea45c6d38a628 Mon Sep 17 00:00:00 2001 From: fatedier Date: Mon, 29 Jan 2018 23:05:17 +0800 Subject: [PATCH 7/9] doc: update --- README.md | 21 ++++++++++++++++----- README_zh.md | 24 +++++++++++++++++++++--- 2 files changed, 37 insertions(+), 8 deletions(-) diff --git a/README.md b/README.md index 070224d1..458e0e6b 100644 --- a/README.md +++ b/README.md @@ -603,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. @@ -625,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! diff --git a/README_zh.md b/README_zh.md index ef71a608..ebc11a38 100644 --- a/README_zh.md +++ b/README_zh.md @@ -40,6 +40,7 @@ frp 是一个可用于内网穿透的高性能的反向代理应用,支持 tcp * [自定义二级域名](#自定义二级域名) * [URL 路由](#url-路由) * [通过代理连接 frps](#通过代理连接-frps) + * [范围端口映射](#范围端口映射) * [插件](#插件) * [开发计划](#开发计划) * [为 frp 做贡献](#为-frp-做贡献) @@ -638,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` 不再需要配置。 @@ -667,8 +687,6 @@ plugin_http_passwd = abc * frps 记录 http 请求日志。 * frps 支持直接反向代理,类似 haproxy。 * frpc 支持负载均衡到后端不同服务。 -* frpc 支持直接作为 webserver 访问指定静态页面。 -* 支持 udp 打洞的方式,提供两边内网机器直接通信,流量不经过服务器转发。 * 集成对 k8s 等平台的支持。 ## 为 frp 做贡献 From 44e8108910d79fa504c7b2ecb0d5a488bc213bc2 Mon Sep 17 00:00:00 2001 From: fatedier Date: Mon, 29 Jan 2018 23:13:10 +0800 Subject: [PATCH 8/9] ci: add test case for range ports mapping --- tests/conf/auto_test_frpc.ini | 6 ++++++ tests/conf/auto_test_frps.ini | 2 +- tests/func_test.go | 15 ++++++++++++++- 3 files changed, 21 insertions(+), 2 deletions(-) diff --git a/tests/conf/auto_test_frpc.ini b/tests/conf/auto_test_frpc.ini index 26da4c1a..93769900 100644 --- a/tests/conf/auto_test_frpc.ini +++ b/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 diff --git a/tests/conf/auto_test_frps.ini b/tests/conf/auto_test_frps.ini index 229ffa84..f59b8a3c 100644 --- a/tests/conf/auto_test_frps.ini +++ b/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 diff --git a/tests/func_test.go b/tests/func_test.go index 4e03f2cf..4da74168 100644 --- a/tests/func_test.go +++ b/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) + } + } +} From 9b990adf96085d6294e26a7dc2d33d17452097da Mon Sep 17 00:00:00 2001 From: fatedier Date: Mon, 29 Jan 2018 23:51:46 +0800 Subject: [PATCH 9/9] frpc: add proxy status 'wait start' --- Makefile | 3 --- client/proxy_manager.go | 16 ++++++++++++---- 2 files changed, 12 insertions(+), 7 deletions(-) diff --git a/Makefile b/Makefile index 6d160351..63927f9b 100644 --- a/Makefile +++ b/Makefile @@ -53,6 +53,3 @@ clean: rm -f ./bin/frpc rm -f ./bin/frps cd ./tests && ./clean_test.sh && cd - - -save: - godep save ./... diff --git a/client/proxy_manager.go b/client/proxy_manager.go index 6f1e32c9..9d81ee76 100644 --- a/client/proxy_manager.go +++ b/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 { @@ -89,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() @@ -226,6 +233,7 @@ func (pm *ProxyManager) CheckAndStartProxy(pxyStatus []string) { pm.Warn("[%s] proxy send NewProxy message error") return } + pxy.WaitStart() break } }