From f804330dbfcf192525add687a8ca829cc611492c Mon Sep 17 00:00:00 2001 From: fatedier Date: Tue, 3 May 2016 22:05:55 +0800 Subject: [PATCH] release v0.5.0 (#24) [new] Optimize for http services.Support virtual host and custom domain binding. [new] Support max days of keeping log files. [fix] Fix a bug when reconnecting. --- Dockerfile | 14 --- README.md | 20 ++-- README_zh.md | 27 +++-- conf/frpc.ini | 19 ++- conf/frps.ini | 19 ++- doc/quick_start_en.md | 86 ++++++++++++-- doc/quick_start_zh.md | 92 +++++++++++++-- src/frp/cmd/frpc/main.go | 2 +- src/frp/cmd/frps/control.go | 2 +- src/frp/cmd/frps/main.go | 21 +++- src/frp/models/client/client.go | 1 + src/frp/models/client/config.go | 16 +++ src/frp/models/server/config.go | 69 +++++++++-- src/frp/models/server/server.go | 118 +++++++++++-------- src/frp/utils/conn/conn.go | 32 +++-- src/frp/utils/log/log.go | 14 +-- src/frp/utils/version/version.go | 2 +- src/frp/utils/vhost/vhost.go | 193 +++++++++++++++++++++++++++++++ 18 files changed, 607 insertions(+), 140 deletions(-) delete mode 100644 Dockerfile create mode 100644 src/frp/utils/vhost/vhost.go diff --git a/Dockerfile b/Dockerfile deleted file mode 100644 index 5480f421..00000000 --- a/Dockerfile +++ /dev/null @@ -1,14 +0,0 @@ -FROM golang:1.5 - -MAINTAINER fatedier - -RUN echo "[common]\nbind_addr = 0.0.0.0\nbind_port = 7000\n[test]\npasswd = 123\nbind_addr = 0.0.0.0\nlisten_port = 80" > /usr/share/frps.ini - -ADD ./ /usr/share/frp/ - -RUN cd /usr/share/frp && make - -EXPOSE 80 -EXPOSE 7000 - -CMD ["/usr/share/frp/bin/frps", "-c", "/usr/share/frps.ini"] diff --git a/README.md b/README.md index 2ed7ffc7..8ebc7b37 100644 --- a/README.md +++ b/README.md @@ -8,6 +8,12 @@ frp is a fast reverse proxy to help you expose a local server behind a NAT or firewall to the internet. +## What can I do with frp? + +* Expose any http service behind a NAT or firewall to the internet by a server with public IP address(Name-based Virtual Host Support). +* Expose any tcp service behind a NAT or firewall to the internet by a server with public IP address. +* Inspect all http requests/responses that are transmitted over the tunnel(future). + ## Status frp is under development and you can try it with latest release version.Master branch for releasing stable version when dev branch for developing. @@ -16,17 +22,15 @@ frp is under development and you can try it with latest release version.Master b ## Quick Start -Read the [QuickStart](doc/quick_start_en.md) | [使用文档](doc/quick_start_zh.md) +Read the [QuickStart](/doc/quick_start_en.md) + +[Tcp port forwarding](/doc/quick_start_en.md#tcp-port-forwarding) + +[Http port forwarding and Custom domain binding](/doc/quick_start_en.md#http-port-forwarding-and-custom-domains-binding) ## Architecture -![architecture](doc/pic/architecture.png) - -## What can I do with frp? - -* Expose any http service behind a NAT or firewall to the internet by a server with public IP address. -* Expose any tcp service behind a NAT or firewall to the internet by a server with public IP address. -* Inspect all http requests/responses that are transmitted over the tunnel(future). +![architecture](/doc/pic/architecture.png) ## Contributing diff --git a/README_zh.md b/README_zh.md index 4b272577..a3485480 100644 --- a/README_zh.md +++ b/README_zh.md @@ -4,31 +4,36 @@ [README](README.md) | [中文文档](README_zh.md) ->frp 是一个高性能的反向代理应用,可以帮助你轻松的进行内网穿透,对外网提供服务。 +>frp 是一个高性能的反向代理应用,可以帮助你轻松的进行内网穿透,对外网提供服务,对于 http 服务还支持虚拟主机功能,访问80端口,可以根据域名路由到后端不同的 http 服务。 + +## frp 的作用? + +* 利用处于内网或防火墙后的机器,对外网环境提供 http 服务。 +* 对于 http 服务支持基于域名的虚拟主机,支持自定义域名绑定,使多个域名可以共用一个80端口。 +* 利用处于内网或防火墙后的机器,对外网环境提供 tcp 服务,例如在家里通过 ssh 访问公司局域网内的主机。 +* 可查看通过代理的所有 http 请求和响应的详细信息。(待开发) ## 开发状态 frp 目前正在前期开发阶段,master分支用于发布稳定版本,dev分支用于开发,您可以尝试下载最新的 release 版本进行测试。 -**在 1.x 版本以前,交互协议都可能会被改变,不能保证向后兼容。** +**在 1.0 版本以前,交互协议都可能会被改变,不能保证向后兼容。** ## 快速开始 -[QuickStart](doc/quick_start_en.md) | [使用文档](doc/quick_start_zh.md) +[使用文档](/doc/quick_start_zh.md) + +[tcp 端口转发](/doc/quick_start_zh.md#tcp-端口转发) + +[http 端口转发,自定义域名绑定](/doc/quick_start_zh.md#http-端口转发自定义域名绑定) ## 架构 -![architecture](doc/pic/architecture.png) - -## frp 的作用? - -* 利用处于内网或防火墙后的机器,对外网环境提供http服务。(针对http的优化正在开发中) -* 利用处于内网或防火墙后的机器,对外网环境提供tcp服务。 -* 可查看通过代理的所有http请求和响应信息。(待开发) +![architecture](/doc/pic/architecture.png) ## 贡献代码 -如果您对这个项目感兴趣,并且想要参与其中,我们非常欢迎! +如果您对这个项目感兴趣,我们非常欢迎您参与其中! * 如果您需要提交问题,可以通过 [issues](https://github.com/fatedier/frp/issues) 来完成。 * 如果您有新的功能需求,可以反馈至 fatedier@gmail.com 共同讨论。 diff --git a/conf/frpc.ini b/conf/frpc.ini index 86679e6d..9a1d8bc8 100644 --- a/conf/frpc.ini +++ b/conf/frpc.ini @@ -6,12 +6,27 @@ server_port = 7000 log_file = ./frpc.log # debug, info, warn, error log_level = info +log_max_days = 3 # for authentication auth_token = 123 -# test1 is the proxy name same as server's configuration -[test1] +# ssh is the proxy name same as server's configuration +[ssh] +# tcp | http, default is tcp +type = tcp local_ip = 127.0.0.1 local_port = 22 # true or false, if true, messages between frps and frpc will be encrypted, default is false use_encryption = true + +# 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, the domains are set in frps.ini +[web01] +type = http +local_ip = 127.0.0.1 +local_port = 80 +use_encryption = true + +[web02] +type = http +local_ip = 127.0.0.1 +local_port = 8000 diff --git a/conf/frps.ini b/conf/frps.ini index cfb9e7b7..91b06297 100644 --- a/conf/frps.ini +++ b/conf/frps.ini @@ -2,13 +2,28 @@ [common] bind_addr = 0.0.0.0 bind_port = 7000 +# optional +vhost_http_port = 80 # console or real logFile path like ./frps.log log_file = ./frps.log # debug, info, warn, error log_level = info +log_max_days = 3 -# test1 is the proxy name, client will use this name and auth_token to connect to server -[test1] +# ssh is the proxy name, client will use this name and auth_token to connect to server +[ssh] +type = tcp auth_token = 123 bind_addr = 0.0.0.0 listen_port = 6000 + +[web01] +type = http +auth_token = 123 +# if proxy type equals http, custom_domains must be set separated by commas +custom_domains = web01.yourdomain.com,web01.yourdomain2.com + +[web02] +type = http +auth_token = 123 +custom_domains = web02.yourdomain.com diff --git a/doc/quick_start_en.md b/doc/quick_start_en.md index 6b2a00c8..162ca182 100644 --- a/doc/quick_start_en.md +++ b/doc/quick_start_en.md @@ -2,7 +2,10 @@ frp is easier to use compared with other similar projects. -We will use a simple demo to demonstrate how to create a connection to server A's ssh port by server B with public IP address x.x.x.x(replace to the real IP address of your server). +We will use two simple demo to demonstrate how to use frp. + +1. How to create a connection to **server A**'s **ssh port** by **server B** with **public IP address** x.x.x.x(replace to the real IP address of your server). +2. How to visit web service in **server A**'s **8000 port** and **8001 port** by **web01.yourdomain.com** and **web02.yourdomain.com** through **server B** with public ID address. ### Download SourceCode @@ -10,6 +13,8 @@ We will use a simple demo to demonstrate how to create a connection to server A' Or you can use `git clone https://github.com/fatedier/frp.git $GOPATH/src/github.com/fatedier/frp`. +If you want to try it quickly, download the compiled program and configuration files from [https://github.com/fatedier/frp/releases](https://github.com/fatedier/frp/releases). + ### Compile Enter the root directory and execute `make`, then wait until finished. @@ -19,16 +24,18 @@ Enter the root directory and execute `make`, then wait until finished. ### Pre-requirement * Go environment. Version of go >= 1.4. -* Godep (if not exist, go get will be executed to download godep when compiling) +* Godep (if not exist, `go get` will be executed to download godep when compiling) ### Deploy -1. Move `./bin/frps` and `./conf/frps.ini` to any directory of server B. -2. Move `./bin/frpc` and `./conf/frpc.ini` to any directory of server A. +1. Move `./bin/frps` and `./conf/frps.ini` to any directory of **server B**. +2. Move `./bin/frpc` and `./conf/frpc.ini` to any directory of **server A**. 3. Modify all configuration files, details in next paragraph. -4. Execute `nohup ./frps &` or `nohup ./frps -c ./frps.ini &` in server B. -5. Execute `nohup ./frpc &` or `nohup ./frpc -c ./frpc.ini &` in server A. -6. Use `ssh -oPort=6000 {user}@x.x.x.x` to test if frp is work(replace {user} to real username in server A). +4. Execute `nohup ./frps &` or `nohup ./frps -c ./frps.ini &` in **server B**. +5. Execute `nohup ./frpc &` or `nohup ./frpc -c ./frpc.ini &` in **server A**. +6. Use `ssh -oPort=6000 {user}@x.x.x.x` to test if frp is work(replace {user} to real username in **server A**), or visit custom domains by browser. + +## Tcp port forwarding ### Configuration files @@ -42,8 +49,8 @@ bind_port = 7000 log_file = ./frps.log log_level = info -# test is the custom name of proxy and there can be many proxies with unique name in one configure file -[test] +# ssh is the custom name of proxy and there can be many proxies with unique name in one configure file +[ssh] auth_token = 123 bind_addr = 0.0.0.0 # finally we connect to server A by this port @@ -62,10 +69,67 @@ log_level = info # for authentication auth_token = 123 -# test is proxy name same with configure in frps.ini -[test] +# ssh is proxy name same with configure in frps.ini +[ssh] # local port which need to be transferred local_port = 22 # if use_encryption equals true, messages between frpc and frps will be encrypted, default is false use_encryption = true ``` + +## Http port forwarding and Custom domains binding + +If you only want to forward port one by one, you just need refer to [Tcp port forwarding](/doc/quick_start_en.md#Tcp-port-forwarding).If you want to visit different web pages deployed in different web servers by **server B**'s **80 port**, you should specify the type as **http**. + +You also need to resolve your **A record** of your custom domain to [server_addr], or resolve your **CNAME record** to [server_addr] if [server_addr] is a domain. + +After that, you can visit your web pages in local server by custom domains. + +### Configuration files + +#### frps.ini + +```ini +[common] +bind_addr = 0.0.0.0 +bind_port = 7000 +# if you want to support vhost, specify one port for http services +vhost_http_port = 80 +log_file = ./frps.log +log_level = info + +[web01] +type = http +auth_token = 123 +# # if proxy type equals http, custom_domains must be set separated by commas +custom_domains = web01.yourdomain.com + +[web02] +type = http +auth_token = 123 +custom_domains = web02.yourdomain.com +``` + +#### frpc.ini + +```ini +[common] +server_addr = x.x.x.x +server_port = 7000 +log_file = ./frpc.log +log_level = info +auth_token = 123 + +# custom domains are set in frps.ini +[web01] +type = http +local_ip = 127.0.0.1 +local_port = 8000 +# encryption is optional, default is false +use_encryption = true + +[web02] +type = http +local_ip = 127.0.0.1 +local_port = 8001 +``` diff --git a/doc/quick_start_zh.md b/doc/quick_start_zh.md index f48b55d7..d26538d9 100644 --- a/doc/quick_start_zh.md +++ b/doc/quick_start_zh.md @@ -1,6 +1,9 @@ # frp 使用文档 -frp 相比于其他项目而言非常易于部署和使用,这里我们用一个简单的示例演示如何通过一台拥有公网IP地址的服务器B,访问处于内网环境中的服务器A的ssh端口,服务器B的IP地址为 x.x.x.x(测试时替换为真实的IP地址)。 +相比于其他项目而言 frp 更易于部署和使用,这里我们用两个简单的示例来演示 frp 的使用过程。 + +1. 如何通过一台拥有公网IP地址的**服务器B**,访问处于公司内部网络环境中的**服务器A**的**ssh**端口,**服务器B**的IP地址为 x.x.x.x(测试时替换为真实的IP地址)。 +2. 如何利用一台拥有公网IP地址的**服务器B**,使通过 **web01.yourdomain.com** 可以访问内网环境中**服务器A**上**8000端口**的web服务,**web02.yourdomain.com** 可以访问**服务器A**上**8001端口**的web服务。 ### 下载源码 @@ -8,6 +11,8 @@ frp 相比于其他项目而言非常易于部署和使用,这里我们用一 或者可以使用 `git clone https://github.com/fatedier/frp.git $GOPATH/src/github.com/fatedier/frp` 拷贝到相应目录下。 +如果您想快速进行测试,也可以根据您服务器的操作系统及架构直接下载编译好的程序及示例配置文件,[https://github.com/fatedier/frp/releases](https://github.com/fatedier/frp/releases)。 + ### 编译 进入下载后的源码根目录,执行 `make` 命令,等待编译完成。 @@ -17,16 +22,20 @@ frp 相比于其他项目而言非常易于部署和使用,这里我们用一 ### 依赖 * go 1.4 以上版本 -* godep (如果检查不存在,编译时会通过 go get 命令安装) +* godep (如果检查不存在,编译时会通过 `go get` 命令安装) ### 部署 -1. 将 ./bin/frps 和 ./conf/frps.ini 拷贝至服务器B任意目录。 -2. 将 ./bin/frpc 和 ./conf/frpc.ini 拷贝至服务器A任意目录。 -3. 修改两边的配置文件,见下一节说明。 +1. 将 ./bin/frps 和 ./conf/frps.ini 拷贝至**服务器B**任意目录。 +2. 将 ./bin/frpc 和 ./conf/frpc.ini 拷贝至**服务器A**任意目录。 +3. 根据要实现的功能修改两边的配置文件,详细内容见后续章节说明。 4. 在服务器B执行 `nohup ./frps &` 或者 `nohup ./frps -c ./frps.ini &`。 5. 在服务器A执行 `nohup ./frpc &` 或者 `nohup ./frpc -c ./frpc.ini &`。 -6. 通过 `ssh -oPort=6000 {user}@x.x.x.x` 测试是否能够成功连接服务器A({user}替换为服务器A上存在的真实用户)。 +6. 通过 `ssh -oPort=6000 {user}@x.x.x.x` 测试是否能够成功连接**服务器A**({user}替换为**服务器A**上存在的真实用户),或通过浏览器访问自定义域名验证 http 服务是否转发成功。 + +## tcp 端口转发 + +转发 tcp 端口需要按照需求修改 frps 和 frpc 的配置文件。 ### 配置文件 @@ -40,9 +49,9 @@ bind_port = 7000 log_file = ./frps.log log_level = info -# test 为代理的自定义名称,可以有多个,不能重复,和frpc中名称对应 -[test] -auth_token = 123 +# ssh 为代理的自定义名称,可以有多个,不能重复,和frpc中名称对应 +[ssh] +auth_token = 123 bind_addr = 0.0.0.0 # 最后将通过此端口访问后端服务 listen_port = 6000 @@ -58,12 +67,71 @@ server_port = 7000 log_file = ./frpc.log log_level = info # 用于身份验证 -auth_token = 123 +auth_token = 123 -# test需要和 frps.ini 中配置一致 -[test] +# ssh 需要和 frps.ini 中配置一致 +[ssh] # 需要转发的本地端口 local_port = 22 # 启用加密,frpc与frps之间通信加密,默认为 false use_encryption = true ``` + +## http 端口转发,自定义域名绑定 + +如果只需要一对一的转发,例如**服务器B**的**80端口**转发**服务器A**的**8000端口**,则只需要配置 [tcp 端口转发](/doc/quick_start_zh.md#tcp-端口转发) 即可,如果需要使**服务器B**的**80端口**可以转发至**多个**web服务端口,则需要指定代理的类型为 http,并且在 frps 的配置文件中配置用于提供 http 转发服务的端口。 + +按照如下的内容修改配置文件后,需要将自定义域名的 **A 记录**解析到 [server_addr],如果 [server_addr] 是域名也可以将自定义域名的 **CNAME 记录**解析到 [server_addr]。 + +之后就可以通过自定义域名访问到本地的多个 web 服务。 + +### 配置文件 + +#### frps.ini + +```ini +[common] +bind_addr = 0.0.0.0 +bind_port = 7000 +# 如果需要支持http类型的代理则需要指定一个端口 +vhost_http_port = 80 +log_file = ./frps.log +log_level = info + +[web01] +# type 默认为 tcp,这里需要特别指定为 http +type = http +auth_token = 123 +# 自定义域名绑定,如果需要同时绑定多个以英文逗号分隔 +custom_domains = web01.yourdomain.com + +[web02] +type = http +auth_token = 123 +custom_domains = web02.yourdomain.com +``` + +#### frpc.ini + +```ini +[common] +server_addr = x.x.x.x +server_port = 7000 +log_file = ./frpc.log +log_level = info +auth_token = 123 + + +# 自定义域名在 frps.ini 中配置,方便做统一管理 +[web01] +type = http +local_ip = 127.0.0.1 +local_port = 8000 +# 可选是否加密 +use_encryption = true + +[web02] +type = http +local_ip = 127.0.0.1 +local_port = 8001 +``` diff --git a/src/frp/cmd/frpc/main.go b/src/frp/cmd/frpc/main.go index 2f8a7f12..9fbab56d 100644 --- a/src/frp/cmd/frpc/main.go +++ b/src/frp/cmd/frpc/main.go @@ -88,7 +88,7 @@ func main() { client.ServerPort = serverPort } - log.InitLog(client.LogWay, client.LogFile, client.LogLevel) + log.InitLog(client.LogWay, client.LogFile, client.LogLevel, client.LogMaxDays) // wait until all control goroutine exit var wait sync.WaitGroup diff --git a/src/frp/cmd/frps/control.go b/src/frp/cmd/frps/control.go index 6566cd73..c8c78271 100644 --- a/src/frp/cmd/frps/control.go +++ b/src/frp/cmd/frps/control.go @@ -30,7 +30,7 @@ import ( func ProcessControlConn(l *conn.Listener) { for { - c, err := l.GetConn() + c, err := l.Accept() if err != nil { return } diff --git a/src/frp/cmd/frps/main.go b/src/frp/cmd/frps/main.go index 4c918f06..a23f3fb9 100644 --- a/src/frp/cmd/frps/main.go +++ b/src/frp/cmd/frps/main.go @@ -19,6 +19,7 @@ import ( "os" "strconv" "strings" + "time" docopt "github.com/docopt/docopt-go" @@ -26,6 +27,7 @@ import ( "frp/utils/conn" "frp/utils/log" "frp/utils/version" + "frp/utils/vhost" ) var ( @@ -88,12 +90,25 @@ func main() { server.BindPort = bindPort } - log.InitLog(server.LogWay, server.LogFile, server.LogLevel) + log.InitLog(server.LogWay, server.LogFile, server.LogLevel, server.LogMaxDays) l, err := conn.Listen(server.BindAddr, server.BindPort) if err != nil { - log.Error("Create listener error, %v", err) - os.Exit(-1) + log.Error("Create server listener error, %v", err) + os.Exit(1) + } + + // create vhost if VhostHttpPort != 0 + if server.VhostHttpPort != 0 { + vhostListener, err := conn.Listen(server.BindAddr, server.VhostHttpPort) + if err != nil { + log.Error("Create vhost http listener error, %v", err) + os.Exit(1) + } + server.VhostMuxer, err = vhost.NewHttpMuxer(vhostListener, 30*time.Second) + if err != nil { + log.Error("Create vhost httpMuxer error, %v", err) + } } log.Info("Start frps success") diff --git a/src/frp/models/client/client.go b/src/frp/models/client/client.go index b0b947b8..6165eee1 100644 --- a/src/frp/models/client/client.go +++ b/src/frp/models/client/client.go @@ -31,6 +31,7 @@ type ProxyClient struct { AuthToken string LocalIp string LocalPort int64 + Type string UseEncryption bool } diff --git a/src/frp/models/client/config.go b/src/frp/models/client/config.go index 3c09c751..1d19c331 100644 --- a/src/frp/models/client/config.go +++ b/src/frp/models/client/config.go @@ -28,6 +28,7 @@ var ( LogFile string = "console" LogWay string = "console" LogLevel string = "info" + LogMaxDays int64 = 3 HeartBeatInterval int64 = 20 HeartBeatTimeout int64 = 90 ) @@ -69,6 +70,11 @@ func LoadConf(confFile string) (err error) { LogLevel = tmpStr } + tmpStr, ok = conf.Get("common", "log_max_days") + if ok { + LogMaxDays, _ = strconv.ParseInt(tmpStr, 10, 64) + } + var authToken string tmpStr, ok = conf.Get("common", "auth_token") if ok { @@ -105,6 +111,16 @@ func LoadConf(confFile string) (err error) { return fmt.Errorf("Parse ini file error: proxy [%s] local_port not found", proxyClient.Name) } + // type + proxyClient.Type = "tcp" + typeStr, ok := section["type"] + if ok { + if typeStr != "tcp" && typeStr != "http" { + return fmt.Errorf("Parse ini file error: proxy [%s] type error", proxyClient.Name) + } + proxyClient.Type = typeStr + } + // use_encryption proxyClient.UseEncryption = false useEncryptionStr, ok := section["use_encryption"] diff --git a/src/frp/models/server/config.go b/src/frp/models/server/config.go index 23fd9699..28e096cd 100644 --- a/src/frp/models/server/config.go +++ b/src/frp/models/server/config.go @@ -17,19 +17,26 @@ package server import ( "fmt" "strconv" + "strings" ini "github.com/vaughan0/go-ini" + + "frp/utils/vhost" ) // common config var ( BindAddr string = "0.0.0.0" BindPort int64 = 7000 + VhostHttpPort int64 = 0 // if VhostHttpPort equals 0, do not listen a public port for http LogFile string = "console" LogWay string = "console" // console or file LogLevel string = "info" + LogMaxDays int64 = 3 HeartBeatTimeout int64 = 90 UserConnTimeout int64 = 10 + + VhostMuxer *vhost.HttpMuxer ) var ProxyServers map[string]*ProxyServer = make(map[string]*ProxyServer) @@ -54,6 +61,13 @@ func LoadConf(confFile string) (err error) { BindPort, _ = strconv.ParseInt(tmpStr, 10, 64) } + tmpStr, ok = conf.Get("common", "vhost_http_port") + if ok { + VhostHttpPort, _ = strconv.ParseInt(tmpStr, 10, 64) + } else { + VhostHttpPort = 0 + } + tmpStr, ok = conf.Get("common", "log_file") if ok { LogFile = tmpStr @@ -69,30 +83,61 @@ func LoadConf(confFile string) (err error) { LogLevel = tmpStr } + tmpStr, ok = conf.Get("common", "log_max_days") + if ok { + LogMaxDays, _ = strconv.ParseInt(tmpStr, 10, 64) + } + // servers for name, section := range conf { if name != "common" { proxyServer := &ProxyServer{} + proxyServer.CustomDomains = make([]string, 0) proxyServer.Name = name + proxyServer.Type, ok = section["type"] + if ok { + if proxyServer.Type != "tcp" && proxyServer.Type != "http" { + return fmt.Errorf("Parse ini file error: proxy [%s] type error", proxyServer.Name) + } + } else { + proxyServer.Type = "tcp" + } + proxyServer.AuthToken, ok = section["auth_token"] if !ok { return fmt.Errorf("Parse ini file error: proxy [%s] no auth_token found", proxyServer.Name) } - proxyServer.BindAddr, ok = section["bind_addr"] - if !ok { - proxyServer.BindAddr = "0.0.0.0" - } - - portStr, ok := section["listen_port"] - if ok { - proxyServer.ListenPort, err = strconv.ParseInt(portStr, 10, 64) - if err != nil { - return fmt.Errorf("Parse ini file error: proxy [%s] listen_port error", proxyServer.Name) + // for tcp + if proxyServer.Type == "tcp" { + proxyServer.BindAddr, ok = section["bind_addr"] + if !ok { + proxyServer.BindAddr = "0.0.0.0" + } + + portStr, ok := section["listen_port"] + if ok { + proxyServer.ListenPort, err = strconv.ParseInt(portStr, 10, 64) + if err != nil { + return fmt.Errorf("Parse ini file error: proxy [%s] listen_port error", proxyServer.Name) + } + } else { + return fmt.Errorf("Parse ini file error: proxy [%s] listen_port not found", proxyServer.Name) + } + } else if proxyServer.Type == "http" { + // for http + domainStr, ok := section["custom_domains"] + if ok { + var suffix string + if VhostHttpPort != 80 { + suffix = fmt.Sprintf(":%d", VhostHttpPort) + } + proxyServer.CustomDomains = strings.Split(domainStr, ",") + for i, domain := range proxyServer.CustomDomains { + proxyServer.CustomDomains[i] = strings.ToLower(strings.TrimSpace(domain)) + suffix + } } - } else { - return fmt.Errorf("Parse ini file error: proxy [%s] listen_port not found", proxyServer.Name) } proxyServer.Init() diff --git a/src/frp/models/server/server.go b/src/frp/models/server/server.go index d6e06daf..adcbaecf 100644 --- a/src/frp/models/server/server.go +++ b/src/frp/models/server/server.go @@ -24,15 +24,22 @@ import ( "frp/utils/log" ) +type Listener interface { + Accept() (*conn.Conn, error) + Close() error +} + type ProxyServer struct { Name string AuthToken string - UseEncryption bool + Type string BindAddr string ListenPort int64 - Status int64 + UseEncryption bool + CustomDomains []string - listener *conn.Listener // accept new connection from remote users + Status int64 + listeners []Listener // accept new connection from remote users ctlMsgChan chan int64 // every time accept a new user conn, put "1" to the channel workConnChan chan *conn.Conn // get new work conns from control goroutine userConnList *list.List // store user conns @@ -44,6 +51,7 @@ func (p *ProxyServer) Init() { p.workConnChan = make(chan *conn.Conn) p.ctlMsgChan = make(chan int64) p.userConnList = list.New() + p.listeners = make([]Listener, 0) } func (p *ProxyServer) Lock() { @@ -57,57 +65,71 @@ func (p *ProxyServer) Unlock() { // start listening for user conns func (p *ProxyServer) Start() (err error) { p.Init() - p.listener, err = conn.Listen(p.BindAddr, p.ListenPort) - if err != nil { - return err + if p.Type == "tcp" { + l, err := conn.Listen(p.BindAddr, p.ListenPort) + if err != nil { + return err + } + p.listeners = append(p.listeners, l) + } else if p.Type == "http" { + for _, domain := range p.CustomDomains { + l, err := VhostMuxer.Listen(domain) + if err != nil { + return err + } + p.listeners = append(p.listeners, l) + } } p.Status = consts.Working // start a goroutine for listener to accept user connection - go func() { - for { - // block - // if listener is closed, err returned - c, err := p.listener.GetConn() - if err != nil { - log.Info("ProxyName [%s], listener is closed", p.Name) - return - } - log.Debug("ProxyName [%s], get one new user conn [%s]", p.Name, c.GetRemoteAddr()) - - // insert into list - p.Lock() - if p.Status != consts.Working { - log.Debug("ProxyName [%s] is not working, new user conn close", p.Name) - c.Close() - p.Unlock() - return - } - p.userConnList.PushBack(c) - p.Unlock() - - // put msg to control conn - p.ctlMsgChan <- 1 - - // set timeout - time.AfterFunc(time.Duration(UserConnTimeout)*time.Second, func() { - p.Lock() - defer p.Unlock() - element := p.userConnList.Front() - if element == nil { + for _, listener := range p.listeners { + go func(l Listener) { + for { + // block + // if listener is closed, err returned + c, err := l.Accept() + if err != nil { + log.Info("ProxyName [%s], listener is closed", p.Name) return } + log.Debug("ProxyName [%s], get one new user conn [%s]", p.Name, c.GetRemoteAddr()) - userConn := element.Value.(*conn.Conn) - if userConn == c { - log.Warn("ProxyName [%s], user conn [%s] timeout", p.Name, c.GetRemoteAddr()) + // insert into list + p.Lock() + if p.Status != consts.Working { + log.Debug("ProxyName [%s] is not working, new user conn close", p.Name) + c.Close() + p.Unlock() + return } - }) - } - }() + p.userConnList.PushBack(c) + p.Unlock() - // start another goroutine for join two conns from client and user + // put msg to control conn + p.ctlMsgChan <- 1 + + // set timeout + time.AfterFunc(time.Duration(UserConnTimeout)*time.Second, func() { + p.Lock() + element := p.userConnList.Front() + p.Unlock() + if element == nil { + return + } + + userConn := element.Value.(*conn.Conn) + if userConn == c { + log.Warn("ProxyName [%s], user conn [%s] timeout", p.Name, c.GetRemoteAddr()) + userConn.Close() + } + }) + } + }(listener) + } + + // start another goroutine for join two conns from frpc and user go func() { for { workConn, ok := <-p.workConnChan @@ -149,8 +171,12 @@ func (p *ProxyServer) Close() { p.Lock() if p.Status != consts.Closed { p.Status = consts.Closed - if p.listener != nil { - p.listener.Close() + if len(p.listeners) != 0 { + for _, l := range p.listeners { + if l != nil { + l.Close() + } + } } close(p.ctlMsgChan) close(p.workConnChan) diff --git a/src/frp/utils/conn/conn.go b/src/frp/utils/conn/conn.go index eb064c4a..29b4d22f 100644 --- a/src/frp/utils/conn/conn.go +++ b/src/frp/utils/conn/conn.go @@ -20,6 +20,7 @@ import ( "io" "net" "sync" + "time" "frp/utils/log" "frp/utils/pcrypto" @@ -28,7 +29,7 @@ import ( type Listener struct { addr net.Addr l *net.TCPListener - conns chan *Conn + accept chan *Conn closeFlag bool } @@ -42,7 +43,7 @@ func Listen(bindAddr string, bindPort int64) (l *Listener, err error) { l = &Listener{ addr: listener.Addr(), l: listener, - conns: make(chan *Conn), + accept: make(chan *Conn), closeFlag: false, } @@ -61,7 +62,7 @@ func Listen(bindAddr string, bindPort int64) (l *Listener, err error) { closeFlag: false, } c.Reader = bufio.NewReader(c.TcpConn) - l.conns <- c + l.accept <- c } }() return l, err @@ -69,30 +70,38 @@ func Listen(bindAddr string, bindPort int64) (l *Listener, err error) { // wait util get one new connection or listener is closed // if listener is closed, err returned -func (l *Listener) GetConn() (conn *Conn, err error) { - var ok bool - conn, ok = <-l.conns +func (l *Listener) Accept() (*Conn, error) { + conn, ok := <-l.accept if !ok { return conn, fmt.Errorf("channel close") } return conn, nil } -func (l *Listener) Close() { +func (l *Listener) Close() error { if l.l != nil && l.closeFlag == false { l.closeFlag = true l.l.Close() - close(l.conns) + close(l.accept) } + return nil } // wrap for TCPConn type Conn struct { - TcpConn *net.TCPConn + TcpConn net.Conn Reader *bufio.Reader closeFlag bool } +func NewConn(conn net.Conn) (c *Conn) { + c = &Conn{} + c.TcpConn = conn + c.Reader = bufio.NewReader(c.TcpConn) + c.closeFlag = false + return c +} + func ConnectServer(host string, port int64) (c *Conn, err error) { c = &Conn{} servertAddr, err := net.ResolveTCPAddr("tcp4", fmt.Sprintf("%s:%d", host, port)) @@ -131,6 +140,11 @@ func (c *Conn) Write(content string) (err error) { } +func (c *Conn) SetDeadline(t time.Time) error { + err := c.TcpConn.SetDeadline(t) + return err +} + func (c *Conn) Close() { if c.TcpConn != nil && c.closeFlag == false { c.closeFlag = true diff --git a/src/frp/utils/log/log.go b/src/frp/utils/log/log.go index ed66ca08..3e751a73 100644 --- a/src/frp/utils/log/log.go +++ b/src/frp/utils/log/log.go @@ -15,6 +15,7 @@ package log import ( + "fmt" "github.com/astaxie/beego/logs" ) @@ -26,24 +27,24 @@ func init() { Log.SetLogFuncCallDepth(Log.GetLogFuncCallDepth() + 1) } -func InitLog(logWay string, logFile string, logLevel string) { - SetLogFile(logWay, logFile) +func InitLog(logWay string, logFile string, logLevel string, maxdays int64) { + SetLogFile(logWay, logFile, maxdays) SetLogLevel(logLevel) } -// logWay: such as file or console -func SetLogFile(logWay string, logFile string) { +// logWay: file or console +func SetLogFile(logWay string, logFile string, maxdays int64) { if logWay == "console" { Log.SetLogger("console", "") } else { - Log.SetLogger("file", `{"filename": "`+logFile+`"}`) + params := fmt.Sprintf(`{"filename": "%s", "maxdays": %d}`, logFile, maxdays) + Log.SetLogger("file", params) } } // value: error, warning, info, debug func SetLogLevel(logLevel string) { level := 4 // warning - switch logLevel { case "error": level = 3 @@ -56,7 +57,6 @@ func SetLogLevel(logLevel string) { default: level = 4 } - Log.SetLevel(level) } diff --git a/src/frp/utils/version/version.go b/src/frp/utils/version/version.go index 075a4c8d..7eb5272f 100644 --- a/src/frp/utils/version/version.go +++ b/src/frp/utils/version/version.go @@ -19,7 +19,7 @@ import ( "strings" ) -var version string = "0.3.0" +var version string = "0.5.0" func Full() string { return version diff --git a/src/frp/utils/vhost/vhost.go b/src/frp/utils/vhost/vhost.go new file mode 100644 index 00000000..d832f537 --- /dev/null +++ b/src/frp/utils/vhost/vhost.go @@ -0,0 +1,193 @@ +// Copyright 2016 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 vhost + +import ( + "bufio" + "bytes" + "fmt" + "io" + "net" + "net/http" + "strings" + "sync" + "time" + + "frp/utils/conn" +) + +type muxFunc func(*conn.Conn) (net.Conn, string, error) + +type VhostMuxer struct { + listener *conn.Listener + timeout time.Duration + vhostFunc muxFunc + registryMap map[string]*Listener + mutex sync.RWMutex +} + +func NewVhostMuxer(listener *conn.Listener, vhostFunc muxFunc, timeout time.Duration) (mux *VhostMuxer, err error) { + mux = &VhostMuxer{ + listener: listener, + timeout: timeout, + vhostFunc: vhostFunc, + registryMap: make(map[string]*Listener), + } + go mux.run() + return mux, nil +} + +func (v *VhostMuxer) Listen(name string) (l *Listener, err error) { + v.mutex.Lock() + defer v.mutex.Unlock() + if _, exist := v.registryMap[name]; exist { + return nil, fmt.Errorf("name %s is already bound", name) + } + + l = &Listener{ + name: name, + mux: v, + accept: make(chan *conn.Conn), + } + v.registryMap[name] = l + return l, nil +} + +func (v *VhostMuxer) getListener(name string) (l *Listener, exist bool) { + v.mutex.RLock() + defer v.mutex.RUnlock() + l, exist = v.registryMap[name] + return l, exist +} + +func (v *VhostMuxer) unRegister(name string) { + v.mutex.Lock() + defer v.mutex.Unlock() + delete(v.registryMap, name) +} + +func (v *VhostMuxer) run() { + for { + conn, err := v.listener.Accept() + if err != nil { + return + } + go v.handle(conn) + } +} + +func (v *VhostMuxer) handle(c *conn.Conn) { + if err := c.SetDeadline(time.Now().Add(v.timeout)); err != nil { + return + } + + sConn, name, err := v.vhostFunc(c) + if err != nil { + return + } + + name = strings.ToLower(name) + + l, ok := v.getListener(name) + if !ok { + return + } + + if err = sConn.SetDeadline(time.Time{}); err != nil { + return + } + c.TcpConn = sConn + + l.accept <- c +} + +type HttpMuxer struct { + *VhostMuxer +} + +func GetHttpHostname(c *conn.Conn) (_ net.Conn, routerName string, err error) { + sc, rd := newShareConn(c.TcpConn) + + request, err := http.ReadRequest(bufio.NewReader(rd)) + if err != nil { + return sc, "", err + } + routerName = request.Host + request.Body.Close() + + return sc, routerName, nil +} + +func NewHttpMuxer(listener *conn.Listener, timeout time.Duration) (*HttpMuxer, error) { + mux, err := NewVhostMuxer(listener, GetHttpHostname, timeout) + return &HttpMuxer{mux}, err +} + +type Listener struct { + name string + mux *VhostMuxer // for closing VhostMuxer + accept chan *conn.Conn +} + +func (l *Listener) Accept() (*conn.Conn, error) { + conn, ok := <-l.accept + if !ok { + return nil, fmt.Errorf("Listener closed") + } + return conn, nil +} + +func (l *Listener) Close() error { + l.mux.unRegister(l.name) + close(l.accept) + return nil +} + +func (l *Listener) Name() string { + return l.name +} + +type sharedConn struct { + net.Conn + sync.Mutex + buff *bytes.Buffer +} + +func newShareConn(conn net.Conn) (*sharedConn, io.Reader) { + sc := &sharedConn{ + Conn: conn, + buff: bytes.NewBuffer(make([]byte, 0, 1024)), + } + return sc, io.TeeReader(conn, sc.buff) +} + +func (sc *sharedConn) Read(p []byte) (n int, err error) { + sc.Lock() + if sc.buff == nil { + sc.Unlock() + return sc.Conn.Read(p) + } + n, err = sc.buff.Read(p) + + if err == io.EOF { + sc.buff = nil + var n2 int + n2, err = sc.Conn.Read(p[n:]) + + n += n2 + } + sc.Unlock() + return +}