Merge pull request #630 from fatedier/dev

bump version to v0.16.0
This commit is contained in:
fatedier 2018-01-30 00:04:02 +08:00 committed by GitHub
commit 456ce09061
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
22 changed files with 563 additions and 146 deletions

View File

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

View File

@ -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) * [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 DNS query request](#forward-dns-query-request)
* [Forward unix domain socket](#forward-unix-domain-socket) * [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) * [Expose your service in security](#expose-your-service-in-security)
* [P2P Mode](#p2p-mode) * [P2P Mode](#p2p-mode)
* [Connect website through frpc's network](#connect-website-through-frpcs-network) * [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` `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 ### Expose your service in security
For some services, if expose them to the public network directly will be a security risk. 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 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 ### Plugin
frpc only forward request to local tcp or udp port by default. 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. 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. `plugin_http_user` and `plugin_http_passwd` are configuration parameters used in `http_proxy` plugin.
## Development Plan ## Development Plan
* Log http request information in frps. * Log http request information in frps.
* Direct reverse proxy, like haproxy. * Direct reverse proxy, like haproxy.
* Load balance to different service in frpc. * 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. * kubernetes ingress support.
## Contributing ## Contributing
Interested in getting involved? We would like to help you! Interested in getting involved? We would like to help you!

View File

@ -18,6 +18,7 @@ frp 是一个可用于内网穿透的高性能的反向代理应用,支持 tcp
* [通过自定义域名访问部署于内网的 web 服务](#通过自定义域名访问部署于内网的-web-服务) * [通过自定义域名访问部署于内网的 web 服务](#通过自定义域名访问部署于内网的-web-服务)
* [转发 DNS 查询请求](#转发-dns-查询请求) * [转发 DNS 查询请求](#转发-dns-查询请求)
* [转发 Unix域套接字](#转发-unix域套接字) * [转发 Unix域套接字](#转发-unix域套接字)
* [对外提供简单的文件访问服务](#对外提供简单的文件访问服务)
* [安全地暴露内网服务](#安全地暴露内网服务) * [安全地暴露内网服务](#安全地暴露内网服务)
* [点对点内网穿透](#点对点内网穿透) * [点对点内网穿透](#点对点内网穿透)
* [通过 frpc 所在机器访问外网](#通过-frpc-所在机器访问外网) * [通过 frpc 所在机器访问外网](#通过-frpc-所在机器访问外网)
@ -39,6 +40,7 @@ frp 是一个可用于内网穿透的高性能的反向代理应用,支持 tcp
* [自定义二级域名](#自定义二级域名) * [自定义二级域名](#自定义二级域名)
* [URL 路由](#url-路由) * [URL 路由](#url-路由)
* [通过代理连接 frps](#通过代理连接-frps) * [通过代理连接 frps](#通过代理连接-frps)
* [范围端口映射](#范围端口映射)
* [插件](#插件) * [插件](#插件)
* [开发计划](#开发计划) * [开发计划](#开发计划)
* [为 frp 做贡献](#为-frp-做贡献) * [为 frp 做贡献](#为-frp-做贡献)
@ -192,11 +194,11 @@ DNS 查询请求通常使用 UDP 协议frp 支持对内网 UDP 服务的穿
### 转发 Unix域套接字 ### 转发 Unix域套接字
通过 tcp 端口访问内网的 unix域套接字(和 docker daemon 通信)。 通过 tcp 端口访问内网的 unix域套接字(例如和 docker daemon 通信)。
frps 的部署步骤同上。 frps 的部署步骤同上。
1. 启动 frpc启用 unix_domain_socket 插件,配置如下: 1. 启动 frpc启用 `unix_domain_socket` 插件,配置如下:
```ini ```ini
# frpc.ini # frpc.ini
@ -215,6 +217,34 @@ frps 的部署步骤同上。
`curl http://x.x.x.x:6000/version` `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 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 端口。 默认情况下frpc 只会转发请求到本地 tcp 或 udp 端口。
插件模式是为了在客户端提供更加丰富的功能,目前内置的插件有 **unix_domain_socket**、**http_proxy**、**socks5**。具体使用方式请查看[使用示例](#使用示例)。 插件模式是为了在客户端提供更加丰富的功能,目前内置的插件有 `unix_domain_socket`、`http_proxy`、`socks5`、`static_file`。具体使用方式请查看[使用示例](#使用示例)。
通过 `plugin` 指定需要使用的插件,插件的配置参数都以 `plugin_` 开头。使用插件后 `local_ip``local_port` 不再需要配置。 通过 `plugin` 指定需要使用的插件,插件的配置参数都以 `plugin_` 开头。使用插件后 `local_ip``local_port` 不再需要配置。
@ -638,8 +687,6 @@ plugin_http_passwd = abc
* frps 记录 http 请求日志。 * frps 记录 http 请求日志。
* frps 支持直接反向代理,类似 haproxy。 * frps 支持直接反向代理,类似 haproxy。
* frpc 支持负载均衡到后端不同服务。 * frpc 支持负载均衡到后端不同服务。
* frpc 支持直接作为 webserver 访问指定静态页面。
* 支持 udp 打洞的方式,提供两边内网机器直接通信,流量不经过服务器转发。
* 集成对 k8s 等平台的支持。 * 集成对 k8s 等平台的支持。
## 为 frp 做贡献 ## 为 frp 做贡献

View File

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

View File

@ -12,10 +12,11 @@ import (
) )
const ( const (
ProxyStatusNew = "new" ProxyStatusNew = "new"
ProxyStatusStartErr = "start error" ProxyStatusStartErr = "start error"
ProxyStatusRunning = "running" ProxyStatusWaitStart = "wait start"
ProxyStatusClosed = "closed" ProxyStatusRunning = "running"
ProxyStatusClosed = "closed"
) )
type ProxyManager struct { 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() pw.mu.RLock()
defer pw.mu.RUnlock() defer pw.mu.RUnlock()
if pw.Status == ProxyStatusRunning { return pw.Status
return true
} else {
return false
}
} }
func (pw *ProxyWrapper) GetStatus() *ProxyStatus { func (pw *ProxyWrapper) GetStatus() *ProxyStatus {
@ -93,6 +90,12 @@ func (pw *ProxyWrapper) GetStatus() *ProxyStatus {
return ps 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 { func (pw *ProxyWrapper) Start(remoteAddr string, serverRespErr string) error {
if pw.pxy != nil { if pw.pxy != nil {
pw.pxy.Close() 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() pm.mu.RLock()
defer pm.mu.RUnlock() defer pm.mu.RUnlock()
if pm.closed { if pm.closed {
@ -219,13 +223,18 @@ func (pm *ProxyManager) CheckAndStartProxy() {
} }
for _, pxy := range pm.proxies { for _, pxy := range pm.proxies {
if !pxy.IsRunning() { status := pxy.GetStatusStr()
var newProxyMsg msg.NewProxy for _, s := range pxyStatus {
pxy.Cfg.UnMarshalToMsg(&newProxyMsg) if status == s {
err := pm.sendMsg(&newProxyMsg) var newProxyMsg msg.NewProxy
if err != nil { pxy.Cfg.UnMarshalToMsg(&newProxyMsg)
pm.Warn("[%s] proxy send NewProxy message error") err := pm.sendMsg(&newProxyMsg)
return 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() pm.mu.Lock()
defer pm.mu.Unlock() defer func() {
pm.mu.Unlock()
if startNow {
go pm.CheckAndStartProxy([]string{ProxyStatusNew})
}
}()
if pm.closed { if pm.closed {
err := fmt.Errorf("Reload error: ProxyManager is closed now") err := fmt.Errorf("Reload error: ProxyManager is closed now")
pm.Warn(err.Error()) pm.Warn(err.Error())

View File

@ -99,7 +99,7 @@ func main() {
if args["status"] != nil { if args["status"] != nil {
if args["status"].(bool) { if args["status"].(bool) {
if err = CmdStatus(); err != nil { 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) os.Exit(1)
} else { } else {
os.Exit(0) os.Exit(0)

View File

@ -73,6 +73,16 @@ local_port = 22
# if remote_port is 0, frps will assgin a random port for you # if remote_port is 0, frps will assgin a random port for you
remote_port = 0 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] [dns]
type = udp type = udp
local_ip = 114.114.114.114 local_ip = 114.114.114.114
@ -81,6 +91,14 @@ remote_port = 6002
use_encryption = false use_encryption = false
use_compression = 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 # 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] [web01]
type = http type = http
@ -124,6 +142,22 @@ plugin = http_proxy
plugin_http_user = abc plugin_http_user = abc
plugin_http_passwd = 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] [secret_tcp]
# If the type is secret tcp, remote_port is useless # 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 # Who want to connect local port should deploy another frpc with stcp proxy and role is visitor

View File

@ -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 # pool_count in each proxy will change to max_pool_count if they exceed the maximum value
max_pool_count = 5 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 # 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 # if authentication_timeout is zero, the time is not verified, default is 900s
authentication_timeout = 900 authentication_timeout = 900

View File

@ -22,6 +22,7 @@ import (
"github.com/fatedier/frp/models/consts" "github.com/fatedier/frp/models/consts"
"github.com/fatedier/frp/models/msg" "github.com/fatedier/frp/models/msg"
"github.com/fatedier/frp/utils/util"
ini "github.com/vaughan0/go-ini" ini "github.com/vaughan0/go-ini"
) )
@ -770,6 +771,38 @@ func (cfg *XtcpProxyConf) Check() (err error) {
return 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 // if len(startProxy) is 0, start all
// otherwise just start proxies in startProxy map // otherwise just start proxies in startProxy map
func LoadProxyConfFromFile(prefix string, conf ini.File, startProxy map[string]struct{}) ( 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) proxyConfs = make(map[string]ProxyConf)
visitorConfs = make(map[string]ProxyConf) visitorConfs = make(map[string]ProxyConf)
for name, section := range conf { for name, section := range conf {
if name == "common" {
continue
}
_, shouldStart := startProxy[name] _, 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 // some proxy or visotr configure may be used this prefix
section["prefix"] = prefix subSection["prefix"] = prefix
cfg, err := NewProxyConfFromFile(name, section) cfg, err := NewProxyConfFromFile(subName, subSection)
if err != nil { if err != nil {
return proxyConfs, visitorConfs, err return proxyConfs, visitorConfs, err
} }
role := section["role"] role := subSection["role"]
if role == "visitor" { if role == "visitor" {
visitorConfs[prefix+name] = cfg visitorConfs[prefix+subName] = cfg
} else { } else {
proxyConfs[prefix+name] = cfg proxyConfs[prefix+subName] = cfg
} }
} }
} }
return return
} }
func copySection(section ini.Section) (out ini.Section) {
out = make(ini.Section)
for k, v := range section {
out[k] = v
}
return
}

View File

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

View File

@ -17,14 +17,11 @@ package plugin
import ( import (
"bufio" "bufio"
"encoding/base64" "encoding/base64"
"fmt"
"io" "io"
"net" "net"
"net/http" "net/http"
"strings" "strings"
"sync"
"github.com/fatedier/frp/utils/errors"
frpIo "github.com/fatedier/frp/utils/io" frpIo "github.com/fatedier/frp/utils/io"
frpNet "github.com/fatedier/frp/utils/net" frpNet "github.com/fatedier/frp/utils/net"
) )
@ -35,47 +32,6 @@ func init() {
Register(PluginHttpProxy, NewHttpProxyPlugin) 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 { type HttpProxy struct {
l *Listener l *Listener
s *http.Server s *http.Server

View File

@ -17,7 +17,10 @@ package plugin
import ( import (
"fmt" "fmt"
"io" "io"
"net"
"sync"
"github.com/fatedier/frp/utils/errors"
frpNet "github.com/fatedier/frp/utils/net" frpNet "github.com/fatedier/frp/utils/net"
) )
@ -45,3 +48,44 @@ type Plugin interface {
Handle(conn io.ReadWriteCloser, realConn frpNet.Conn) Handle(conn io.ReadWriteCloser, realConn frpNet.Conn)
Close() error 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)
}

View File

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

View File

@ -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
}

View File

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

View File

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

View File

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

View File

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

View File

@ -53,8 +53,9 @@ var (
ProxyUdpPortNotAllowed string = "udp_port_not_allowed" ProxyUdpPortNotAllowed string = "udp_port_not_allowed"
ProxyUdpPortNormal string = "udp_port_normal" ProxyUdpPortNormal string = "udp_port_normal"
ProxyUdpRandomPort string = "udp_random_port" ProxyUdpRandomPort string = "udp_random_port"
ProxyHttpProxy string = "http_proxy"
ProxyHttpProxy string = "http_proxy" ProxyRangeTcpPrefix string = "range_tcp"
) )
func init() { 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)
}
}
}

View File

@ -19,6 +19,8 @@ import (
"crypto/rand" "crypto/rand"
"encoding/hex" "encoding/hex"
"fmt" "fmt"
"strconv"
"strings"
) )
// RandId return a rand string used in frp. // RandId return a rand string used in frp.
@ -54,3 +56,48 @@ func CanonicalAddr(host string, port int) (addr string) {
} }
return 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
}

View File

@ -20,3 +20,29 @@ func TestGetAuthKey(t *testing.T) {
t.Log(key) t.Log(key)
assert.Equal("6df41a43725f0c770fd56379e12acf8c", 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)
}

View File

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