diff --git a/README.md b/README.md index 29d63cb..5fff2b7 100644 --- a/README.md +++ b/README.md @@ -22,6 +22,7 @@ Now it also try to support p2p connect. * [Forward DNS query request](#forward-dns-query-request) * [Forward unix domain socket](#forward-unix-domain-socket) * [Expose a simple http file server](#expose-a-simple-http-file-server) + * [Enable HTTPS for local HTTP service](#enable-https-for-local-http-service) * [Expose your service in security](#expose-your-service-in-security) * [P2P Mode](#p2p-mode) * [Features](#features) @@ -44,6 +45,8 @@ Now it also try to support p2p connect. * [Rewriting the Host Header](#rewriting-the-host-header) * [Set Headers In HTTP Request](#set-headers-in-http-request) * [Get Real IP](#get-real-ip) + * [HTTP X-Forwarded-For](#http-x-forwarded-for) + * [Proxy Protocol](#proxy-protocol) * [Password protecting your web service](#password-protecting-your-web-service) * [Custom subdomain names](#custom-subdomain-names) * [URL routing](#url-routing) @@ -243,11 +246,34 @@ Configure frps same as above. 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`. +### Enable HTTPS for local HTTP service + +1. Start frpc with configurations: + + ```ini + # frpc.ini + [common] + server_addr = x.x.x.x + server_port = 7000 + + [test_htts2http] + type = https + custom_domains = test.yourdomain.com + + plugin = https2http + plugin_local_addr = 127.0.0.1:80 + plugin_crt_path = ./server.crt + plugin_key_path = ./server.key + plugin_host_header_rewrite = 127.0.0.1 + ``` + +2. Visit `https://test.yourdomain.com`. + ### Expose your service in security For some services, if expose them to the public network directly will be a security risk. -**stcp(secret tcp)** help you create a proxy avoiding any one can access it. +**stcp(secret tcp)** helps you create a proxy avoiding any one can access it. Configure frps same as above. @@ -484,8 +510,6 @@ tcp_mux = false ### Support KCP Protocol -frp support kcp protocol since v0.12.0. - KCP is a fast and reliable protocol that can achieve the transmission effect of a reduction of the average latency by 30% to 40% and reduction of the maximum delay by a factor of three, at the cost of 10% to 20% more bandwidth wasted than TCP. Using kcp in frp: @@ -639,9 +663,32 @@ In this example, it will set header `X-From-Where: frp` to http request. ### Get Real IP +#### HTTP X-Forwarded-For + Features for http proxy only. -You can get user's real IP from http request header `X-Forwarded-For` and `X-Real-IP`. +You can get user's real IP from HTTP request header `X-Forwarded-For` and `X-Real-IP`. + +#### Proxy Protocol + +frp support Proxy Protocol to send user's real IP to local service. It support all types without UDP. + +Here is an example for https service: + +```ini +# frpc.ini +[web] +type = https +local_port = 443 +custom_domains = test.yourdomain.com + +# now v1 and v2 is supported +proxy_protocol_version = v2 +``` + +You can enable Proxy Protocol support in nginx to parse user's real IP to http header `X-Real-IP`. + +Then you can get it from HTTP request header in your local service. ### Password protecting your web service diff --git a/README_zh.md b/README_zh.md index 952aeb6..7fdaa0f 100644 --- a/README_zh.md +++ b/README_zh.md @@ -18,6 +18,7 @@ frp 是一个可用于内网穿透的高性能的反向代理应用,支持 tcp * [转发 DNS 查询请求](#转发-dns-查询请求) * [转发 Unix域套接字](#转发-unix域套接字) * [对外提供简单的文件访问服务](#对外提供简单的文件访问服务) + * [为本地 HTTP 服务启用 HTTPS](#为本地-http-服务启用-https) * [安全地暴露内网服务](#安全地暴露内网服务) * [点对点内网穿透](#点对点内网穿透) * [功能说明](#功能说明) @@ -40,6 +41,8 @@ frp 是一个可用于内网穿透的高性能的反向代理应用,支持 tcp * [修改 Host Header](#修改-host-header) * [设置 HTTP 请求的 header](#设置-http-请求的-header) * [获取用户真实 IP](#获取用户真实-ip) + * [HTTP X-Forwarded-For](#http-x-forwarded-for) + * [Proxy Protocol](#proxy-protocol) * [通过密码保护你的 web 服务](#通过密码保护你的-web-服务) * [自定义二级域名](#自定义二级域名) * [URL 路由](#url-路由) @@ -244,6 +247,33 @@ frps 的部署步骤同上。 2. 通过浏览器访问 `http://x.x.x.x:6000/static/` 来查看位于 `/tmp/file` 目录下的文件,会要求输入已设置好的用户名和密码。 +### 为本地 HTTP 服务启用 HTTPS + +通过 `https2http` 插件可以让本地 HTTP 服务转换成 HTTPS 服务对外提供。 + +1. 启用 frpc,启用 `https2http` 插件,配置如下: + + ```ini + # frpc.ini + [common] + server_addr = x.x.x.x + server_port = 7000 + + [test_htts2http] + type = https + custom_domains = test.yourdomain.com + + plugin = https2http + plugin_local_addr = 127.0.0.1:80 + + # HTTPS 证书相关的配置 + plugin_crt_path = ./server.crt + plugin_key_path = ./server.key + plugin_host_header_rewrite = 127.0.0.1 + ``` + +2. 通过浏览器访问 `https://test.yourdomain.com` 即可。 + ### 安全地暴露内网服务 对于某些服务来说如果直接暴露于公网上将会存在安全隐患。 @@ -514,7 +544,7 @@ tcp_mux = false ### 底层通信可选 kcp 协议 -从 v0.12.0 版本开始,底层通信协议支持选择 kcp 协议,在弱网环境下传输效率提升明显,但是会有一些额外的流量消耗。 +底层通信协议支持选择 kcp 协议,在弱网环境下传输效率提升明显,但是会有一些额外的流量消耗。 开启 kcp 协议支持: @@ -566,6 +596,7 @@ tcp_mux = false ### 负载均衡 可以将多个相同类型的 proxy 加入到同一个 group 中,从而实现负载均衡的功能。 + 目前只支持 tcp 类型的 proxy。 ```ini @@ -668,7 +699,34 @@ header_X-From-Where = frp ### 获取用户真实 IP -目前只有 **http** 类型的代理支持这一功能,可以通过用户请求的 header 中的 `X-Forwarded-For` 和 `X-Real-IP` 来获取用户真实 IP。 +#### HTTP X-Forwarded-For + +目前只有 **http** 类型的代理支持这一功能,可以通过用户请求的 header 中的 `X-Forwarded-For` 来获取用户真实 IP,默认启用。 + +#### Proxy Protocol + +frp 支持通过 **Proxy Protocol** 协议来传递经过 frp 代理的请求的真实 IP,此功能支持所有以 TCP 为底层协议的类型,不支持 UDP。 + +**Proxy Protocol** 功能启用后,frpc 在和本地服务建立连接后,会先发送一段 **Proxy Protocol** 的协议内容给本地服务,本地服务通过解析这一内容可以获得访问用户的真实 IP。所以不仅仅是 HTTP 服务,任何的 TCP 服务,只要支持这一协议,都可以获得用户的真实 IP 地址。 + +需要注意的是,在代理配置中如果要启用此功能,需要本地的服务能够支持 **Proxy Protocol** 这一协议,目前 nginx 和 haproxy 都能够很好的支持。 + +这里以 https 类型为例: + +```ini +# frpc.ini +[web] +type = https +local_port = 443 +custom_domains = test.yourdomain.com + +# 目前支持 v1 和 v2 两个版本的 proxy protocol 协议。 +proxy_protocol_version = v2 +``` + +只需要在代理配置中增加一行 `proxy_protocol_version = v2` 即可开启此功能。 + +本地的 https 服务可以通过在 nginx 的配置中启用 **Proxy Protocol** 的解析并将结果设置在 `X-Real-IP` 这个 Header 中就可以在自己的 Web 服务中通过 `X-Real-IP` 获取到用户的真实 IP。 ### 通过密码保护你的 web 服务 diff --git a/client/control.go b/client/control.go index bbcece6..6a12b8c 100644 --- a/client/control.go +++ b/client/control.go @@ -131,7 +131,7 @@ func (ctl *Control) HandleReqWorkConn(inMsg *msg.ReqWorkConn) { workConn.AddLogPrefix(startMsg.ProxyName) // dispatch this work connection to related proxy - ctl.pm.HandleWorkConn(startMsg.ProxyName, workConn) + ctl.pm.HandleWorkConn(startMsg.ProxyName, workConn, &startMsg) } func (ctl *Control) HandleNewProxyResp(inMsg *msg.NewProxyResp) { @@ -148,6 +148,9 @@ func (ctl *Control) HandleNewProxyResp(inMsg *msg.NewProxyResp) { func (ctl *Control) Close() error { ctl.pm.Close() ctl.conn.Close() + if ctl.session != nil { + ctl.session.Close() + } return nil } @@ -202,6 +205,7 @@ func (ctl *Control) reader() { return } else { ctl.Warn("read error: %v", err) + ctl.conn.Close() return } } else { @@ -300,6 +304,9 @@ func (ctl *Control) worker() { ctl.vm.Close() close(ctl.closedDoneCh) + if ctl.session != nil { + ctl.session.Close() + } return } } diff --git a/client/proxy/proxy.go b/client/proxy/proxy.go index 55e87a7..c68fe70 100644 --- a/client/proxy/proxy.go +++ b/client/proxy/proxy.go @@ -37,6 +37,7 @@ import ( frpIo "github.com/fatedier/golib/io" "github.com/fatedier/golib/pool" fmux "github.com/hashicorp/yamux" + pp "github.com/pires/go-proxyproto" ) // Proxy defines how to handle work connections for different proxy type. @@ -44,7 +45,7 @@ type Proxy interface { Run() error // InWorkConn accept work connections registered to server. - InWorkConn(conn frpNet.Conn) + InWorkConn(frpNet.Conn, *msg.StartWorkConn) Close() log.Logger @@ -119,9 +120,9 @@ func (pxy *TcpProxy) Close() { } } -func (pxy *TcpProxy) InWorkConn(conn frpNet.Conn) { +func (pxy *TcpProxy) InWorkConn(conn frpNet.Conn, m *msg.StartWorkConn) { HandleTcpWorkConnection(&pxy.cfg.LocalSvrConf, pxy.proxyPlugin, &pxy.cfg.BaseProxyConf, conn, - []byte(g.GlbClientCfg.Token)) + []byte(g.GlbClientCfg.Token), m) } // HTTP @@ -148,9 +149,9 @@ func (pxy *HttpProxy) Close() { } } -func (pxy *HttpProxy) InWorkConn(conn frpNet.Conn) { +func (pxy *HttpProxy) InWorkConn(conn frpNet.Conn, m *msg.StartWorkConn) { HandleTcpWorkConnection(&pxy.cfg.LocalSvrConf, pxy.proxyPlugin, &pxy.cfg.BaseProxyConf, conn, - []byte(g.GlbClientCfg.Token)) + []byte(g.GlbClientCfg.Token), m) } // HTTPS @@ -177,9 +178,9 @@ func (pxy *HttpsProxy) Close() { } } -func (pxy *HttpsProxy) InWorkConn(conn frpNet.Conn) { +func (pxy *HttpsProxy) InWorkConn(conn frpNet.Conn, m *msg.StartWorkConn) { HandleTcpWorkConnection(&pxy.cfg.LocalSvrConf, pxy.proxyPlugin, &pxy.cfg.BaseProxyConf, conn, - []byte(g.GlbClientCfg.Token)) + []byte(g.GlbClientCfg.Token), m) } // STCP @@ -206,9 +207,9 @@ func (pxy *StcpProxy) Close() { } } -func (pxy *StcpProxy) InWorkConn(conn frpNet.Conn) { +func (pxy *StcpProxy) InWorkConn(conn frpNet.Conn, m *msg.StartWorkConn) { HandleTcpWorkConnection(&pxy.cfg.LocalSvrConf, pxy.proxyPlugin, &pxy.cfg.BaseProxyConf, conn, - []byte(g.GlbClientCfg.Token)) + []byte(g.GlbClientCfg.Token), m) } // XTCP @@ -235,7 +236,7 @@ func (pxy *XtcpProxy) Close() { } } -func (pxy *XtcpProxy) InWorkConn(conn frpNet.Conn) { +func (pxy *XtcpProxy) InWorkConn(conn frpNet.Conn, m *msg.StartWorkConn) { defer conn.Close() var natHoleSidMsg msg.NatHoleSid err := msg.ReadMsgInto(conn, &natHoleSidMsg) @@ -353,7 +354,7 @@ func (pxy *XtcpProxy) InWorkConn(conn frpNet.Conn) { } HandleTcpWorkConnection(&pxy.cfg.LocalSvrConf, pxy.proxyPlugin, &pxy.cfg.BaseProxyConf, - frpNet.WrapConn(muxConn), []byte(pxy.cfg.Sk)) + frpNet.WrapConn(muxConn), []byte(pxy.cfg.Sk), m) } func (pxy *XtcpProxy) sendDetectMsg(addr string, port int, laddr *net.UDPAddr, content []byte) (err error) { @@ -415,7 +416,7 @@ func (pxy *UdpProxy) Close() { } } -func (pxy *UdpProxy) InWorkConn(conn frpNet.Conn) { +func (pxy *UdpProxy) InWorkConn(conn frpNet.Conn, m *msg.StartWorkConn) { pxy.Info("incoming a new work connection for udp proxy, %s", conn.RemoteAddr().String()) // close resources releated with old workConn pxy.Close() @@ -482,7 +483,7 @@ func (pxy *UdpProxy) InWorkConn(conn frpNet.Conn) { // Common handler for tcp work connections. func HandleTcpWorkConnection(localInfo *config.LocalSvrConf, proxyPlugin plugin.Plugin, - baseInfo *config.BaseProxyConf, workConn frpNet.Conn, encKey []byte) { + baseInfo *config.BaseProxyConf, workConn frpNet.Conn, encKey []byte, m *msg.StartWorkConn) { var ( remote io.ReadWriteCloser @@ -518,6 +519,37 @@ func HandleTcpWorkConnection(localInfo *config.LocalSvrConf, proxyPlugin plugin. workConn.Debug("join connections, localConn(l[%s] r[%s]) workConn(l[%s] r[%s])", localConn.LocalAddr().String(), localConn.RemoteAddr().String(), workConn.LocalAddr().String(), workConn.RemoteAddr().String()) + + // check if we need to send proxy protocol info + if baseInfo.ProxyProtocolVersion != "" { + if m.SrcAddr != "" && m.SrcPort != 0 { + if m.DstAddr == "" { + m.DstAddr = "127.0.0.1" + } + h := &pp.Header{ + Command: pp.PROXY, + SourceAddress: net.ParseIP(m.SrcAddr), + SourcePort: m.SrcPort, + DestinationAddress: net.ParseIP(m.DstAddr), + DestinationPort: m.DstPort, + } + + if h.SourceAddress.To16() == nil { + h.TransportProtocol = pp.TCPv4 + } else { + h.TransportProtocol = pp.TCPv6 + } + + if baseInfo.ProxyProtocolVersion == "v1" { + h.Version = 1 + } else if baseInfo.ProxyProtocolVersion == "v2" { + h.Version = 2 + } + + h.WriteTo(localConn) + } + } + frpIo.Join(localConn, remote) workConn.Debug("join connections closed") } diff --git a/client/proxy/proxy_manager.go b/client/proxy/proxy_manager.go index 6f0bb41..fb230b3 100644 --- a/client/proxy/proxy_manager.go +++ b/client/proxy/proxy_manager.go @@ -58,12 +58,12 @@ func (pm *ProxyManager) Close() { pm.proxies = make(map[string]*ProxyWrapper) } -func (pm *ProxyManager) HandleWorkConn(name string, workConn frpNet.Conn) { +func (pm *ProxyManager) HandleWorkConn(name string, workConn frpNet.Conn, m *msg.StartWorkConn) { pm.mu.RLock() pw, ok := pm.proxies[name] pm.mu.RUnlock() if ok { - pw.InWorkConn(workConn) + pw.InWorkConn(workConn, m) } else { workConn.Close() } diff --git a/client/proxy/proxy_wrapper.go b/client/proxy/proxy_wrapper.go index f95144c..0b29e48 100644 --- a/client/proxy/proxy_wrapper.go +++ b/client/proxy/proxy_wrapper.go @@ -217,13 +217,13 @@ func (pw *ProxyWrapper) statusFailedCallback() { pw.Info("health check failed") } -func (pw *ProxyWrapper) InWorkConn(workConn frpNet.Conn) { +func (pw *ProxyWrapper) InWorkConn(workConn frpNet.Conn, m *msg.StartWorkConn) { pw.mu.RLock() pxy := pw.pxy pw.mu.RUnlock() if pxy != nil { workConn.Debug("start a new work connection, localAddr: %s remoteAddr: %s", workConn.LocalAddr().String(), workConn.RemoteAddr().String()) - go pxy.InWorkConn(workConn) + go pxy.InWorkConn(workConn, m) } else { workConn.Close() } diff --git a/cmd/frpc/sub/root.go b/cmd/frpc/sub/root.go index 00e3369..61d9b3d 100644 --- a/cmd/frpc/sub/root.go +++ b/cmd/frpc/sub/root.go @@ -73,7 +73,7 @@ var ( ) func init() { - rootCmd.PersistentFlags().StringVarP(&cfgFile, "", "c", "./frpc.ini", "config file of frpc") + rootCmd.PersistentFlags().StringVarP(&cfgFile, "config", "c", "./frpc.ini", "config file of frpc") rootCmd.PersistentFlags().BoolVarP(&showVersion, "version", "v", false, "version of frpc") kcpDoneCh = make(chan struct{}) diff --git a/cmd/frps/root.go b/cmd/frps/root.go index cc7e0e1..e8def79 100644 --- a/cmd/frps/root.go +++ b/cmd/frps/root.go @@ -62,7 +62,7 @@ var ( ) func init() { - rootCmd.PersistentFlags().StringVarP(&cfgFile, "", "c", "", "config file of frps") + rootCmd.PersistentFlags().StringVarP(&cfgFile, "config", "c", "", "config file of frps") rootCmd.PersistentFlags().BoolVarP(&showVersion, "version", "v", false, "version of frpc") rootCmd.PersistentFlags().StringVarP(&bindAddr, "bind_addr", "", "0.0.0.0", "bind address") diff --git a/conf/frpc_full.ini b/conf/frpc_full.ini index 60bf859..e0e523c 100644 --- a/conf/frpc_full.ini +++ b/conf/frpc_full.ini @@ -154,6 +154,9 @@ use_encryption = false use_compression = false subdomain = web01 custom_domains = web02.yourdomain.com +# if not empty, frpc will use proxy protocol to transfer connection info to your local service +# v1 or v2 or empty +proxy_protocol_version = v2 [plugin_unix_domain_socket] type = tcp @@ -187,6 +190,15 @@ plugin_strip_prefix = static plugin_http_user = abc plugin_http_passwd = abc +[plugin_https2http] +type = https +custom_domains = test.yourdomain.com +plugin = https2http +plugin_local_addr = 127.0.0.1:80 +plugin_crt_path = ./server.crt +plugin_key_path = ./server.key +plugin_host_header_rewrite = 127.0.0.1 + [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/go.mod b/go.mod index 9bd172b..b9a6493 100644 --- a/go.mod +++ b/go.mod @@ -17,6 +17,7 @@ require ( github.com/klauspost/cpuid v1.2.0 // indirect github.com/klauspost/reedsolomon v1.9.1 // indirect github.com/mattn/go-runewidth v0.0.4 // indirect + github.com/pires/go-proxyproto v0.0.0-20190111085350-4d51b51e3bfc github.com/pkg/errors v0.8.0 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect github.com/rakyll/statik v0.1.1 diff --git a/go.sum b/go.sum index d8d783d..64df382 100644 --- a/go.sum +++ b/go.sum @@ -20,6 +20,8 @@ github.com/klauspost/reedsolomon v1.9.1 h1:kYrT1MlR4JH6PqOpC+okdb9CDTcwEC/BqpzK4 github.com/klauspost/reedsolomon v1.9.1/go.mod h1:CwCi+NUr9pqSVktrkN+Ondf06rkhYZ/pcNv7fu+8Un4= github.com/mattn/go-runewidth v0.0.4 h1:2BvfKmzob6Bmd4YsL0zygOqfdFnK7GR4QL06Do4/p7Y= github.com/mattn/go-runewidth v0.0.4/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU= +github.com/pires/go-proxyproto v0.0.0-20190111085350-4d51b51e3bfc h1:lNOt1SMsgHXTdpuGw+RpnJtzUcCb/oRKZP65pBy9pr8= +github.com/pires/go-proxyproto v0.0.0-20190111085350-4d51b51e3bfc/go.mod h1:6/gX3+E/IYGa0wMORlSMla999awQFdbaeQCHjSMKIzY= github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/rakyll/statik v0.1.1/go.mod h1:OEi9wJV/fMUAGx1eNjq75DKDsJVuEv1U0oYdX6GX8Zs= diff --git a/models/config/proxy.go b/models/config/proxy.go index cdb06b8..a27416f 100644 --- a/models/config/proxy.go +++ b/models/config/proxy.go @@ -107,8 +107,10 @@ type BaseProxyConf struct { Group string `json:"group"` GroupKey string `json:"group_key"` + // only used for client + ProxyProtocolVersion string `json:"proxy_protocol_version"` LocalSvrConf - HealthCheckConf // only used for client + HealthCheckConf } func (cfg *BaseProxyConf) GetBaseInfo() *BaseProxyConf { @@ -121,7 +123,8 @@ func (cfg *BaseProxyConf) compare(cmp *BaseProxyConf) bool { cfg.UseEncryption != cmp.UseEncryption || cfg.UseCompression != cmp.UseCompression || cfg.Group != cmp.Group || - cfg.GroupKey != cmp.GroupKey { + cfg.GroupKey != cmp.GroupKey || + cfg.ProxyProtocolVersion != cmp.ProxyProtocolVersion { return false } if !cfg.LocalSvrConf.compare(&cmp.LocalSvrConf) { @@ -162,6 +165,7 @@ func (cfg *BaseProxyConf) UnmarshalFromIni(prefix string, name string, section i cfg.Group = section["group"] cfg.GroupKey = section["group_key"] + cfg.ProxyProtocolVersion = section["proxy_protocol_version"] if err := cfg.LocalSvrConf.UnmarshalFromIni(prefix, name, section); err != nil { return err @@ -194,6 +198,12 @@ func (cfg *BaseProxyConf) MarshalToMsg(pMsg *msg.NewProxy) { } func (cfg *BaseProxyConf) checkForCli() (err error) { + if cfg.ProxyProtocolVersion != "" { + if cfg.ProxyProtocolVersion != "v1" && cfg.ProxyProtocolVersion != "v2" { + return fmt.Errorf("no support proxy protocol version: %s", cfg.ProxyProtocolVersion) + } + } + if err = cfg.LocalSvrConf.checkForCli(); err != nil { return } diff --git a/models/msg/msg.go b/models/msg/msg.go index 2d5985c..11d2542 100644 --- a/models/msg/msg.go +++ b/models/msg/msg.go @@ -126,6 +126,10 @@ type ReqWorkConn struct { type StartWorkConn struct { ProxyName string `json:"proxy_name"` + SrcAddr string `json:"src_addr"` + DstAddr string `json:"dst_addr"` + SrcPort uint16 `json:"src_port"` + DstPort uint16 `json:"dst_port"` } type NewVisitorConn struct { diff --git a/models/plugin/https2http.go b/models/plugin/https2http.go new file mode 100644 index 0000000..746995f --- /dev/null +++ b/models/plugin/https2http.go @@ -0,0 +1,119 @@ +// Copyright 2019 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 ( + "crypto/tls" + "fmt" + "io" + "net/http" + "net/http/httputil" + + frpNet "github.com/fatedier/frp/utils/net" +) + +const PluginHTTPS2HTTP = "https2http" + +func init() { + Register(PluginHTTPS2HTTP, NewHTTPS2HTTPPlugin) +} + +type HTTPS2HTTPPlugin struct { + crtPath string + keyPath string + hostHeaderRewrite string + localAddr string + + l *Listener + s *http.Server +} + +func NewHTTPS2HTTPPlugin(params map[string]string) (Plugin, error) { + crtPath := params["plugin_crt_path"] + keyPath := params["plugin_key_path"] + localAddr := params["plugin_local_addr"] + hostHeaderRewrite := params["plugin_host_header_rewrite"] + + if crtPath == "" { + return nil, fmt.Errorf("plugin_crt_path is required") + } + if keyPath == "" { + return nil, fmt.Errorf("plugin_key_path is required") + } + if localAddr == "" { + return nil, fmt.Errorf("plugin_local_addr is required") + } + + listener := NewProxyListener() + + p := &HTTPS2HTTPPlugin{ + crtPath: crtPath, + keyPath: keyPath, + localAddr: localAddr, + hostHeaderRewrite: hostHeaderRewrite, + l: listener, + } + + rp := &httputil.ReverseProxy{ + Director: func(req *http.Request) { + req.URL.Scheme = "http" + req.URL.Host = p.localAddr + if p.hostHeaderRewrite != "" { + req.Host = p.hostHeaderRewrite + } + }, + } + + p.s = &http.Server{ + Handler: rp, + } + + tlsConfig, err := p.genTLSConfig() + if err != nil { + return nil, fmt.Errorf("gen TLS config error: %v", err) + } + ln := tls.NewListener(listener, tlsConfig) + + go p.s.Serve(ln) + return p, nil +} + +func (p *HTTPS2HTTPPlugin) genTLSConfig() (*tls.Config, error) { + cert, err := tls.LoadX509KeyPair(p.crtPath, p.keyPath) + if err != nil { + return nil, err + } + + config := &tls.Config{Certificates: []tls.Certificate{cert}} + return config, nil +} + +func (p *HTTPS2HTTPPlugin) Handle(conn io.ReadWriteCloser, realConn frpNet.Conn) { + wrapConn := frpNet.WrapReadWriteCloserToConn(conn, realConn) + p.l.PutConn(wrapConn) +} + +func (p *HTTPS2HTTPPlugin) handleRequest(w http.ResponseWriter, r *http.Request) { + w.Write([]byte("hello")) + return +} + +func (p *HTTPS2HTTPPlugin) Name() string { + return PluginHTTPS2HTTP +} + +func (p *HTTPS2HTTPPlugin) Close() error { + return nil +} diff --git a/server/control.go b/server/control.go index 169e173..8374ab2 100644 --- a/server/control.go +++ b/server/control.go @@ -301,6 +301,7 @@ func (ctl *Control) reader() { return } else { ctl.conn.Warn("read error: %v", err) + ctl.conn.Close() return } } else { diff --git a/server/proxy/http.go b/server/proxy/http.go index b76fe67..c5bc1ac 100644 --- a/server/proxy/http.go +++ b/server/proxy/http.go @@ -16,6 +16,7 @@ package proxy import ( "io" + "net" "strings" "github.com/fatedier/frp/g" @@ -51,6 +52,10 @@ func (pxy *HttpProxy) Run() (remoteAddr string, err error) { addrs := make([]string, 0) for _, domain := range pxy.cfg.CustomDomains { + if domain == "" { + continue + } + routeConfig.Domain = domain for _, location := range locations { routeConfig.Location = location @@ -93,8 +98,14 @@ func (pxy *HttpProxy) GetConf() config.ProxyConf { return pxy.cfg } -func (pxy *HttpProxy) GetRealConn() (workConn frpNet.Conn, err error) { - tmpConn, errRet := pxy.GetWorkConnFromPool() +func (pxy *HttpProxy) GetRealConn(remoteAddr string) (workConn frpNet.Conn, err error) { + rAddr, errRet := net.ResolveTCPAddr("tcp", remoteAddr) + if errRet != nil { + pxy.Warn("resolve TCP addr [%s] error: %v", remoteAddr, errRet) + // we do not return error here since remoteAddr is not necessary for proxies without proxy protocol enabled + } + + tmpConn, errRet := pxy.GetWorkConnFromPool(rAddr, nil) if errRet != nil { err = errRet return diff --git a/server/proxy/https.go b/server/proxy/https.go index a35af23..888fcbe 100644 --- a/server/proxy/https.go +++ b/server/proxy/https.go @@ -33,6 +33,10 @@ func (pxy *HttpsProxy) Run() (remoteAddr string, err error) { addrs := make([]string, 0) for _, domain := range pxy.cfg.CustomDomains { + if domain == "" { + continue + } + routeConfig.Domain = domain l, errRet := pxy.rc.VhostHttpsMuxer.Listen(routeConfig) if errRet != nil { diff --git a/server/proxy/proxy.go b/server/proxy/proxy.go index c07453a..dd7bb79 100644 --- a/server/proxy/proxy.go +++ b/server/proxy/proxy.go @@ -17,6 +17,8 @@ package proxy import ( "fmt" "io" + "net" + "strconv" "sync" "github.com/fatedier/frp/g" @@ -36,7 +38,7 @@ type Proxy interface { Run() (remoteAddr string, err error) GetName() string GetConf() config.ProxyConf - GetWorkConnFromPool() (workConn frpNet.Conn, err error) + GetWorkConnFromPool(src, dst net.Addr) (workConn frpNet.Conn, err error) GetUsedPortsNum() int Close() log.Logger @@ -70,7 +72,7 @@ func (pxy *BaseProxy) Close() { } } -func (pxy *BaseProxy) GetWorkConnFromPool() (workConn frpNet.Conn, err error) { +func (pxy *BaseProxy) GetWorkConnFromPool(src, dst net.Addr) (workConn frpNet.Conn, err error) { // try all connections from the pool for i := 0; i < pxy.poolCount+1; i++ { if workConn, err = pxy.getWorkConnFn(); err != nil { @@ -80,8 +82,29 @@ func (pxy *BaseProxy) GetWorkConnFromPool() (workConn frpNet.Conn, err error) { pxy.Info("get a new work connection: [%s]", workConn.RemoteAddr().String()) workConn.AddLogPrefix(pxy.GetName()) + var ( + srcAddr string + dstAddr string + srcPortStr string + dstPortStr string + srcPort int + dstPort int + ) + + if src != nil { + srcAddr, srcPortStr, _ = net.SplitHostPort(src.String()) + srcPort, _ = strconv.Atoi(srcPortStr) + } + if dst != nil { + dstAddr, dstPortStr, _ = net.SplitHostPort(dst.String()) + dstPort, _ = strconv.Atoi(dstPortStr) + } err := msg.WriteMsg(workConn, &msg.StartWorkConn{ ProxyName: pxy.GetName(), + SrcAddr: srcAddr, + SrcPort: uint16(srcPort), + DstAddr: dstAddr, + DstPort: uint16(dstPort), }) if err != nil { workConn.Warn("failed to send message to work connection from pool: %v, times: %d", err, i) @@ -177,7 +200,7 @@ func HandleUserTcpConnection(pxy Proxy, userConn frpNet.Conn, statsCollector sta defer userConn.Close() // try all connections from the pool - workConn, err := pxy.GetWorkConnFromPool() + workConn, err := pxy.GetWorkConnFromPool(userConn.RemoteAddr(), userConn.LocalAddr()) if err != nil { return } diff --git a/server/proxy/udp.go b/server/proxy/udp.go index 104bff8..b5dd5fb 100644 --- a/server/proxy/udp.go +++ b/server/proxy/udp.go @@ -160,7 +160,7 @@ func (pxy *UdpProxy) Run() (remoteAddr string, err error) { // Sleep a while for waiting control send the NewProxyResp to client. time.Sleep(500 * time.Millisecond) for { - workConn, err := pxy.GetWorkConnFromPool() + workConn, err := pxy.GetWorkConnFromPool(nil, nil) if err != nil { time.Sleep(1 * time.Second) // check if proxy is closed diff --git a/server/proxy/xtcp.go b/server/proxy/xtcp.go index 8726666..925485c 100644 --- a/server/proxy/xtcp.go +++ b/server/proxy/xtcp.go @@ -44,7 +44,7 @@ func (pxy *XtcpProxy) Run() (remoteAddr string, err error) { break case sidRequest := <-sidCh: sr := sidRequest - workConn, errRet := pxy.GetWorkConnFromPool() + workConn, errRet := pxy.GetWorkConnFromPool(nil, nil) if errRet != nil { continue } diff --git a/utils/version/version.go b/utils/version/version.go index ae0707d..d2bd46c 100644 --- a/utils/version/version.go +++ b/utils/version/version.go @@ -19,7 +19,7 @@ import ( "strings" ) -var version string = "0.25.3" +var version string = "0.26.0" func Full() string { return version diff --git a/utils/vhost/newhttp.go b/utils/vhost/http.go similarity index 95% rename from utils/vhost/newhttp.go rename to utils/vhost/http.go index 59a4a0e..7bbc361 100644 --- a/utils/vhost/newhttp.go +++ b/utils/vhost/http.go @@ -89,13 +89,15 @@ func NewHttpReverseProxy(option HttpReverseProxyOptions) *HttpReverseProxy { DialContext: func(ctx context.Context, network, addr string) (net.Conn, error) { url := ctx.Value("url").(string) host := getHostFromAddr(ctx.Value("host").(string)) - return rp.CreateConnection(host, url) + remote := ctx.Value("remote").(string) + return rp.CreateConnection(host, url, remote) }, }, WebSocketDialContext: func(ctx context.Context, network, addr string) (net.Conn, error) { url := ctx.Value("url").(string) host := getHostFromAddr(ctx.Value("host").(string)) - return rp.CreateConnection(host, url) + remote := ctx.Value("remote").(string) + return rp.CreateConnection(host, url, remote) }, BufferPool: newWrapPool(), ErrorLog: log.New(newWrapLogger(), "", 0), @@ -138,12 +140,12 @@ func (rp *HttpReverseProxy) GetHeaders(domain string, location string) (headers return } -func (rp *HttpReverseProxy) CreateConnection(domain string, location string) (net.Conn, error) { +func (rp *HttpReverseProxy) CreateConnection(domain string, location string, remoteAddr string) (net.Conn, error) { vr, ok := rp.getVhost(domain, location) if ok { fn := vr.payload.(*VhostRouteConfig).CreateConnFn if fn != nil { - return fn() + return fn(remoteAddr) } } return nil, fmt.Errorf("%v: %s %s", ErrNoDomain, domain, location) diff --git a/utils/vhost/reverseproxy.go b/utils/vhost/reverseproxy.go index 6b0c292..5662589 100644 --- a/utils/vhost/reverseproxy.go +++ b/utils/vhost/reverseproxy.go @@ -158,6 +158,7 @@ func (p *ReverseProxy) serveWebSocket(rw http.ResponseWriter, req *http.Request) req = req.WithContext(context.WithValue(req.Context(), "url", req.URL.Path)) req = req.WithContext(context.WithValue(req.Context(), "host", req.Host)) + req = req.WithContext(context.WithValue(req.Context(), "remote", req.RemoteAddr)) targetConn, err := p.WebSocketDialContext(req.Context(), "tcp", "") if err != nil { @@ -215,6 +216,7 @@ func (p *ReverseProxy) serveHTTP(rw http.ResponseWriter, req *http.Request) { // Modify for frp outreq = outreq.WithContext(context.WithValue(outreq.Context(), "url", req.URL.Path)) outreq = outreq.WithContext(context.WithValue(outreq.Context(), "host", req.Host)) + outreq = outreq.WithContext(context.WithValue(outreq.Context(), "remote", req.RemoteAddr)) p.Director(outreq) outreq.Close = false diff --git a/utils/vhost/vhost.go b/utils/vhost/vhost.go index 2e38652..f366e1e 100644 --- a/utils/vhost/vhost.go +++ b/utils/vhost/vhost.go @@ -51,7 +51,7 @@ func NewVhostMuxer(listener frpNet.Listener, vhostFunc muxFunc, authFunc httpAut return mux, nil } -type CreateConnFunc func() (frpNet.Conn, error) +type CreateConnFunc func(remoteAddr string) (frpNet.Conn, error) type VhostRouteConfig struct { Domain string diff --git a/vendor/github.com/pires/go-proxyproto/.gitignore b/vendor/github.com/pires/go-proxyproto/.gitignore new file mode 100644 index 0000000..d19ada0 --- /dev/null +++ b/vendor/github.com/pires/go-proxyproto/.gitignore @@ -0,0 +1,9 @@ +# Compiled Object files, Static and Dynamic libs (Shared Objects) +*.o +*.a +*.so + +# Folders +.idea +bin +pkg diff --git a/vendor/github.com/pires/go-proxyproto/.travis.yml b/vendor/github.com/pires/go-proxyproto/.travis.yml new file mode 100644 index 0000000..76e99a6 --- /dev/null +++ b/vendor/github.com/pires/go-proxyproto/.travis.yml @@ -0,0 +1,15 @@ +language: go +sudo: false +go: + - 1.11 +install: + - go get golang.org/x/tools/cmd/cover + - go get github.com/mattn/goveralls +script: + - go fmt + - go vet + - go test -v -covermode=count -coverprofile=coverage.out + - $HOME/gopath/bin/goveralls -coverprofile=coverage.out -service=travis-ci -repotoken $COVERALLS_TOKEN +env: + global: + secure: NRDefpPiVrhkRetDbJ1bek+7+Ojwh9dUSAAP4KBw5cqQbEDMUo/fgTHnTBywe4p7zaJ2IX7B2gmQk8zsAHu8D74A8baYzZBOJzpgAGx6GzTSMtLKTX62TKrKGvslru0/e9V/OaOGRuy1ETteuOb/b23rtBqQ7+N0JfC5+9wjH1mmYd6rbeU8bGMzyvXoCYorgf7VNV1KQM+4355pSDR5cvbV1lHfSut6Pw1dcjOahxheXi1YVhohdkQOwvBlSifVritJgwWcUtrb4xW97pZ8TWnHi5TlqSnxtRSKiPq5aojgsAt9ETnouPBhs0cToyteN3xi5N0SWvn5RRs7mPFFwkpvspghWNtqU4/uPRR0NrbcEiYcEFghicoq7pTthP0iP/KsBb7F1mH2YC79uuNMnOgoByKxLjD/TOybhSvyTRt2TldHZwePxcukXwwL7LHALhojsN299KQgIIiMdn9+oXESIzJXwI10ZDEfLPfhX+LHBQylobNqnnFM/tzFyFDGAqDVEn+yc4GVEu+FjpJ/kqDpTDpnUZ7Ui6KJX/VJfGMgwOrMDegOYlm5Cg6xPug0zb08taciTcWByDWOzZfmHcxOt3JyJXFTh49oFK70Xn+C7YNQt7VxfdsjCJ84HEDxxeY/Rp3HowTzjvqjsVVncKtJm0o7epOnY58RKP/GFwg= diff --git a/vendor/github.com/pires/go-proxyproto/LICENSE b/vendor/github.com/pires/go-proxyproto/LICENSE new file mode 100644 index 0000000..8dada3e --- /dev/null +++ b/vendor/github.com/pires/go-proxyproto/LICENSE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "{}" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright {yyyy} {name of copyright owner} + + 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. diff --git a/vendor/github.com/pires/go-proxyproto/README.md b/vendor/github.com/pires/go-proxyproto/README.md new file mode 100644 index 0000000..6a359d0 --- /dev/null +++ b/vendor/github.com/pires/go-proxyproto/README.md @@ -0,0 +1,32 @@ +# go-proxyproto + +[![Build Status](https://travis-ci.org/pires/go-proxyproto.svg?branch=master)](https://travis-ci.org/pires/go-proxyproto) +[![Coverage Status](https://coveralls.io/repos/github/pires/go-proxyproto/badge.svg?branch=master)](https://coveralls.io/github/pires/go-proxyproto?branch=master) +[![Go Report Card](https://goreportcard.com/badge/github.com/pires/go-proxyproto)](https://goreportcard.com/report/github.com/pires/go-proxyproto) + +A Go library implementation of the [PROXY protocol, versions 1 and 2](http://www.haproxy.org/download/1.5/doc/proxy-protocol.txt), +which provides, as per specification: +> (...) a convenient way to safely transport connection +> information such as a client's address across multiple layers of NAT or TCP +> proxies. It is designed to require little changes to existing components and +> to limit the performance impact caused by the processing of the transported +> information. + +This library is to be used in one of or both proxy clients and proxy servers that need to support said protocol. +Both protocol versions, 1 (text-based) and 2 (binary-based) are supported. + +## Installation + +```shell +$ go get -u github.com/pires/go-proxyproto +``` + +## Usage + +### Client (TODO) + +### Server (TODO) + +## Documentation + +[http://godoc.org/github.com/pires/go-proxyproto](http://godoc.org/github.com/pires/go-proxyproto) diff --git a/vendor/github.com/pires/go-proxyproto/addr_proto.go b/vendor/github.com/pires/go-proxyproto/addr_proto.go new file mode 100644 index 0000000..56b9155 --- /dev/null +++ b/vendor/github.com/pires/go-proxyproto/addr_proto.go @@ -0,0 +1,71 @@ +package proxyproto + +// AddressFamilyAndProtocol represents address family and transport protocol. +type AddressFamilyAndProtocol byte + +const ( + UNSPEC = '\x00' + TCPv4 = '\x11' + UDPv4 = '\x12' + TCPv6 = '\x21' + UDPv6 = '\x22' + UnixStream = '\x31' + UnixDatagram = '\x32' +) + +var supportedTransportProtocol = map[AddressFamilyAndProtocol]bool{ + TCPv4: true, + UDPv4: true, + TCPv6: true, + UDPv6: true, + UnixStream: true, + UnixDatagram: true, +} + +// IsIPv4 returns true if the address family is IPv4 (AF_INET4), false otherwise. +func (ap AddressFamilyAndProtocol) IsIPv4() bool { + return 0x10 == ap&0xF0 +} + +// IsIPv6 returns true if the address family is IPv6 (AF_INET6), false otherwise. +func (ap AddressFamilyAndProtocol) IsIPv6() bool { + return 0x20 == ap&0xF0 +} + +// IsUnix returns true if the address family is UNIX (AF_UNIX), false otherwise. +func (ap AddressFamilyAndProtocol) IsUnix() bool { + return 0x30 == ap&0xF0 +} + +// IsStream returns true if the transport protocol is TCP or STREAM (SOCK_STREAM), false otherwise. +func (ap AddressFamilyAndProtocol) IsStream() bool { + return 0x01 == ap&0x0F +} + +// IsDatagram returns true if the transport protocol is UDP or DGRAM (SOCK_DGRAM), false otherwise. +func (ap AddressFamilyAndProtocol) IsDatagram() bool { + return 0x02 == ap&0x0F +} + +// IsUnspec returns true if the transport protocol or address family is unspecified, false otherwise. +func (ap AddressFamilyAndProtocol) IsUnspec() bool { + return (0x00 == ap&0xF0) || (0x00 == ap&0x0F) +} + +func (ap AddressFamilyAndProtocol) toByte() byte { + if ap.IsIPv4() && ap.IsStream() { + return TCPv4 + } else if ap.IsIPv4() && ap.IsDatagram() { + return UDPv4 + } else if ap.IsIPv6() && ap.IsStream() { + return TCPv6 + } else if ap.IsIPv6() && ap.IsDatagram() { + return UDPv6 + } else if ap.IsUnix() && ap.IsStream() { + return UnixStream + } else if ap.IsUnix() && ap.IsDatagram() { + return UnixDatagram + } + + return UNSPEC +} diff --git a/vendor/github.com/pires/go-proxyproto/header.go b/vendor/github.com/pires/go-proxyproto/header.go new file mode 100644 index 0000000..23981b7 --- /dev/null +++ b/vendor/github.com/pires/go-proxyproto/header.go @@ -0,0 +1,116 @@ +// Package proxyproto implements Proxy Protocol (v1 and v2) parser and writer, as per specification: +// http://www.haproxy.org/download/1.5/doc/proxy-protocol.txt +package proxyproto + +import ( + "bufio" + "bytes" + "errors" + "io" + "net" + "time" +) + +var ( + // Protocol + SIGV1 = []byte{'\x50', '\x52', '\x4F', '\x58', '\x59'} + SIGV2 = []byte{'\x0D', '\x0A', '\x0D', '\x0A', '\x00', '\x0D', '\x0A', '\x51', '\x55', '\x49', '\x54', '\x0A'} + + ErrCantReadProtocolVersionAndCommand = errors.New("Can't read proxy protocol version and command") + ErrCantReadAddressFamilyAndProtocol = errors.New("Can't read address family or protocol") + ErrCantReadLength = errors.New("Can't read length") + ErrCantResolveSourceUnixAddress = errors.New("Can't resolve source Unix address") + ErrCantResolveDestinationUnixAddress = errors.New("Can't resolve destination Unix address") + ErrNoProxyProtocol = errors.New("Proxy protocol signature not present") + ErrUnknownProxyProtocolVersion = errors.New("Unknown proxy protocol version") + ErrUnsupportedProtocolVersionAndCommand = errors.New("Unsupported proxy protocol version and command") + ErrUnsupportedAddressFamilyAndProtocol = errors.New("Unsupported address family and protocol") + ErrInvalidLength = errors.New("Invalid length") + ErrInvalidAddress = errors.New("Invalid address") + ErrInvalidPortNumber = errors.New("Invalid port number") +) + +// Header is the placeholder for proxy protocol header. +type Header struct { + Version byte + Command ProtocolVersionAndCommand + TransportProtocol AddressFamilyAndProtocol + SourceAddress net.IP + DestinationAddress net.IP + SourcePort uint16 + DestinationPort uint16 +} + +// EqualTo returns true if headers are equivalent, false otherwise. +func (header *Header) EqualTo(q *Header) bool { + if header == nil || q == nil { + return false + } + if header.Command.IsLocal() { + return true + } + return header.TransportProtocol == q.TransportProtocol && + header.SourceAddress.String() == q.SourceAddress.String() && + header.DestinationAddress.String() == q.DestinationAddress.String() && + header.SourcePort == q.SourcePort && + header.DestinationPort == q.DestinationPort +} + +// WriteTo renders a proxy protocol header in a format to write over the wire. +func (header *Header) WriteTo(w io.Writer) (int64, error) { + switch header.Version { + case 1: + return header.writeVersion1(w) + case 2: + return header.writeVersion2(w) + default: + return 0, ErrUnknownProxyProtocolVersion + } +} + +// Read identifies the proxy protocol version and reads the remaining of +// the header, accordingly. +// +// If proxy protocol header signature is not present, the reader buffer remains untouched +// and is safe for reading outside of this code. +// +// If proxy protocol header signature is present but an error is raised while processing +// the remaining header, assume the reader buffer to be in a corrupt state. +// Also, this operation will block until enough bytes are available for peeking. +func Read(reader *bufio.Reader) (*Header, error) { + // In order to improve speed for small non-PROXYed packets, take a peek at the first byte alone. + if b1, err := reader.Peek(1); err == nil && (bytes.Equal(b1[:1], SIGV1[:1]) || bytes.Equal(b1[:1], SIGV2[:1])) { + if signature, err := reader.Peek(5); err == nil && bytes.Equal(signature[:5], SIGV1) { + return parseVersion1(reader) + } else if signature, err := reader.Peek(12); err == nil && bytes.Equal(signature[:12], SIGV2) { + return parseVersion2(reader) + } + } + + return nil, ErrNoProxyProtocol +} + +// ReadTimeout acts as Read but takes a timeout. If that timeout is reached, it's assumed +// there's no proxy protocol header. +func ReadTimeout(reader *bufio.Reader, timeout time.Duration) (*Header, error) { + type header struct { + h *Header + e error + } + read := make(chan *header, 1) + + go func() { + h := &header{} + h.h, h.e = Read(reader) + read <- h + }() + + timer := time.NewTimer(timeout) + select { + case result := <-read: + timer.Stop() + return result.h, result.e + case <-timer.C: + return nil, ErrNoProxyProtocol + } +} diff --git a/vendor/github.com/pires/go-proxyproto/v1.go b/vendor/github.com/pires/go-proxyproto/v1.go new file mode 100644 index 0000000..3447dab --- /dev/null +++ b/vendor/github.com/pires/go-proxyproto/v1.go @@ -0,0 +1,116 @@ +package proxyproto + +import ( + "bufio" + "bytes" + "io" + "net" + "strconv" + "strings" +) + +const ( + CRLF = "\r\n" + SEPARATOR = " " +) + +func initVersion1() *Header { + header := new(Header) + header.Version = 1 + // Command doesn't exist in v1 + header.Command = PROXY + return header +} + +func parseVersion1(reader *bufio.Reader) (*Header, error) { + // Make sure we have a v1 header + line, err := reader.ReadString('\n') + if !strings.HasSuffix(line, CRLF) { + return nil, ErrCantReadProtocolVersionAndCommand + } + tokens := strings.Split(line[:len(line)-2], SEPARATOR) + if len(tokens) < 6 { + return nil, ErrCantReadProtocolVersionAndCommand + } + + header := initVersion1() + + // Read address family and protocol + switch tokens[1] { + case "TCP4": + header.TransportProtocol = TCPv4 + case "TCP6": + header.TransportProtocol = TCPv6 + default: + header.TransportProtocol = UNSPEC + } + + // Read addresses and ports + header.SourceAddress, err = parseV1IPAddress(header.TransportProtocol, tokens[2]) + if err != nil { + return nil, err + } + header.DestinationAddress, err = parseV1IPAddress(header.TransportProtocol, tokens[3]) + if err != nil { + return nil, err + } + header.SourcePort, err = parseV1PortNumber(tokens[4]) + if err != nil { + return nil, err + } + header.DestinationPort, err = parseV1PortNumber(tokens[5]) + if err != nil { + return nil, err + } + return header, nil +} + +func (header *Header) writeVersion1(w io.Writer) (int64, error) { + // As of version 1, only "TCP4" ( \x54 \x43 \x50 \x34 ) for TCP over IPv4, + // and "TCP6" ( \x54 \x43 \x50 \x36 ) for TCP over IPv6 are allowed. + proto := "UNKNOWN" + if header.TransportProtocol == TCPv4 { + proto = "TCP4" + } else if header.TransportProtocol == TCPv6 { + proto = "TCP6" + } + + var buf bytes.Buffer + buf.Write(SIGV1) + buf.WriteString(SEPARATOR) + buf.WriteString(proto) + buf.WriteString(SEPARATOR) + buf.WriteString(header.SourceAddress.String()) + buf.WriteString(SEPARATOR) + buf.WriteString(header.DestinationAddress.String()) + buf.WriteString(SEPARATOR) + buf.WriteString(strconv.Itoa(int(header.SourcePort))) + buf.WriteString(SEPARATOR) + buf.WriteString(strconv.Itoa(int(header.DestinationPort))) + buf.WriteString(CRLF) + + return buf.WriteTo(w) +} + +func parseV1PortNumber(portStr string) (uint16, error) { + var port uint16 + + _port, err := strconv.Atoi(portStr) + if err == nil { + if port < 0 || port > 65535 { + err = ErrInvalidPortNumber + } + port = uint16(_port) + } + + return port, err +} + +func parseV1IPAddress(protocol AddressFamilyAndProtocol, addrStr string) (addr net.IP, err error) { + addr = net.ParseIP(addrStr) + tryV4 := addr.To4() + if (protocol == TCPv4 && tryV4 == nil) || (protocol == TCPv6 && tryV4 != nil) { + err = ErrInvalidAddress + } + return +} diff --git a/vendor/github.com/pires/go-proxyproto/v2.go b/vendor/github.com/pires/go-proxyproto/v2.go new file mode 100644 index 0000000..208a9b4 --- /dev/null +++ b/vendor/github.com/pires/go-proxyproto/v2.go @@ -0,0 +1,202 @@ +package proxyproto + +import ( + "bufio" + "bytes" + "encoding/binary" + "io" +) + +var ( + lengthV4 = uint16(12) + lengthV6 = uint16(36) + lengthUnix = uint16(218) + + lengthV4Bytes = func() []byte { + a := make([]byte, 2) + binary.BigEndian.PutUint16(a, lengthV4) + return a + }() + lengthV6Bytes = func() []byte { + a := make([]byte, 2) + binary.BigEndian.PutUint16(a, lengthV6) + return a + }() + lengthUnixBytes = func() []byte { + a := make([]byte, 2) + binary.BigEndian.PutUint16(a, lengthUnix) + return a + }() +) + +type _ports struct { + SrcPort uint16 + DstPort uint16 +} + +type _addr4 struct { + Src [4]byte + Dst [4]byte + SrcPort uint16 + DstPort uint16 +} + +type _addr6 struct { + Src [16]byte + Dst [16]byte + _ports +} + +type _addrUnix struct { + Src [108]byte + Dst [108]byte +} + +func parseVersion2(reader *bufio.Reader) (header *Header, err error) { + // Skip first 12 bytes (signature) + for i := 0; i < 12; i++ { + if _, err = reader.ReadByte(); err != nil { + return nil, ErrCantReadProtocolVersionAndCommand + } + } + + header = new(Header) + header.Version = 2 + + // Read the 13th byte, protocol version and command + b13, err := reader.ReadByte() + if err != nil { + return nil, ErrCantReadProtocolVersionAndCommand + } + header.Command = ProtocolVersionAndCommand(b13) + if _, ok := supportedCommand[header.Command]; !ok { + return nil, ErrUnsupportedProtocolVersionAndCommand + } + // If command is LOCAL, header ends here + if header.Command.IsLocal() { + return header, nil + } + + // Read the 14th byte, address family and protocol + b14, err := reader.ReadByte() + if err != nil { + return nil, ErrCantReadAddressFamilyAndProtocol + } + header.TransportProtocol = AddressFamilyAndProtocol(b14) + if _, ok := supportedTransportProtocol[header.TransportProtocol]; !ok { + return nil, ErrUnsupportedAddressFamilyAndProtocol + } + + // Make sure there are bytes available as specified in length + var length uint16 + if err := binary.Read(io.LimitReader(reader, 2), binary.BigEndian, &length); err != nil { + return nil, ErrCantReadLength + } + if !header.validateLength(length) { + return nil, ErrInvalidLength + } + + if _, err := reader.Peek(int(length)); err != nil { + return nil, ErrInvalidLength + } + + // Length-limited reader for payload section + payloadReader := io.LimitReader(reader, int64(length)) + + // Read addresses and ports + if header.TransportProtocol.IsIPv4() { + var addr _addr4 + if err := binary.Read(payloadReader, binary.BigEndian, &addr); err != nil { + return nil, ErrInvalidAddress + } + header.SourceAddress = addr.Src[:] + header.DestinationAddress = addr.Dst[:] + header.SourcePort = addr.SrcPort + header.DestinationPort = addr.DstPort + } else if header.TransportProtocol.IsIPv6() { + var addr _addr6 + if err := binary.Read(payloadReader, binary.BigEndian, &addr); err != nil { + return nil, ErrInvalidAddress + } + header.SourceAddress = addr.Src[:] + header.DestinationAddress = addr.Dst[:] + header.SourcePort = addr.SrcPort + header.DestinationPort = addr.DstPort + } + // TODO fully support Unix addresses + // else if header.TransportProtocol.IsUnix() { + // var addr _addrUnix + // if err := binary.Read(payloadReader, binary.BigEndian, &addr); err != nil { + // return nil, ErrInvalidAddress + // } + // + //if header.SourceAddress, err = net.ResolveUnixAddr("unix", string(addr.Src[:])); err != nil { + // return nil, ErrCantResolveSourceUnixAddress + //} + //if header.DestinationAddress, err = net.ResolveUnixAddr("unix", string(addr.Dst[:])); err != nil { + // return nil, ErrCantResolveDestinationUnixAddress + //} + //} + + // TODO add encapsulated TLV support + + // Drain the remaining padding + payloadReader.Read(make([]byte, length)) + + return header, nil +} + +func (header *Header) writeVersion2(w io.Writer) (int64, error) { + var buf bytes.Buffer + buf.Write(SIGV2) + buf.WriteByte(header.Command.toByte()) + if !header.Command.IsLocal() { + buf.WriteByte(header.TransportProtocol.toByte()) + // TODO add encapsulated TLV length + var addrSrc, addrDst []byte + if header.TransportProtocol.IsIPv4() { + buf.Write(lengthV4Bytes) + addrSrc = header.SourceAddress.To4() + addrDst = header.DestinationAddress.To4() + } else if header.TransportProtocol.IsIPv6() { + buf.Write(lengthV6Bytes) + addrSrc = header.SourceAddress.To16() + addrDst = header.DestinationAddress.To16() + } else if header.TransportProtocol.IsUnix() { + buf.Write(lengthUnixBytes) + // TODO is below right? + addrSrc = []byte(header.SourceAddress.String()) + addrDst = []byte(header.DestinationAddress.String()) + } + buf.Write(addrSrc) + buf.Write(addrDst) + + portSrcBytes := func() []byte { + a := make([]byte, 2) + binary.BigEndian.PutUint16(a, header.SourcePort) + return a + }() + buf.Write(portSrcBytes) + + portDstBytes := func() []byte { + a := make([]byte, 2) + binary.BigEndian.PutUint16(a, header.DestinationPort) + return a + }() + buf.Write(portDstBytes) + + } + + return buf.WriteTo(w) +} + +func (header *Header) validateLength(length uint16) bool { + if header.TransportProtocol.IsIPv4() { + return length >= lengthV4 + } else if header.TransportProtocol.IsIPv6() { + return length >= lengthV6 + } else if header.TransportProtocol.IsUnix() { + return length >= lengthUnix + } + return false +} diff --git a/vendor/github.com/pires/go-proxyproto/version_cmd.go b/vendor/github.com/pires/go-proxyproto/version_cmd.go new file mode 100644 index 0000000..2ee1a05 --- /dev/null +++ b/vendor/github.com/pires/go-proxyproto/version_cmd.go @@ -0,0 +1,39 @@ +package proxyproto + +// ProtocolVersionAndCommand represents proxy protocol version and command. +type ProtocolVersionAndCommand byte + +const ( + LOCAL = '\x20' + PROXY = '\x21' +) + +var supportedCommand = map[ProtocolVersionAndCommand]bool{ + LOCAL: true, + PROXY: true, +} + +// IsLocal returns true if the protocol version is \x2 and command is LOCAL, false otherwise. +func (pvc ProtocolVersionAndCommand) IsLocal() bool { + return 0x20 == pvc&0xF0 && 0x00 == pvc&0x0F +} + +// IsProxy returns true if the protocol version is \x2 and command is PROXY, false otherwise. +func (pvc ProtocolVersionAndCommand) IsProxy() bool { + return 0x20 == pvc&0xF0 && 0x01 == pvc&0x0F +} + +// IsUnspec returns true if the protocol version or command is unspecified, false otherwise. +func (pvc ProtocolVersionAndCommand) IsUnspec() bool { + return !(pvc.IsLocal() || pvc.IsProxy()) +} + +func (pvc ProtocolVersionAndCommand) toByte() byte { + if pvc.IsLocal() { + return LOCAL + } else if pvc.IsProxy() { + return PROXY + } + + return LOCAL +} diff --git a/vendor/modules.txt b/vendor/modules.txt index a6efdf4..2e56391 100644 --- a/vendor/modules.txt +++ b/vendor/modules.txt @@ -31,6 +31,8 @@ github.com/inconshreveable/mousetrap github.com/klauspost/cpuid # github.com/klauspost/reedsolomon v1.9.1 github.com/klauspost/reedsolomon +# github.com/pires/go-proxyproto v0.0.0-20190111085350-4d51b51e3bfc +github.com/pires/go-proxyproto # github.com/pkg/errors v0.8.0 github.com/pkg/errors # github.com/pmezard/go-difflib v1.0.0