diff --git a/.travis.yml b/.travis.yml index f66096e6..44a04832 100644 --- a/.travis.yml +++ b/.travis.yml @@ -2,8 +2,8 @@ sudo: false language: go go: - - 1.10.x - 1.11.x + - 1.12.x install: - make diff --git a/README.md b/README.md index d119713c..29d63cb4 100644 --- a/README.md +++ b/README.md @@ -28,8 +28,10 @@ Now it also try to support p2p connect. * [Configuration File](#configuration-file) * [Configuration file template](#configuration-file-template) * [Dashboard](#dashboard) + * [Admin UI](#admin-ui) * [Authentication](#authentication) * [Encryption and Compression](#encryption-and-compression) + * [TLS](#tls) * [Hot-Reload frpc configuration](#hot-reload-frpc-configuration) * [Get proxy status from client](#get-proxy-status-from-client) * [Port White List](#port-white-list) @@ -389,6 +391,22 @@ Then visit `http://[server_addr]:7500` to see dashboard, default username and pa ![dashboard](/doc/pic/dashboard.png) +### Admin UI + +Admin UI help you check and manage frpc's configure. + +Configure a address for admin UI to enable this feature: + +```ini +[common] +admin_addr = 127.0.0.1 +admin_port = 7400 +admin_user = admin +admin_pwd = admin +``` + +Then visit `http://127.0.0.1:7400` to see admin UI, default username and password are both `admin`. + ### Authentication `token` in frps.ini and frpc.ini should be same. @@ -407,6 +425,14 @@ use_encryption = true use_compression = true ``` +#### TLS + +frp support TLS protocol between frpc and frps since v0.25.0. + +Config `tls_enable = true` in `common` section to frpc.ini to enable this feature. + +For port multiplexing, frp send a first byte 0x17 to dial a TLS connection. + ### Hot-Reload frpc configuration First you need to set admin port in frpc's configure file to let it provide HTTP API for more features. @@ -592,7 +618,7 @@ custom_domains = test.yourdomain.com host_header_rewrite = dev.yourdomain.com ``` -If `host_header_rewrite` is specified, the host header will be rewritten to match the hostname portion of the forwarding address. +The `Host` request header will be rewritten to `Host: dev.yourdomain.com` before it reach your local http server. ### Set Headers In HTTP Request @@ -736,8 +762,6 @@ plugin_http_passwd = abc ## Development Plan * Log http request information in frps. -* Direct reverse proxy, like haproxy. -* kubernetes ingress support. ## Contributing diff --git a/README_zh.md b/README_zh.md index cd705b78..952aeb69 100644 --- a/README_zh.md +++ b/README_zh.md @@ -24,8 +24,10 @@ frp 是一个可用于内网穿透的高性能的反向代理应用,支持 tcp * [配置文件](#配置文件) * [配置文件模版渲染](#配置文件模版渲染) * [Dashboard](#dashboard) + * [Admin UI](#admin-ui) * [身份验证](#身份验证) * [加密与压缩](#加密与压缩) + * [TLS](#tls) * [客户端热加载配置文件](#客户端热加载配置文件) * [客户端查看代理状态](#客户端查看代理状态) * [端口白名单](#端口白名单) @@ -47,6 +49,7 @@ frp 是一个可用于内网穿透的高性能的反向代理应用,支持 tcp * [开发计划](#开发计划) * [为 frp 做贡献](#为-frp-做贡献) * [捐助](#捐助) + * [知识星球](#知识星球) * [支付宝扫码捐赠](#支付宝扫码捐赠) * [微信支付捐赠](#微信支付捐赠) * [Paypal 捐赠](#paypal-捐赠) @@ -404,6 +407,24 @@ dashboard_pwd = admin ![dashboard](/doc/pic/dashboard.png) +### Admin UI + +Admin UI 可以帮助用户通过浏览器来查询和管理客户端的 proxy 状态和配置。 + +需要在 frpc.ini 中指定 admin 服务使用的端口,即可开启此功能: + +```ini +[common] +admin_addr = 127.0.0.1 +admin_port = 7400 +admin_user = admin +admin_pwd = admin +``` + +打开浏览器通过 `http://127.0.0.1:7400` 访问 Admin UI,用户名密码默认为 `admin`。 + +如果想要在外网环境访问 Admin UI,将 7400 端口映射出去即可,但需要重视安全风险。 + ### 身份验证 服务端和客户端的 common 配置中的 `token` 参数一致则身份验证通过。 @@ -426,6 +447,14 @@ use_compression = true 如果传输的报文长度较长,通过设置 `use_compression = true` 对传输内容进行压缩,可以有效减小 frpc 与 frps 之间的网络流量,加快流量转发速度,但是会额外消耗一些 cpu 资源。 +#### TLS + +从 v0.25.0 版本开始 frpc 和 frps 之间支持通过 TLS 协议加密传输。通过在 `frpc.ini` 的 `common` 中配置 `tls_enable = true` 来启用此功能,安全性更高。 + +为了端口复用,frp 建立 TLS 连接的第一个字节为 0x17。 + +**注意: 启用此功能后除 xtcp 外,不需要再设置 use_encryption。** + ### 客户端热加载配置文件 当修改了 frpc 中的代理配置,可以通过 `frpc reload` 命令来动态加载配置文件,通常会在 10 秒内完成代理的更新。 diff --git a/client/control.go b/client/control.go index ba687f94..4b458a7b 100644 --- a/client/control.go +++ b/client/control.go @@ -15,6 +15,8 @@ package client import ( + "crypto/tls" + "fmt" "io" "runtime/debug" "sync" @@ -174,8 +176,14 @@ func (ctl *Control) connectServer() (conn frpNet.Conn, err error) { } conn = frpNet.WrapConn(stream) } else { + var tlsConfig *tls.Config + if g.GlbClientCfg.TLSEnable { + tlsConfig = &tls.Config{ + InsecureSkipVerify: true, + } + } conn, err = frpNet.ConnectServerByProxy(g.GlbClientCfg.HttpProxy, g.GlbClientCfg.Protocol, - newAddress(g.GlbClientCfg.ServerAddr, g.GlbClientCfg.ServerPort)) + newAddress(g.GlbClientCfg.ServerAddr, g.GlbClientCfg.ServerPort), tlsConfig) if err != nil { ctl.Warn("start new connection to server error: %v", err) return diff --git a/client/proxy/proxy.go b/client/proxy/proxy.go index b38f75e3..e342b73a 100644 --- a/client/proxy/proxy.go +++ b/client/proxy/proxy.go @@ -18,7 +18,9 @@ import ( "bytes" "fmt" "io" + "io/ioutil" "net" + "strconv" "strings" "sync" "time" @@ -34,6 +36,7 @@ import ( "github.com/fatedier/golib/errors" frpIo "github.com/fatedier/golib/io" "github.com/fatedier/golib/pool" + fmux "github.com/hashicorp/yamux" ) // Proxy defines how to handle work connections for different proxy type. @@ -287,32 +290,97 @@ func (pxy *XtcpProxy) InWorkConn(conn frpNet.Conn) { return } - pxy.Trace("get natHoleRespMsg, sid [%s], client address [%s]", natHoleRespMsg.Sid, natHoleRespMsg.ClientAddr) + pxy.Trace("get natHoleRespMsg, sid [%s], client address [%s] visitor address [%s]", natHoleRespMsg.Sid, natHoleRespMsg.ClientAddr, natHoleRespMsg.VisitorAddr) - // Send sid to visitor udp address. - time.Sleep(time.Second) + // Send detect message + array := strings.Split(natHoleRespMsg.VisitorAddr, ":") + if len(array) <= 1 { + pxy.Error("get NatHoleResp visitor address error: %v", natHoleRespMsg.VisitorAddr) + } laddr, _ := net.ResolveUDPAddr("udp", clientConn.LocalAddr().String()) - daddr, err := net.ResolveUDPAddr("udp", natHoleRespMsg.VisitorAddr) + /* + for i := 1000; i < 65000; i++ { + pxy.sendDetectMsg(array[0], int64(i), laddr, "a") + } + */ + port, err := strconv.ParseInt(array[1], 10, 64) if err != nil { - pxy.Error("resolve visitor udp address error: %v", err) + pxy.Error("get natHoleResp visitor address error: %v", natHoleRespMsg.VisitorAddr) return } + pxy.sendDetectMsg(array[0], int(port), laddr, []byte(natHoleRespMsg.Sid)) + pxy.Trace("send all detect msg done") - lConn, err := net.DialUDP("udp", laddr, daddr) + msg.WriteMsg(conn, &msg.NatHoleClientDetectOK{}) + + // Listen for clientConn's address and wait for visitor connection + lConn, err := net.ListenUDP("udp", laddr) if err != nil { - pxy.Error("dial visitor udp address error: %v", err) + pxy.Error("listen on visitorConn's local adress error: %v", err) return } - lConn.Write([]byte(natHoleRespMsg.Sid)) + defer lConn.Close() - kcpConn, err := frpNet.NewKcpConnFromUdp(lConn, true, natHoleRespMsg.VisitorAddr) + lConn.SetReadDeadline(time.Now().Add(8 * time.Second)) + sidBuf := pool.GetBuf(1024) + var uAddr *net.UDPAddr + n, uAddr, err = lConn.ReadFromUDP(sidBuf) + if err != nil { + pxy.Warn("get sid from visitor error: %v", err) + return + } + lConn.SetReadDeadline(time.Time{}) + if string(sidBuf[:n]) != natHoleRespMsg.Sid { + pxy.Warn("incorrect sid from visitor") + return + } + pool.PutBuf(sidBuf) + pxy.Info("nat hole connection make success, sid [%s]", natHoleRespMsg.Sid) + + lConn.WriteToUDP(sidBuf[:n], uAddr) + + kcpConn, err := frpNet.NewKcpConnFromUdp(lConn, false, natHoleRespMsg.VisitorAddr) if err != nil { pxy.Error("create kcp connection from udp connection error: %v", err) return } + fmuxCfg := fmux.DefaultConfig() + fmuxCfg.KeepAliveInterval = 5 * time.Second + fmuxCfg.LogOutput = ioutil.Discard + sess, err := fmux.Server(kcpConn, fmuxCfg) + if err != nil { + pxy.Error("create yamux server from kcp connection error: %v", err) + return + } + defer sess.Close() + muxConn, err := sess.Accept() + if err != nil { + pxy.Error("accept for yamux connection error: %v", err) + return + } + HandleTcpWorkConnection(&pxy.cfg.LocalSvrConf, pxy.proxyPlugin, &pxy.cfg.BaseProxyConf, - frpNet.WrapConn(kcpConn), []byte(pxy.cfg.Sk)) + frpNet.WrapConn(muxConn), []byte(pxy.cfg.Sk)) +} + +func (pxy *XtcpProxy) sendDetectMsg(addr string, port int, laddr *net.UDPAddr, content []byte) (err error) { + daddr, err := net.ResolveUDPAddr("udp", fmt.Sprintf("%s:%d", addr, port)) + if err != nil { + return err + } + + tConn, err := net.DialUDP("udp", laddr, daddr) + if err != nil { + return err + } + + //uConn := ipv4.NewConn(tConn) + //uConn.SetTTL(3) + + tConn.Write(content) + tConn.Close() + return nil } // UDP diff --git a/client/service.go b/client/service.go index 19bb90cd..7fed2dc2 100644 --- a/client/service.go +++ b/client/service.go @@ -15,6 +15,7 @@ package client import ( + "crypto/tls" "fmt" "io/ioutil" "runtime" @@ -163,8 +164,14 @@ func (svr *Service) keepControllerWorking() { // conn: control connection // session: if it's not nil, using tcp mux func (svr *Service) login() (conn frpNet.Conn, session *fmux.Session, err error) { + var tlsConfig *tls.Config + if g.GlbClientCfg.TLSEnable { + tlsConfig = &tls.Config{ + InsecureSkipVerify: true, + } + } conn, err = frpNet.ConnectServerByProxy(g.GlbClientCfg.HttpProxy, g.GlbClientCfg.Protocol, - newAddress(g.GlbClientCfg.ServerAddr, g.GlbClientCfg.ServerPort)) + newAddress(g.GlbClientCfg.ServerAddr, g.GlbClientCfg.ServerPort), tlsConfig) if err != nil { return } diff --git a/client/visitor.go b/client/visitor.go index e184b249..15866983 100644 --- a/client/visitor.go +++ b/client/visitor.go @@ -17,14 +17,11 @@ package client import ( "bytes" "io" + "io/ioutil" "net" - "strconv" - "strings" "sync" "time" - "golang.org/x/net/ipv4" - "github.com/fatedier/frp/g" "github.com/fatedier/frp/models/config" "github.com/fatedier/frp/models/msg" @@ -34,6 +31,7 @@ import ( frpIo "github.com/fatedier/golib/io" "github.com/fatedier/golib/pool" + fmux "github.com/hashicorp/yamux" ) // Visitor is used for forward traffics from local port tot remote service. @@ -247,40 +245,31 @@ func (sv *XtcpVisitor) handleConn(userConn frpNet.Conn) { return } - sv.Trace("get natHoleRespMsg, sid [%s], client address [%s]", natHoleRespMsg.Sid, natHoleRespMsg.ClientAddr) + sv.Trace("get natHoleRespMsg, sid [%s], client address [%s], visitor address [%s]", natHoleRespMsg.Sid, natHoleRespMsg.ClientAddr, natHoleRespMsg.VisitorAddr) // Close visitorConn, so we can use it's local address. visitorConn.Close() - // Send detect message. - array := strings.Split(natHoleRespMsg.ClientAddr, ":") - if len(array) <= 1 { - sv.Error("get natHoleResp client address error: %s", natHoleRespMsg.ClientAddr) - return - } + // send sid message to client laddr, _ := net.ResolveUDPAddr("udp", visitorConn.LocalAddr().String()) - /* - for i := 1000; i < 65000; i++ { - sv.sendDetectMsg(array[0], int64(i), laddr, "a") - } - */ - port, err := strconv.ParseInt(array[1], 10, 64) + daddr, err := net.ResolveUDPAddr("udp", natHoleRespMsg.ClientAddr) if err != nil { - sv.Error("get natHoleResp client address error: %s", natHoleRespMsg.ClientAddr) + sv.Error("resolve client udp address error: %v", err) return } - sv.sendDetectMsg(array[0], int(port), laddr, []byte(natHoleRespMsg.Sid)) - sv.Trace("send all detect msg done") + lConn, err := net.DialUDP("udp", laddr, daddr) + if err != nil { + sv.Error("dial client udp address error: %v", err) + return + } + defer lConn.Close() - // Listen for visitorConn's address and wait for client connection. - lConn, err := net.ListenUDP("udp", laddr) - if err != nil { - sv.Error("listen on visitorConn's local adress error: %v", err) - return - } - lConn.SetReadDeadline(time.Now().Add(5 * time.Second)) + lConn.Write([]byte(natHoleRespMsg.Sid)) + + // read ack sid from client sidBuf := pool.GetBuf(1024) - n, _, err = lConn.ReadFromUDP(sidBuf) + lConn.SetReadDeadline(time.Now().Add(8 * time.Second)) + n, err = lConn.Read(sidBuf) if err != nil { sv.Warn("get sid from client error: %v", err) return @@ -290,11 +279,13 @@ func (sv *XtcpVisitor) handleConn(userConn frpNet.Conn) { sv.Warn("incorrect sid from client") return } - sv.Info("nat hole connection make success, sid [%s]", string(sidBuf[:n])) pool.PutBuf(sidBuf) + sv.Info("nat hole connection make success, sid [%s]", natHoleRespMsg.Sid) + + // wrap kcp connection var remote io.ReadWriteCloser - remote, err = frpNet.NewKcpConnFromUdp(lConn, false, natHoleRespMsg.ClientAddr) + remote, err = frpNet.NewKcpConnFromUdp(lConn, true, natHoleRespMsg.ClientAddr) if err != nil { sv.Error("create kcp connection from udp connection error: %v", err) return @@ -312,25 +303,21 @@ func (sv *XtcpVisitor) handleConn(userConn frpNet.Conn) { remote = frpIo.WithCompression(remote) } - frpIo.Join(userConn, remote) + fmuxCfg := fmux.DefaultConfig() + fmuxCfg.KeepAliveInterval = 5 * time.Second + fmuxCfg.LogOutput = ioutil.Discard + sess, err := fmux.Client(remote, fmuxCfg) + if err != nil { + sv.Error("create yamux session error: %v", err) + return + } + defer sess.Close() + muxConn, err := sess.Open() + if err != nil { + sv.Error("open yamux stream error: %v", err) + return + } + + frpIo.Join(userConn, muxConn) sv.Debug("join connections closed") } - -func (sv *XtcpVisitor) sendDetectMsg(addr string, port int, laddr *net.UDPAddr, content []byte) (err error) { - daddr, err := net.ResolveUDPAddr("udp", newAddress(addr, port)) - if err != nil { - return err - } - - tConn, err := net.DialUDP("udp", laddr, daddr) - if err != nil { - return err - } - - uConn := ipv4.NewConn(tConn) - uConn.SetTTL(3) - - tConn.Write(content) - tConn.Close() - return nil -} diff --git a/cmd/frpc/sub/xtcp.go b/cmd/frpc/sub/xtcp.go index 0a81e18b..c15ac5ad 100644 --- a/cmd/frpc/sub/xtcp.go +++ b/cmd/frpc/sub/xtcp.go @@ -68,7 +68,7 @@ var xtcpCmd = &cobra.Command{ if role == "server" { cfg := &config.XtcpProxyConf{} cfg.ProxyName = prefix + proxyName - cfg.ProxyType = consts.StcpProxy + cfg.ProxyType = consts.XtcpProxy cfg.UseEncryption = useEncryption cfg.UseCompression = useCompression cfg.Role = role @@ -84,7 +84,7 @@ var xtcpCmd = &cobra.Command{ } else if role == "visitor" { cfg := &config.XtcpVisitorConf{} cfg.ProxyName = prefix + proxyName - cfg.ProxyType = consts.StcpProxy + cfg.ProxyType = consts.XtcpProxy cfg.UseEncryption = useEncryption cfg.UseCompression = useCompression cfg.Role = role diff --git a/cmd/frps/root.go b/cmd/frps/root.go index 573417f7..cc7e0e17 100644 --- a/cmd/frps/root.go +++ b/cmd/frps/root.go @@ -187,9 +187,9 @@ func parseServerCommonCfgFromCmd() (err error) { g.GlbServerCfg.MaxPortsPerClient = maxPortsPerClient if logFile == "console" { - g.GlbClientCfg.LogWay = "console" + g.GlbServerCfg.LogWay = "console" } else { - g.GlbClientCfg.LogWay = "file" + g.GlbServerCfg.LogWay = "file" } return } diff --git a/conf/frpc_full.ini b/conf/frpc_full.ini index 29ef0e14..60bf859d 100644 --- a/conf/frpc_full.ini +++ b/conf/frpc_full.ini @@ -44,6 +44,9 @@ login_fail_exit = true # now it supports tcp and kcp and websocket, default is tcp protocol = tcp +# if tls_enable is true, frpc will connect frps by tls +tls_enable = true + # specify a dns server, so frpc will use this instead of default one # dns_server = 8.8.8.8 diff --git a/go.mod b/go.mod index 3870c2f0..fe82c1fc 100644 --- a/go.mod +++ b/go.mod @@ -12,7 +12,7 @@ require ( github.com/gorilla/context v1.1.1 // indirect github.com/gorilla/mux v1.6.2 github.com/gorilla/websocket v1.2.0 - github.com/hashicorp/yamux v0.0.0-20180314200745-2658be15c5f0 + github.com/hashicorp/yamux v0.0.0-20181012175058-2f1d1f20f75d github.com/inconshreveable/mousetrap v1.0.0 // indirect github.com/mattn/go-runewidth v0.0.4 // indirect github.com/pkg/errors v0.8.0 // indirect diff --git a/go.sum b/go.sum index 86992a17..e71b6020 100644 --- a/go.sum +++ b/go.sum @@ -9,7 +9,8 @@ github.com/gorilla/context v1.1.1 h1:AWwleXJkX/nhcU9bZSnZoi3h/qGYqQAGhq6zZe/aQW8 github.com/gorilla/context v1.1.1/go.mod h1:kBGZzfjB9CEq2AlWe17Uuf7NDRt0dE0s8S51q0aT7Yg= github.com/gorilla/mux v1.6.2/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs= github.com/gorilla/websocket v1.2.0/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ= -github.com/hashicorp/yamux v0.0.0-20180314200745-2658be15c5f0/go.mod h1:+NfK9FKeTrX5uv1uIXGdwYDTeHna2qgaIlx54MXqjAM= +github.com/hashicorp/yamux v0.0.0-20181012175058-2f1d1f20f75d h1:kJCB4vdITiW1eC1vq2e6IsrXKrZit1bv/TDYFGMp4BQ= +github.com/hashicorp/yamux v0.0.0-20181012175058-2f1d1f20f75d/go.mod h1:+NfK9FKeTrX5uv1uIXGdwYDTeHna2qgaIlx54MXqjAM= github.com/inconshreveable/mousetrap v1.0.0 h1:Z8tu5sraLXCXIcARxBp/8cbvlwVa7Z1NHg9XEKhtSvM= github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8= github.com/mattn/go-runewidth v0.0.4 h1:2BvfKmzob6Bmd4YsL0zygOqfdFnK7GR4QL06Do4/p7Y= diff --git a/models/config/client_common.go b/models/config/client_common.go index 5dc49aa0..1cd9ffc5 100644 --- a/models/config/client_common.go +++ b/models/config/client_common.go @@ -44,6 +44,7 @@ type ClientCommonConf struct { LoginFailExit bool `json:"login_fail_exit"` Start map[string]struct{} `json:"start"` Protocol string `json:"protocol"` + TLSEnable bool `json:"tls_enable"` HeartBeatInterval int64 `json:"heartbeat_interval"` HeartBeatTimeout int64 `json:"heartbeat_timeout"` } @@ -69,6 +70,7 @@ func GetDefaultClientConf() *ClientCommonConf { LoginFailExit: true, Start: make(map[string]struct{}), Protocol: "tcp", + TLSEnable: false, HeartBeatInterval: 30, HeartBeatTimeout: 90, } @@ -194,6 +196,12 @@ func UnmarshalClientConfFromIni(defaultCfg *ClientCommonConf, content string) (c cfg.Protocol = tmpStr } + if tmpStr, ok = conf.Get("common", "tls_enable"); ok && tmpStr == "true" { + cfg.TLSEnable = true + } else { + cfg.TLSEnable = false + } + if tmpStr, ok = conf.Get("common", "heartbeat_timeout"); ok { if v, err = strconv.ParseInt(tmpStr, 10, 64); err != nil { err = fmt.Errorf("Parse conf error: invalid heartbeat_timeout") diff --git a/models/msg/msg.go b/models/msg/msg.go index e06fa371..2d5985c4 100644 --- a/models/msg/msg.go +++ b/models/msg/msg.go @@ -17,44 +17,46 @@ package msg import "net" const ( - TypeLogin = 'o' - TypeLoginResp = '1' - TypeNewProxy = 'p' - TypeNewProxyResp = '2' - TypeCloseProxy = 'c' - TypeNewWorkConn = 'w' - TypeReqWorkConn = 'r' - TypeStartWorkConn = 's' - TypeNewVisitorConn = 'v' - TypeNewVisitorConnResp = '3' - TypePing = 'h' - TypePong = '4' - TypeUdpPacket = 'u' - TypeNatHoleVisitor = 'i' - TypeNatHoleClient = 'n' - TypeNatHoleResp = 'm' - TypeNatHoleSid = '5' + TypeLogin = 'o' + TypeLoginResp = '1' + TypeNewProxy = 'p' + TypeNewProxyResp = '2' + TypeCloseProxy = 'c' + TypeNewWorkConn = 'w' + TypeReqWorkConn = 'r' + TypeStartWorkConn = 's' + TypeNewVisitorConn = 'v' + TypeNewVisitorConnResp = '3' + TypePing = 'h' + TypePong = '4' + TypeUdpPacket = 'u' + TypeNatHoleVisitor = 'i' + TypeNatHoleClient = 'n' + TypeNatHoleResp = 'm' + TypeNatHoleClientDetectOK = 'd' + TypeNatHoleSid = '5' ) var ( msgTypeMap = map[byte]interface{}{ - TypeLogin: Login{}, - TypeLoginResp: LoginResp{}, - TypeNewProxy: NewProxy{}, - TypeNewProxyResp: NewProxyResp{}, - TypeCloseProxy: CloseProxy{}, - TypeNewWorkConn: NewWorkConn{}, - TypeReqWorkConn: ReqWorkConn{}, - TypeStartWorkConn: StartWorkConn{}, - TypeNewVisitorConn: NewVisitorConn{}, - TypeNewVisitorConnResp: NewVisitorConnResp{}, - TypePing: Ping{}, - TypePong: Pong{}, - TypeUdpPacket: UdpPacket{}, - TypeNatHoleVisitor: NatHoleVisitor{}, - TypeNatHoleClient: NatHoleClient{}, - TypeNatHoleResp: NatHoleResp{}, - TypeNatHoleSid: NatHoleSid{}, + TypeLogin: Login{}, + TypeLoginResp: LoginResp{}, + TypeNewProxy: NewProxy{}, + TypeNewProxyResp: NewProxyResp{}, + TypeCloseProxy: CloseProxy{}, + TypeNewWorkConn: NewWorkConn{}, + TypeReqWorkConn: ReqWorkConn{}, + TypeStartWorkConn: StartWorkConn{}, + TypeNewVisitorConn: NewVisitorConn{}, + TypeNewVisitorConnResp: NewVisitorConnResp{}, + TypePing: Ping{}, + TypePong: Pong{}, + TypeUdpPacket: UdpPacket{}, + TypeNatHoleVisitor: NatHoleVisitor{}, + TypeNatHoleClient: NatHoleClient{}, + TypeNatHoleResp: NatHoleResp{}, + TypeNatHoleClientDetectOK: NatHoleClientDetectOK{}, + TypeNatHoleSid: NatHoleSid{}, } ) @@ -169,6 +171,9 @@ type NatHoleResp struct { Error string `json:"error"` } +type NatHoleClientDetectOK struct { +} + type NatHoleSid struct { Sid string `json:"sid"` } diff --git a/models/nathole/nathole.go b/models/nathole/nathole.go index 1e120ae2..0c33dfe4 100644 --- a/models/nathole/nathole.go +++ b/models/nathole/nathole.go @@ -18,6 +18,11 @@ import ( // Timeout seconds. var NatHoleTimeout int64 = 10 +type SidRequest struct { + Sid string + NotifyCh chan struct{} +} + type NatHoleController struct { listener *net.UDPConn @@ -44,11 +49,11 @@ func NewNatHoleController(udpBindAddr string) (nc *NatHoleController, err error) return nc, nil } -func (nc *NatHoleController) ListenClient(name string, sk string) (sidCh chan string) { +func (nc *NatHoleController) ListenClient(name string, sk string) (sidCh chan *SidRequest) { clientCfg := &NatHoleClientCfg{ Name: name, Sk: sk, - SidCh: make(chan string), + SidCh: make(chan *SidRequest), } nc.mu.Lock() nc.clientCfgs[name] = clientCfg @@ -132,7 +137,10 @@ func (nc *NatHoleController) HandleVisitor(m *msg.NatHoleVisitor, raddr *net.UDP }() err := errors.PanicToError(func() { - clientCfg.SidCh <- sid + clientCfg.SidCh <- &SidRequest{ + Sid: sid, + NotifyCh: session.NotifyCh, + } }) if err != nil { return @@ -158,7 +166,6 @@ func (nc *NatHoleController) HandleClient(m *msg.NatHoleClient, raddr *net.UDPAd } log.Trace("handle client message, sid [%s]", session.Sid) session.ClientAddr = raddr - session.NotifyCh <- struct{}{} resp := nc.GenNatHoleResponse(session, "") log.Trace("send nat hole response to client") @@ -201,5 +208,5 @@ type NatHoleSession struct { type NatHoleClientCfg struct { Name string Sk string - SidCh chan string + SidCh chan *SidRequest } diff --git a/server/proxy/xtcp.go b/server/proxy/xtcp.go index 9c5f9112..87266669 100644 --- a/server/proxy/xtcp.go +++ b/server/proxy/xtcp.go @@ -42,18 +42,40 @@ func (pxy *XtcpProxy) Run() (remoteAddr string, err error) { select { case <-pxy.closeCh: break - case sid := <-sidCh: + case sidRequest := <-sidCh: + sr := sidRequest workConn, errRet := pxy.GetWorkConnFromPool() if errRet != nil { continue } m := &msg.NatHoleSid{ - Sid: sid, + Sid: sr.Sid, } errRet = msg.WriteMsg(workConn, m) if errRet != nil { pxy.Warn("write nat hole sid package error, %v", errRet) + workConn.Close() + break } + + go func() { + raw, errRet := msg.ReadMsg(workConn) + if errRet != nil { + pxy.Warn("read nat hole client ok package error: %v", errRet) + workConn.Close() + return + } + if _, ok := raw.(*msg.NatHoleClientDetectOK); !ok { + pxy.Warn("read nat hole client ok package format error") + workConn.Close() + return + } + + select { + case sr.NotifyCh <- struct{}{}: + default: + } + }() } } }() diff --git a/server/service.go b/server/service.go index 453fe4a7..f0873311 100644 --- a/server/service.go +++ b/server/service.go @@ -16,8 +16,14 @@ package server import ( "bytes" + "crypto/rand" + "crypto/rsa" + "crypto/tls" + "crypto/x509" + "encoding/pem" "fmt" "io/ioutil" + "math/big" "net" "net/http" "strings" @@ -62,6 +68,9 @@ type Service struct { // Accept connections using websocket websocketListener frpNet.Listener + // Accept frp tls connections + tlsListener frpNet.Listener + // Manage all controllers ctlManager *ControlManager @@ -79,6 +88,8 @@ type Service struct { // close chan closedCh chan bool + + tlsConfig *tls.Config } func newAddress(addr string, port int) string { @@ -101,6 +112,7 @@ func NewService() (svr *Service, err error) { }, Closed: true, closedCh: make(chan bool), + tlsConfig: generateTLSConfig(), } // Init group controller @@ -204,6 +216,12 @@ func NewService() (svr *Service, err error) { log.Info("https service listen on %s:%d", cfg.ProxyBindAddr, cfg.VhostHttpsPort) } + // frp tls listener + tlsListener := svr.muxer.Listen(1, 1, func(data []byte) bool { + return int(data[0]) == frpNet.FRP_TLS_HEAD_BYTE + }) + svr.tlsListener = frpNet.WrapLogListener(tlsListener) + // Create nat hole controller. if cfg.BindUdpPort > 0 { var nc *nathole.NatHoleController @@ -242,6 +260,7 @@ func (svr *Service) Run() { } go svr.HandleListener(svr.websocketListener) + go svr.HandleListener(svr.tlsListener) svr.Closed = false svr.HandleListener(svr.listener) @@ -279,6 +298,7 @@ func (svr *Service) HandleListener(l frpNet.Listener) { return } log.Info("New Conn: ", c, err) + c = frpNet.CheckAndEnableTLSServerConn(c, svr.tlsConfig) // Start a new goroutine for dealing connections. go func(frpConn frpNet.Conn) { @@ -415,3 +435,24 @@ func (svr *Service) RegisterVisitorConn(visitorConn frpNet.Conn, newMsg *msg.New return svr.rc.VisitorManager.NewConn(newMsg.ProxyName, visitorConn, newMsg.Timestamp, newMsg.SignKey, newMsg.UseEncryption, newMsg.UseCompression) } + +// Setup a bare-bones TLS config for the server +func generateTLSConfig() *tls.Config { + key, err := rsa.GenerateKey(rand.Reader, 1024) + if err != nil { + panic(err) + } + template := x509.Certificate{SerialNumber: big.NewInt(1)} + certDER, err := x509.CreateCertificate(rand.Reader, &template, &template, &key.PublicKey, key) + if err != nil { + panic(err) + } + keyPEM := pem.EncodeToMemory(&pem.Block{Type: "RSA PRIVATE KEY", Bytes: x509.MarshalPKCS1PrivateKey(key)}) + certPEM := pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: certDER}) + + tlsCert, err := tls.X509KeyPair(certPEM, keyPEM) + if err != nil { + panic(err) + } + return &tls.Config{Certificates: []tls.Certificate{tlsCert}} +} diff --git a/tests/ci/auto_test_frpc.ini b/tests/ci/auto_test_frpc.ini index 407d679e..28ea5fd5 100644 --- a/tests/ci/auto_test_frpc.ini +++ b/tests/ci/auto_test_frpc.ini @@ -127,6 +127,12 @@ custom_domains = test6.frp.com host_header_rewrite = test6.frp.com header_X-From-Where = frp +[wildcard_http] +type = http +local_ip = 127.0.0.1 +local_port = 10704 +custom_domains = *.frp1.com + [subhost01] type = http local_ip = 127.0.0.1 diff --git a/tests/ci/cmd_test.go b/tests/ci/cmd_test.go index ea76e6de..44f2e9e8 100644 --- a/tests/ci/cmd_test.go +++ b/tests/ci/cmd_test.go @@ -19,7 +19,7 @@ func TestCmdTcp(t *testing.T) { if assert.NoError(err) { defer s.Stop() } - time.Sleep(100 * time.Millisecond) + time.Sleep(200 * time.Millisecond) c := util.NewProcess(consts.FRPC_BIN_PATH, []string{"tcp", "-s", "127.0.0.1:20000", "-t", "123", "-u", "test", "-l", "10701", "-r", "20801", "-n", "tcp_test"}) @@ -27,7 +27,7 @@ func TestCmdTcp(t *testing.T) { if assert.NoError(err) { defer c.Stop() } - time.Sleep(250 * time.Millisecond) + time.Sleep(500 * time.Millisecond) res, err := util.SendTcpMsg("127.0.0.1:20801", consts.TEST_TCP_ECHO_STR) assert.NoError(err) @@ -43,7 +43,7 @@ func TestCmdUdp(t *testing.T) { if assert.NoError(err) { defer s.Stop() } - time.Sleep(100 * time.Millisecond) + time.Sleep(200 * time.Millisecond) c := util.NewProcess(consts.FRPC_BIN_PATH, []string{"udp", "-s", "127.0.0.1:20000", "-t", "123", "-u", "test", "-l", "10702", "-r", "20802", "-n", "udp_test"}) @@ -51,7 +51,7 @@ func TestCmdUdp(t *testing.T) { if assert.NoError(err) { defer c.Stop() } - time.Sleep(250 * time.Millisecond) + time.Sleep(500 * time.Millisecond) res, err := util.SendUdpMsg("127.0.0.1:20802", consts.TEST_UDP_ECHO_STR) assert.NoError(err) @@ -67,7 +67,7 @@ func TestCmdHttp(t *testing.T) { if assert.NoError(err) { defer s.Stop() } - time.Sleep(100 * time.Millisecond) + time.Sleep(200 * time.Millisecond) c := util.NewProcess(consts.FRPC_BIN_PATH, []string{"http", "-s", "127.0.0.1:20000", "-t", "123", "-u", "test", "-n", "udp_test", "-l", "10704", "--custom_domain", "127.0.0.1"}) @@ -75,7 +75,7 @@ func TestCmdHttp(t *testing.T) { if assert.NoError(err) { defer c.Stop() } - time.Sleep(250 * time.Millisecond) + time.Sleep(500 * time.Millisecond) code, body, _, err := util.SendHttpMsg("GET", "http://127.0.0.1:20001", "", nil, "") if assert.NoError(err) { diff --git a/tests/ci/normal_test.go b/tests/ci/normal_test.go index 24f5795a..4f976c81 100644 --- a/tests/ci/normal_test.go +++ b/tests/ci/normal_test.go @@ -182,6 +182,21 @@ func TestHttp(t *testing.T) { assert.Equal("true", header.Get("X-Header-Set")) } + // wildcard_http + // test.frp1.com match *.frp1.com + code, body, _, err = util.SendHttpMsg("GET", fmt.Sprintf("http://127.0.0.1:%d", consts.TEST_HTTP_FRP_PORT), "test.frp1.com", nil, "") + if assert.NoError(err) { + assert.Equal(200, code) + assert.Equal(consts.TEST_HTTP_NORMAL_STR, body) + } + + // new.test.frp1.com also match *.frp1.com + code, body, _, err = util.SendHttpMsg("GET", fmt.Sprintf("http://127.0.0.1:%d", consts.TEST_HTTP_FRP_PORT), "new.test.frp1.com", nil, "") + if assert.NoError(err) { + assert.Equal(200, code) + assert.Equal(consts.TEST_HTTP_NORMAL_STR, body) + } + // subhost01 code, body, _, err = util.SendHttpMsg("GET", fmt.Sprintf("http://127.0.0.1:%d", consts.TEST_HTTP_FRP_PORT), "test01.sub.com", nil, "") if assert.NoError(err) { diff --git a/tests/ci/reconnect_test.go b/tests/ci/reconnect_test.go index 114567b2..9fe53b5e 100644 --- a/tests/ci/reconnect_test.go +++ b/tests/ci/reconnect_test.go @@ -56,14 +56,14 @@ func TestReconnect(t *testing.T) { defer frpsProcess.Stop() } - time.Sleep(100 * time.Millisecond) + time.Sleep(200 * time.Millisecond) frpcProcess := util.NewProcess(consts.FRPC_BIN_PATH, []string{"-c", frpcCfgPath}) err = frpcProcess.Start() if assert.NoError(err) { defer frpcProcess.Stop() } - time.Sleep(250 * time.Millisecond) + time.Sleep(500 * time.Millisecond) // test tcp res, err := util.SendTcpMsg("127.0.0.1:20801", consts.TEST_TCP_ECHO_STR) @@ -72,7 +72,7 @@ func TestReconnect(t *testing.T) { // stop frpc frpcProcess.Stop() - time.Sleep(100 * time.Millisecond) + time.Sleep(200 * time.Millisecond) // test tcp, expect failed _, err = util.SendTcpMsg("127.0.0.1:20801", consts.TEST_TCP_ECHO_STR) @@ -84,7 +84,7 @@ func TestReconnect(t *testing.T) { if assert.NoError(err) { defer newFrpcProcess.Stop() } - time.Sleep(250 * time.Millisecond) + time.Sleep(500 * time.Millisecond) // test tcp res, err = util.SendTcpMsg("127.0.0.1:20801", consts.TEST_TCP_ECHO_STR) @@ -93,7 +93,7 @@ func TestReconnect(t *testing.T) { // stop frps frpsProcess.Stop() - time.Sleep(100 * time.Millisecond) + time.Sleep(200 * time.Millisecond) // test tcp, expect failed _, err = util.SendTcpMsg("127.0.0.1:20801", consts.TEST_TCP_ECHO_STR) diff --git a/tests/ci/reload_test.go b/tests/ci/reload_test.go index f05f2edb..4c5b4d45 100644 --- a/tests/ci/reload_test.go +++ b/tests/ci/reload_test.go @@ -94,7 +94,7 @@ func TestReload(t *testing.T) { defer frpsProcess.Stop() } - time.Sleep(100 * time.Millisecond) + time.Sleep(200 * time.Millisecond) frpcProcess := util.NewProcess(consts.FRPC_BIN_PATH, []string{"-c", frpcCfgPath}) err = frpcProcess.Start() @@ -102,7 +102,7 @@ func TestReload(t *testing.T) { defer frpcProcess.Stop() } - time.Sleep(250 * time.Millisecond) + time.Sleep(500 * time.Millisecond) // test tcp1 res, err := util.SendTcpMsg("127.0.0.1:20801", consts.TEST_TCP_ECHO_STR) diff --git a/tests/ci/template_test.go b/tests/ci/template_test.go index 09b84177..1e30af52 100644 --- a/tests/ci/template_test.go +++ b/tests/ci/template_test.go @@ -55,7 +55,7 @@ func TestConfTemplate(t *testing.T) { defer frpsProcess.Stop() } - time.Sleep(100 * time.Millisecond) + time.Sleep(200 * time.Millisecond) frpcProcess := util.NewProcess("env", []string{"FRP_TOKEN=123456", "TCP_REMOTE_PORT=20801", consts.FRPC_BIN_PATH, "-c", frpcCfgPath}) err = frpcProcess.Start() @@ -63,7 +63,7 @@ func TestConfTemplate(t *testing.T) { defer frpcProcess.Stop() } - time.Sleep(250 * time.Millisecond) + time.Sleep(500 * time.Millisecond) // test tcp1 res, err := util.SendTcpMsg("127.0.0.1:20801", consts.TEST_TCP_ECHO_STR) diff --git a/tests/ci/tls_test.go b/tests/ci/tls_test.go new file mode 100644 index 00000000..d2ad8013 --- /dev/null +++ b/tests/ci/tls_test.go @@ -0,0 +1,188 @@ +package ci + +import ( + "os" + "testing" + "time" + + "github.com/fatedier/frp/tests/config" + "github.com/fatedier/frp/tests/consts" + "github.com/fatedier/frp/tests/util" + + "github.com/stretchr/testify/assert" +) + +const FRPS_TLS_TCP_CONF = ` +[common] +bind_addr = 0.0.0.0 +bind_port = 20000 +log_file = console +log_level = debug +token = 123456 +` + +const FRPC_TLS_TCP_CONF = ` +[common] +server_addr = 127.0.0.1 +server_port = 20000 +log_file = console +log_level = debug +token = 123456 +protocol = tcp +tls_enable = true + +[tcp] +type = tcp +local_port = 10701 +remote_port = 20801 +` + +func TestTlsOverTCP(t *testing.T) { + assert := assert.New(t) + frpsCfgPath, err := config.GenerateConfigFile(consts.FRPS_NORMAL_CONFIG, FRPS_TLS_TCP_CONF) + if assert.NoError(err) { + defer os.Remove(frpsCfgPath) + } + + frpcCfgPath, err := config.GenerateConfigFile(consts.FRPC_NORMAL_CONFIG, FRPC_TLS_TCP_CONF) + if assert.NoError(err) { + defer os.Remove(frpcCfgPath) + } + + frpsProcess := util.NewProcess(consts.FRPS_BIN_PATH, []string{"-c", frpsCfgPath}) + err = frpsProcess.Start() + if assert.NoError(err) { + defer frpsProcess.Stop() + } + + time.Sleep(200 * time.Millisecond) + + frpcProcess := util.NewProcess(consts.FRPC_BIN_PATH, []string{"-c", frpcCfgPath}) + err = frpcProcess.Start() + if assert.NoError(err) { + defer frpcProcess.Stop() + } + time.Sleep(500 * time.Millisecond) + + // test tcp + res, err := util.SendTcpMsg("127.0.0.1:20801", consts.TEST_TCP_ECHO_STR) + assert.NoError(err) + assert.Equal(consts.TEST_TCP_ECHO_STR, res) +} + +const FRPS_TLS_KCP_CONF = ` +[common] +bind_addr = 0.0.0.0 +bind_port = 20000 +kcp_bind_port = 20000 +log_file = console +log_level = debug +token = 123456 +` + +const FRPC_TLS_KCP_CONF = ` +[common] +server_addr = 127.0.0.1 +server_port = 20000 +log_file = console +log_level = debug +token = 123456 +protocol = kcp +tls_enable = true + +[tcp] +type = tcp +local_port = 10701 +remote_port = 20801 +` + +func TestTLSOverKCP(t *testing.T) { + assert := assert.New(t) + frpsCfgPath, err := config.GenerateConfigFile(consts.FRPS_NORMAL_CONFIG, FRPS_TLS_KCP_CONF) + if assert.NoError(err) { + defer os.Remove(frpsCfgPath) + } + + frpcCfgPath, err := config.GenerateConfigFile(consts.FRPC_NORMAL_CONFIG, FRPC_TLS_KCP_CONF) + if assert.NoError(err) { + defer os.Remove(frpcCfgPath) + } + + frpsProcess := util.NewProcess(consts.FRPS_BIN_PATH, []string{"-c", frpsCfgPath}) + err = frpsProcess.Start() + if assert.NoError(err) { + defer frpsProcess.Stop() + } + + time.Sleep(200 * time.Millisecond) + + frpcProcess := util.NewProcess(consts.FRPC_BIN_PATH, []string{"-c", frpcCfgPath}) + err = frpcProcess.Start() + if assert.NoError(err) { + defer frpcProcess.Stop() + } + time.Sleep(500 * time.Millisecond) + + // test tcp + res, err := util.SendTcpMsg("127.0.0.1:20801", consts.TEST_TCP_ECHO_STR) + assert.NoError(err) + assert.Equal(consts.TEST_TCP_ECHO_STR, res) +} + +const FRPS_TLS_WS_CONF = ` +[common] +bind_addr = 0.0.0.0 +bind_port = 20000 +log_file = console +log_level = debug +token = 123456 +` + +const FRPC_TLS_WS_CONF = ` +[common] +server_addr = 127.0.0.1 +server_port = 20000 +log_file = console +log_level = debug +token = 123456 +protocol = websocket +tls_enable = true + +[tcp] +type = tcp +local_port = 10701 +remote_port = 20801 +` + +func TestTLSOverWebsocket(t *testing.T) { + assert := assert.New(t) + frpsCfgPath, err := config.GenerateConfigFile(consts.FRPS_NORMAL_CONFIG, FRPS_TLS_WS_CONF) + if assert.NoError(err) { + defer os.Remove(frpsCfgPath) + } + + frpcCfgPath, err := config.GenerateConfigFile(consts.FRPC_NORMAL_CONFIG, FRPC_TLS_WS_CONF) + if assert.NoError(err) { + defer os.Remove(frpcCfgPath) + } + + frpsProcess := util.NewProcess(consts.FRPS_BIN_PATH, []string{"-c", frpsCfgPath}) + err = frpsProcess.Start() + if assert.NoError(err) { + defer frpsProcess.Stop() + } + + time.Sleep(200 * time.Millisecond) + + frpcProcess := util.NewProcess(consts.FRPC_BIN_PATH, []string{"-c", frpcCfgPath}) + err = frpcProcess.Start() + if assert.NoError(err) { + defer frpcProcess.Stop() + } + time.Sleep(500 * time.Millisecond) + + // test tcp + res, err := util.SendTcpMsg("127.0.0.1:20801", consts.TEST_TCP_ECHO_STR) + assert.NoError(err) + assert.Equal(consts.TEST_TCP_ECHO_STR, res) +} diff --git a/tests/mock/http_server.go b/tests/mock/http_server.go index 37b2b1e6..92c51a6c 100644 --- a/tests/mock/http_server.go +++ b/tests/mock/http_server.go @@ -88,8 +88,10 @@ func handleHttp(w http.ResponseWriter, r *http.Request) { return } - if strings.Contains(r.Host, "127.0.0.1") || strings.Contains(r.Host, "test2.frp.com") || - strings.Contains(r.Host, "test5.frp.com") || strings.Contains(r.Host, "test6.frp.com") { + if strings.HasPrefix(r.Host, "127.0.0.1") || strings.HasPrefix(r.Host, "test2.frp.com") || + strings.HasPrefix(r.Host, "test5.frp.com") || strings.HasPrefix(r.Host, "test6.frp.com") || + strings.HasPrefix(r.Host, "test.frp1.com") || strings.HasPrefix(r.Host, "new.test.frp1.com") { + w.WriteHeader(200) w.Write([]byte(consts.TEST_HTTP_NORMAL_STR)) } else if strings.Contains(r.Host, "test3.frp.com") { diff --git a/utils/net/conn.go b/utils/net/conn.go index 6dab2bdb..e7164571 100644 --- a/utils/net/conn.go +++ b/utils/net/conn.go @@ -15,6 +15,7 @@ package net import ( + "crypto/tls" "errors" "fmt" "io" @@ -207,3 +208,13 @@ func ConnectServerByProxy(proxyUrl string, protocol string, addr string) (c Conn return nil, fmt.Errorf("unsupport protocol: %s", protocol) } } + +func ConnectServerByProxyWithTLS(proxyUrl string, protocol string, addr string, tlsConfig *tls.Config) (c Conn, err error) { + c, err = ConnectServerByProxy(proxyUrl, protocol, addr) + if tlsConfig == nil { + return + } + + c = WrapTLSClientConn(c, tlsConfig) + return +} diff --git a/utils/net/tls.go b/utils/net/tls.go new file mode 100644 index 00000000..ae1bfc70 --- /dev/null +++ b/utils/net/tls.go @@ -0,0 +1,44 @@ +// 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 net + +import ( + "crypto/tls" + "net" + + gnet "github.com/fatedier/golib/net" +) + +var ( + FRP_TLS_HEAD_BYTE = 0x17 +) + +func WrapTLSClientConn(c net.Conn, tlsConfig *tls.Config) (out Conn) { + c.Write([]byte{byte(FRP_TLS_HEAD_BYTE)}) + out = WrapConn(tls.Client(c, tlsConfig)) + return +} + +func CheckAndEnableTLSServerConn(c net.Conn, tlsConfig *tls.Config) (out Conn) { + sc, r := gnet.NewSharedConnSize(c, 1) + buf := make([]byte, 1) + n, _ := r.Read(buf) + if n == 1 && int(buf[0]) == FRP_TLS_HEAD_BYTE { + out = WrapConn(tls.Server(c, tlsConfig)) + } else { + out = WrapConn(sc) + } + return +} diff --git a/utils/version/version.go b/utils/version/version.go index 41897cb8..26ce42c3 100644 --- a/utils/version/version.go +++ b/utils/version/version.go @@ -19,7 +19,7 @@ import ( "strings" ) -var version string = "0.24.1" +var version string = "0.25.1" func Full() string { return version diff --git a/utils/vhost/http.go b/utils/vhost/http.go deleted file mode 100644 index 9fc05bdb..00000000 --- a/utils/vhost/http.go +++ /dev/null @@ -1,235 +0,0 @@ -// 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" - "encoding/base64" - "fmt" - "io" - "net/http" - "net/url" - "strings" - "time" - - frpNet "github.com/fatedier/frp/utils/net" - - gnet "github.com/fatedier/golib/net" - "github.com/fatedier/golib/pool" -) - -type HttpMuxer struct { - *VhostMuxer -} - -func GetHttpRequestInfo(c frpNet.Conn) (_ frpNet.Conn, _ map[string]string, err error) { - reqInfoMap := make(map[string]string, 0) - sc, rd := gnet.NewSharedConn(c) - - request, err := http.ReadRequest(bufio.NewReader(rd)) - if err != nil { - return nil, reqInfoMap, err - } - // hostName - tmpArr := strings.Split(request.Host, ":") - reqInfoMap["Host"] = tmpArr[0] - reqInfoMap["Path"] = request.URL.Path - reqInfoMap["Scheme"] = request.URL.Scheme - - // Authorization - authStr := request.Header.Get("Authorization") - if authStr != "" { - reqInfoMap["Authorization"] = authStr - } - request.Body.Close() - return frpNet.WrapConn(sc), reqInfoMap, nil -} - -func NewHttpMuxer(listener frpNet.Listener, timeout time.Duration) (*HttpMuxer, error) { - mux, err := NewVhostMuxer(listener, GetHttpRequestInfo, HttpAuthFunc, ModifyHttpRequest, timeout) - return &HttpMuxer{mux}, err -} - -func ModifyHttpRequest(c frpNet.Conn, rewriteHost string) (_ frpNet.Conn, err error) { - sc, rd := gnet.NewSharedConn(c) - var buff []byte - remoteIP := strings.Split(c.RemoteAddr().String(), ":")[0] - if buff, err = hostNameRewrite(rd, rewriteHost, remoteIP); err != nil { - return nil, err - } - err = sc.ResetBuf(buff) - return frpNet.WrapConn(sc), err -} - -func hostNameRewrite(request io.Reader, rewriteHost string, remoteIP string) (_ []byte, err error) { - buf := pool.GetBuf(1024) - defer pool.PutBuf(buf) - - var n int - n, err = request.Read(buf) - if err != nil { - return - } - retBuffer, err := parseRequest(buf[:n], rewriteHost, remoteIP) - return retBuffer, err -} - -func parseRequest(org []byte, rewriteHost string, remoteIP string) (ret []byte, err error) { - tp := bytes.NewBuffer(org) - // First line: GET /index.html HTTP/1.0 - var b []byte - if b, err = tp.ReadBytes('\n'); err != nil { - return nil, err - } - req := new(http.Request) - // we invoked ReadRequest in GetHttpHostname before, so we ignore error - req.Method, req.RequestURI, req.Proto, _ = parseRequestLine(string(b)) - rawurl := req.RequestURI - // CONNECT www.google.com:443 HTTP/1.1 - justAuthority := req.Method == "CONNECT" && !strings.HasPrefix(rawurl, "/") - if justAuthority { - rawurl = "http://" + rawurl - } - req.URL, _ = url.ParseRequestURI(rawurl) - if justAuthority { - // Strip the bogus "http://" back off. - req.URL.Scheme = "" - } - - // RFC2616: first case - // GET /index.html HTTP/1.1 - // Host: www.google.com - if req.URL.Host == "" { - var changedBuf []byte - if rewriteHost != "" { - changedBuf, err = changeHostName(tp, rewriteHost) - } - buf := new(bytes.Buffer) - buf.Write(b) - buf.WriteString(fmt.Sprintf("X-Forwarded-For: %s\r\n", remoteIP)) - buf.WriteString(fmt.Sprintf("X-Real-IP: %s\r\n", remoteIP)) - if len(changedBuf) == 0 { - tp.WriteTo(buf) - } else { - buf.Write(changedBuf) - } - return buf.Bytes(), err - } - - // RFC2616: second case - // GET http://www.google.com/index.html HTTP/1.1 - // Host: doesntmatter - // In this case, any Host line is ignored. - if rewriteHost != "" { - hostPort := strings.Split(req.URL.Host, ":") - if len(hostPort) == 1 { - req.URL.Host = rewriteHost - } else if len(hostPort) == 2 { - req.URL.Host = fmt.Sprintf("%s:%s", rewriteHost, hostPort[1]) - } - } - firstLine := req.Method + " " + req.URL.String() + " " + req.Proto - buf := new(bytes.Buffer) - buf.WriteString(firstLine) - buf.WriteString(fmt.Sprintf("X-Forwarded-For: %s\r\n", remoteIP)) - buf.WriteString(fmt.Sprintf("X-Real-IP: %s\r\n", remoteIP)) - tp.WriteTo(buf) - return buf.Bytes(), err -} - -// parseRequestLine parses "GET /foo HTTP/1.1" into its three parts. -func parseRequestLine(line string) (method, requestURI, proto string, ok bool) { - s1 := strings.Index(line, " ") - s2 := strings.Index(line[s1+1:], " ") - if s1 < 0 || s2 < 0 { - return - } - s2 += s1 + 1 - return line[:s1], line[s1+1 : s2], line[s2+1:], true -} - -func changeHostName(buff *bytes.Buffer, rewriteHost string) (_ []byte, err error) { - retBuf := new(bytes.Buffer) - - peek := buff.Bytes() - for len(peek) > 0 { - i := bytes.IndexByte(peek, '\n') - if i < 3 { - // Not present (-1) or found within the next few bytes, - // implying we're at the end ("\r\n\r\n" or "\n\n") - return nil, err - } - kv := peek[:i] - j := bytes.IndexByte(kv, ':') - if j < 0 { - return nil, fmt.Errorf("malformed MIME header line: " + string(kv)) - } - if strings.Contains(strings.ToLower(string(kv[:j])), "host") { - var hostHeader string - portPos := bytes.IndexByte(kv[j+1:], ':') - if portPos == -1 { - hostHeader = fmt.Sprintf("Host: %s\r\n", rewriteHost) - } else { - hostHeader = fmt.Sprintf("Host: %s:%s\r\n", rewriteHost, kv[j+portPos+2:]) - } - retBuf.WriteString(hostHeader) - peek = peek[i+1:] - break - } else { - retBuf.Write(peek[:i]) - retBuf.WriteByte('\n') - } - - peek = peek[i+1:] - } - retBuf.Write(peek) - return retBuf.Bytes(), err -} - -func HttpAuthFunc(c frpNet.Conn, userName, passWord, authorization string) (bAccess bool, err error) { - s := strings.SplitN(authorization, " ", 2) - if len(s) != 2 { - res := noAuthResponse() - res.Write(c) - return - } - b, err := base64.StdEncoding.DecodeString(s[1]) - if err != nil { - return - } - pair := strings.SplitN(string(b), ":", 2) - if len(pair) != 2 { - return - } - if pair[0] != userName || pair[1] != passWord { - return - } - return true, nil -} - -func noAuthResponse() *http.Response { - header := make(map[string][]string) - header["WWW-Authenticate"] = []string{`Basic realm="Restricted"`} - res := &http.Response{ - Status: "401 Not authorized", - StatusCode: 401, - Proto: "HTTP/1.1", - ProtoMajor: 1, - ProtoMinor: 1, - Header: header, - } - return res -} diff --git a/utils/vhost/newhttp.go b/utils/vhost/newhttp.go index fef991fa..59a4a0e6 100644 --- a/utils/vhost/newhttp.go +++ b/utils/vhost/newhttp.go @@ -18,6 +18,7 @@ import ( "bytes" "context" "errors" + "fmt" "log" "net" "net/http" @@ -145,7 +146,7 @@ func (rp *HttpReverseProxy) CreateConnection(domain string, location string) (ne return fn() } } - return nil, ErrNoDomain + return nil, fmt.Errorf("%v: %s %s", ErrNoDomain, domain, location) } func (rp *HttpReverseProxy) CheckAuth(domain, location, user, passwd string) bool { @@ -173,11 +174,22 @@ func (rp *HttpReverseProxy) getVhost(domain string, location string) (vr *VhostR domainSplit := strings.Split(domain, ".") if len(domainSplit) < 3 { - return vr, false + return nil, false + } + + for { + if len(domainSplit) < 3 { + return nil, false + } + + domainSplit[0] = "*" + domain = strings.Join(domainSplit, ".") + vr, ok = rp.vhostRouter.Get(domain, location) + if ok { + return vr, true + } + domainSplit = domainSplit[1:] } - domainSplit[0] = "*" - domain = strings.Join(domainSplit, ".") - vr, ok = rp.vhostRouter.Get(domain, location) return } diff --git a/utils/vhost/resource.go b/utils/vhost/resource.go index ed8149c6..40cb9523 100644 --- a/utils/vhost/resource.go +++ b/utils/vhost/resource.go @@ -61,3 +61,17 @@ func notFoundResponse() *http.Response { } return res } + +func noAuthResponse() *http.Response { + header := make(map[string][]string) + header["WWW-Authenticate"] = []string{`Basic realm="Restricted"`} + res := &http.Response{ + Status: "401 Not authorized", + StatusCode: 401, + Proto: "HTTP/1.1", + ProtoMajor: 1, + ProtoMinor: 1, + Header: header, + } + return res +} diff --git a/utils/vhost/vhost.go b/utils/vhost/vhost.go index 84e57e95..2e386524 100644 --- a/utils/vhost/vhost.go +++ b/utils/vhost/vhost.go @@ -102,17 +102,24 @@ func (v *VhostMuxer) getListener(name, path string) (l *Listener, exist bool) { domainSplit := strings.Split(name, ".") if len(domainSplit) < 3 { - return l, false - } - domainSplit[0] = "*" - name = strings.Join(domainSplit, ".") - - vr, found = v.registryRouter.Get(name, path) - if !found { return } - return vr.payload.(*Listener), true + for { + if len(domainSplit) < 3 { + return + } + + domainSplit[0] = "*" + name = strings.Join(domainSplit, ".") + + vr, found = v.registryRouter.Get(name, path) + if found { + return vr.payload.(*Listener), true + } + domainSplit = domainSplit[1:] + } + return } func (v *VhostMuxer) run() { diff --git a/vendor/github.com/hashicorp/yamux/go.mod b/vendor/github.com/hashicorp/yamux/go.mod new file mode 100644 index 00000000..672a0e58 --- /dev/null +++ b/vendor/github.com/hashicorp/yamux/go.mod @@ -0,0 +1 @@ +module github.com/hashicorp/yamux diff --git a/vendor/github.com/hashicorp/yamux/mux.go b/vendor/github.com/hashicorp/yamux/mux.go index 7abc7c74..18a078c8 100644 --- a/vendor/github.com/hashicorp/yamux/mux.go +++ b/vendor/github.com/hashicorp/yamux/mux.go @@ -3,6 +3,7 @@ package yamux import ( "fmt" "io" + "log" "os" "time" ) @@ -30,8 +31,13 @@ type Config struct { // window size that we allow for a stream. MaxStreamWindowSize uint32 - // LogOutput is used to control the log destination + // LogOutput is used to control the log destination. Either Logger or + // LogOutput can be set, not both. LogOutput io.Writer + + // Logger is used to pass in the logger to be used. Either Logger or + // LogOutput can be set, not both. + Logger *log.Logger } // DefaultConfig is used to return a default configuration @@ -57,6 +63,11 @@ func VerifyConfig(config *Config) error { if config.MaxStreamWindowSize < initialStreamWindow { return fmt.Errorf("MaxStreamWindowSize must be larger than %d", initialStreamWindow) } + if config.LogOutput != nil && config.Logger != nil { + return fmt.Errorf("both Logger and LogOutput may not be set, select one") + } else if config.LogOutput == nil && config.Logger == nil { + return fmt.Errorf("one of Logger or LogOutput must be set, select one") + } return nil } diff --git a/vendor/github.com/hashicorp/yamux/session.go b/vendor/github.com/hashicorp/yamux/session.go index d8446fa6..a80ddec3 100644 --- a/vendor/github.com/hashicorp/yamux/session.go +++ b/vendor/github.com/hashicorp/yamux/session.go @@ -86,9 +86,14 @@ type sendReady struct { // newSession is used to construct a new session func newSession(config *Config, conn io.ReadWriteCloser, client bool) *Session { + logger := config.Logger + if logger == nil { + logger = log.New(config.LogOutput, "", log.LstdFlags) + } + s := &Session{ config: config, - logger: log.New(config.LogOutput, "", log.LstdFlags), + logger: logger, conn: conn, bufRead: bufio.NewReader(conn), pings: make(map[uint32]chan struct{}), @@ -309,8 +314,10 @@ func (s *Session) keepalive() { case <-time.After(s.config.KeepAliveInterval): _, err := s.Ping() if err != nil { - s.logger.Printf("[ERR] yamux: keepalive failed: %v", err) - s.exitErr(ErrKeepAliveTimeout) + if err != ErrSessionShutdown { + s.logger.Printf("[ERR] yamux: keepalive failed: %v", err) + s.exitErr(ErrKeepAliveTimeout) + } return } case <-s.shutdownCh: diff --git a/vendor/modules.txt b/vendor/modules.txt index 58d2b73e..733731c3 100644 --- a/vendor/modules.txt +++ b/vendor/modules.txt @@ -23,7 +23,7 @@ github.com/gorilla/context github.com/gorilla/mux # github.com/gorilla/websocket v1.2.0 github.com/gorilla/websocket -# github.com/hashicorp/yamux v0.0.0-20180314200745-2658be15c5f0 +# github.com/hashicorp/yamux v0.0.0-20181012175058-2f1d1f20f75d github.com/hashicorp/yamux # github.com/inconshreveable/mousetrap v1.0.0 github.com/inconshreveable/mousetrap @@ -61,11 +61,11 @@ golang.org/x/crypto/twofish golang.org/x/crypto/xtea golang.org/x/crypto/salsa20/salsa # golang.org/x/net v0.0.0-20180524181706-dfa909b99c79 -golang.org/x/net/ipv4 golang.org/x/net/websocket +golang.org/x/net/context +golang.org/x/net/proxy +golang.org/x/net/ipv4 +golang.org/x/net/internal/socks golang.org/x/net/bpf golang.org/x/net/internal/iana golang.org/x/net/internal/socket -golang.org/x/net/context -golang.org/x/net/proxy -golang.org/x/net/internal/socks