diff --git a/README.md b/README.md index a0bdaa1..1a2e81b 100644 --- a/README.md +++ b/README.md @@ -752,7 +752,7 @@ proxy_protocol_version = v2 You can enable Proxy Protocol support in nginx to expose user's real IP in HTTP header `X-Real-IP`, and then read `X-Real-IP` header in your web service for the real IP. -### Require HTTP Basic auth (password) for web services +### Require HTTP Basic Auth (Password) for Web Services Anyone who can guess your tunnel URL can access your local web server unless you protect it with a password. @@ -772,7 +772,7 @@ http_pwd = abc Visit `http://test.example.com` in the browser and now you are prompted to enter the username and password. -### Custom subdomain names +### Custom Subdomain Names It is convenient to use `subdomain` configure for http and https types when many people share one frps server. @@ -795,7 +795,7 @@ Now you can visit your web service on `test.frps.com`. Note that if `subdomain_host` is not empty, `custom_domains` should not be the subdomain of `subdomain_host`. -### URL routing +### URL Routing frp supports forwarding HTTP requests to different backend web services by url routing. @@ -818,6 +818,49 @@ locations = /news,/about HTTP requests with URL prefix `/news` or `/about` will be forwarded to **web02** and other requests to **web01**. +### TCP Multiplexing + +frp supports receiving TCP sockets directed to different proxies on a single port on frps, similar to `vhost_http_port` and `vhost_https_port`. + +The only supported TCP multiplexing method available at the moment is `httpconnect` - HTTP CONNECT tunnel. + +When setting `tcpmux_httpconnect_port` to anything other than 0 in frps under `[common]`, frps will listen on this port for HTTP CONNECT requests. + +The host of the HTTP CONNECT request will be used to match the proxy in frps. Proxy hosts can be configured in frpc by configuring `custom_domain` and / or `subdomain` under `type = tcpmux` proxies, when `multiplexer = httpconnect`. + +For example: + +```ini +# frps.ini +[common] +bind_port = 7000 +tcpmux_httpconnect_port = 1337 +``` + +```ini +# frpc.ini +[common] +server_addr = x.x.x.x +server_port = 7000 + +[proxy1] +type = tcpmux +multiplexer = httpconnect +custom_domains = test1 + +[proxy2] +type = tcpmux +multiplexer = httpconnect +custom_domains = test2 +``` + +In the above configuration - frps can be contacted on port 1337 with a HTTP CONNECT header such as: + +``` +CONNECT test1 HTTP/1.1\r\n\r\n +``` +and the connection will be routed to `proxy1`. + ### Connecting to frps via HTTP PROXY frpc can connect to frps using HTTP proxy if you set OS environment variable `HTTP_PROXY`, or if `http_proxy` is set in frpc.ini file. diff --git a/client/proxy/proxy.go b/client/proxy/proxy.go index c9ef8dd..c263e1d 100644 --- a/client/proxy/proxy.go +++ b/client/proxy/proxy.go @@ -72,6 +72,11 @@ func NewProxy(ctx context.Context, pxyConf config.ProxyConf, clientCfg config.Cl BaseProxy: &baseProxy, cfg: cfg, } + case *config.TcpMuxProxyConf: + pxy = &TcpMuxProxy{ + BaseProxy: &baseProxy, + cfg: cfg, + } case *config.UdpProxyConf: pxy = &UdpProxy{ BaseProxy: &baseProxy, @@ -141,6 +146,35 @@ func (pxy *TcpProxy) InWorkConn(conn net.Conn, m *msg.StartWorkConn) { conn, []byte(pxy.clientCfg.Token), m) } +// TCP Multiplexer +type TcpMuxProxy struct { + *BaseProxy + + cfg *config.TcpMuxProxyConf + proxyPlugin plugin.Plugin +} + +func (pxy *TcpMuxProxy) Run() (err error) { + if pxy.cfg.Plugin != "" { + pxy.proxyPlugin, err = plugin.Create(pxy.cfg.Plugin, pxy.cfg.PluginParams) + if err != nil { + return + } + } + return +} + +func (pxy *TcpMuxProxy) Close() { + if pxy.proxyPlugin != nil { + pxy.proxyPlugin.Close() + } +} + +func (pxy *TcpMuxProxy) InWorkConn(conn net.Conn, m *msg.StartWorkConn) { + HandleTcpWorkConnection(pxy.ctx, &pxy.cfg.LocalSvrConf, pxy.proxyPlugin, &pxy.cfg.BaseProxyConf, pxy.limiter, + conn, []byte(pxy.clientCfg.Token), m) +} + // HTTP type HttpProxy struct { *BaseProxy diff --git a/cmd/frpc/sub/root.go b/cmd/frpc/sub/root.go index 8ef25e4..8f47986 100644 --- a/cmd/frpc/sub/root.go +++ b/cmd/frpc/sub/root.go @@ -66,6 +66,7 @@ var ( hostHeaderRewrite string role string sk string + multiplexer string serverName string bindAddr string bindPort int diff --git a/cmd/frpc/sub/tcpmux.go b/cmd/frpc/sub/tcpmux.go new file mode 100644 index 0000000..d67d498 --- /dev/null +++ b/cmd/frpc/sub/tcpmux.go @@ -0,0 +1,91 @@ +// Copyright 2020 guylewin, guy@lewin.co.il +// +// 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 sub + +import ( + "fmt" + "os" + "strings" + + "github.com/spf13/cobra" + + "github.com/fatedier/frp/models/config" + "github.com/fatedier/frp/models/consts" +) + +func init() { + tcpMuxCmd.PersistentFlags().StringVarP(&serverAddr, "server_addr", "s", "127.0.0.1:7000", "frp server's address") + tcpMuxCmd.PersistentFlags().StringVarP(&user, "user", "u", "", "user") + tcpMuxCmd.PersistentFlags().StringVarP(&protocol, "protocol", "p", "tcp", "tcp or kcp or websocket") + tcpMuxCmd.PersistentFlags().StringVarP(&token, "token", "t", "", "auth token") + tcpMuxCmd.PersistentFlags().StringVarP(&logLevel, "log_level", "", "info", "log level") + tcpMuxCmd.PersistentFlags().StringVarP(&logFile, "log_file", "", "console", "console or file path") + tcpMuxCmd.PersistentFlags().IntVarP(&logMaxDays, "log_max_days", "", 3, "log file reversed days") + tcpMuxCmd.PersistentFlags().BoolVarP(&disableLogColor, "disable_log_color", "", false, "disable log color in console") + + tcpMuxCmd.PersistentFlags().StringVarP(&proxyName, "proxy_name", "n", "", "proxy name") + tcpMuxCmd.PersistentFlags().StringVarP(&localIp, "local_ip", "i", "127.0.0.1", "local ip") + tcpMuxCmd.PersistentFlags().IntVarP(&localPort, "local_port", "l", 0, "local port") + tcpMuxCmd.PersistentFlags().StringVarP(&customDomains, "custom_domain", "d", "", "custom domain") + tcpMuxCmd.PersistentFlags().StringVarP(&subDomain, "sd", "", "", "sub domain") + tcpMuxCmd.PersistentFlags().StringVarP(&multiplexer, "mux", "", "", "multiplexer") + tcpMuxCmd.PersistentFlags().BoolVarP(&useEncryption, "ue", "", false, "use encryption") + tcpMuxCmd.PersistentFlags().BoolVarP(&useCompression, "uc", "", false, "use compression") + + rootCmd.AddCommand(tcpMuxCmd) +} + +var tcpMuxCmd = &cobra.Command{ + Use: "tcpmux", + Short: "Run frpc with a single tcpmux proxy", + RunE: func(cmd *cobra.Command, args []string) error { + clientCfg, err := parseClientCommonCfg(CfgFileTypeCmd, "") + if err != nil { + fmt.Println(err) + os.Exit(1) + } + + cfg := &config.TcpMuxProxyConf{} + var prefix string + if user != "" { + prefix = user + "." + } + cfg.ProxyName = prefix + proxyName + cfg.ProxyType = consts.TcpMuxProxy + cfg.LocalIp = localIp + cfg.LocalPort = localPort + cfg.CustomDomains = strings.Split(customDomains, ",") + cfg.SubDomain = subDomain + cfg.Multiplexer = multiplexer + cfg.UseEncryption = useEncryption + cfg.UseCompression = useCompression + + err = cfg.CheckForCli() + if err != nil { + fmt.Println(err) + os.Exit(1) + } + + proxyConfs := map[string]config.ProxyConf{ + cfg.ProxyName: cfg, + } + err = startService(clientCfg, proxyConfs, nil, "") + if err != nil { + fmt.Println(err) + os.Exit(1) + } + return nil + }, +} diff --git a/models/config/proxy.go b/models/config/proxy.go index c8c948c..591011f 100644 --- a/models/config/proxy.go +++ b/models/config/proxy.go @@ -34,6 +34,7 @@ var ( func init() { proxyConfTypeMap = make(map[string]reflect.Type) proxyConfTypeMap[consts.TcpProxy] = reflect.TypeOf(TcpProxyConf{}) + proxyConfTypeMap[consts.TcpMuxProxy] = reflect.TypeOf(TcpMuxProxyConf{}) proxyConfTypeMap[consts.UdpProxy] = reflect.TypeOf(UdpProxyConf{}) proxyConfTypeMap[consts.HttpProxy] = reflect.TypeOf(HttpProxyConf{}) proxyConfTypeMap[consts.HttpsProxy] = reflect.TypeOf(HttpsProxyConf{}) @@ -574,6 +575,84 @@ func (cfg *TcpProxyConf) CheckForCli() (err error) { func (cfg *TcpProxyConf) CheckForSvr(serverCfg ServerCommonConf) error { return nil } +// TCP Multiplexer +type TcpMuxProxyConf struct { + BaseProxyConf + DomainConf + + Multiplexer string `json:"multiplexer"` +} + +func (cfg *TcpMuxProxyConf) Compare(cmp ProxyConf) bool { + cmpConf, ok := cmp.(*TcpMuxProxyConf) + if !ok { + return false + } + + if !cfg.BaseProxyConf.compare(&cmpConf.BaseProxyConf) || + !cfg.DomainConf.compare(&cmpConf.DomainConf) || + cfg.Multiplexer != cmpConf.Multiplexer { + return false + } + return true +} + +func (cfg *TcpMuxProxyConf) UnmarshalFromMsg(pMsg *msg.NewProxy) { + cfg.BaseProxyConf.UnmarshalFromMsg(pMsg) + cfg.DomainConf.UnmarshalFromMsg(pMsg) + cfg.Multiplexer = pMsg.Multiplexer +} + +func (cfg *TcpMuxProxyConf) UnmarshalFromIni(prefix string, name string, section ini.Section) (err error) { + if err = cfg.BaseProxyConf.UnmarshalFromIni(prefix, name, section); err != nil { + return + } + if err = cfg.DomainConf.UnmarshalFromIni(prefix, name, section); err != nil { + return + } + + cfg.Multiplexer = section["multiplexer"] + if cfg.Multiplexer != consts.HttpConnectTcpMultiplexer { + return fmt.Errorf("parse conf error: proxy [%s] incorrect multiplexer [%s]", name, cfg.Multiplexer) + } + return +} + +func (cfg *TcpMuxProxyConf) MarshalToMsg(pMsg *msg.NewProxy) { + cfg.BaseProxyConf.MarshalToMsg(pMsg) + cfg.DomainConf.MarshalToMsg(pMsg) + pMsg.Multiplexer = cfg.Multiplexer +} + +func (cfg *TcpMuxProxyConf) CheckForCli() (err error) { + if err = cfg.BaseProxyConf.checkForCli(); err != nil { + return err + } + if err = cfg.DomainConf.checkForCli(); err != nil { + return err + } + if cfg.Multiplexer != consts.HttpConnectTcpMultiplexer { + return fmt.Errorf("parse conf error: incorrect multiplexer [%s]", cfg.Multiplexer) + } + return +} + +func (cfg *TcpMuxProxyConf) CheckForSvr(serverCfg ServerCommonConf) (err error) { + if cfg.Multiplexer != consts.HttpConnectTcpMultiplexer { + return fmt.Errorf("proxy [%s] incorrect multiplexer [%s]", cfg.ProxyName, cfg.Multiplexer) + } + + if cfg.Multiplexer == consts.HttpConnectTcpMultiplexer && serverCfg.TcpMuxHttpConnectPort == 0 { + return fmt.Errorf("proxy [%s] type [tcpmux] with multiplexer [httpconnect] requires tcpmux_httpconnect_port configuration", cfg.ProxyName) + } + + if err = cfg.DomainConf.checkForSvr(serverCfg); err != nil { + err = fmt.Errorf("proxy [%s] domain conf check error: %v", cfg.ProxyName, err) + return + } + return +} + // UDP type UdpProxyConf struct { BaseProxyConf diff --git a/models/config/server_common.go b/models/config/server_common.go index f4f3c41..54703ce 100644 --- a/models/config/server_common.go +++ b/models/config/server_common.go @@ -59,6 +59,12 @@ type ServerCommonConf struct { // requests. By default, this value is 0. VhostHttpsPort int `json:"vhost_https_port"` + // TcpMuxHttpConnectPort specifies the port that the server listens for TCP + // HTTP CONNECT requests. If the value is 0, the server will not multiplex TCP + // requests on one single port. If it's not - it will listen on this value for + // HTTP CONNECT requests. By default, this value is 0. + TcpMuxHttpConnectPort int `json:"tcpmux_httpconnect_port"` + // VhostHttpTimeout specifies the response header timeout for the Vhost // HTTP server, in seconds. By default, this value is 60. VhostHttpTimeout int64 `json:"vhost_http_timeout"` @@ -155,6 +161,7 @@ func GetDefaultServerConf() ServerCommonConf { ProxyBindAddr: "0.0.0.0", VhostHttpPort: 0, VhostHttpsPort: 0, + TcpMuxHttpConnectPort: 0, VhostHttpTimeout: 60, DashboardAddr: "0.0.0.0", DashboardPort: 0, @@ -259,6 +266,17 @@ func UnmarshalServerConfFromIni(content string) (cfg ServerCommonConf, err error cfg.VhostHttpsPort = 0 } + if tmpStr, ok = conf.Get("common", "tcpmux_httpconnect_port"); ok { + if v, err = strconv.ParseInt(tmpStr, 10, 64); err != nil { + err = fmt.Errorf("Parse conf error: invalid tcpmux_httpconnect_port") + return + } else { + cfg.TcpMuxHttpConnectPort = int(v) + } + } else { + cfg.TcpMuxHttpConnectPort = 0 + } + if tmpStr, ok = conf.Get("common", "vhost_http_timeout"); ok { v, errRet := strconv.ParseInt(tmpStr, 10, 64) if errRet != nil || v < 0 { diff --git a/models/consts/consts.go b/models/consts/consts.go index f3c480f..4c1ca4c 100644 --- a/models/consts/consts.go +++ b/models/consts/consts.go @@ -23,14 +23,18 @@ var ( Offline string = "offline" // proxy type - TcpProxy string = "tcp" - UdpProxy string = "udp" - HttpProxy string = "http" - HttpsProxy string = "https" - StcpProxy string = "stcp" - XtcpProxy string = "xtcp" + TcpProxy string = "tcp" + UdpProxy string = "udp" + TcpMuxProxy string = "tcpmux" + HttpProxy string = "http" + HttpsProxy string = "https" + StcpProxy string = "stcp" + XtcpProxy string = "xtcp" // authentication method TokenAuthMethod string = "token" OidcAuthMethod string = "oidc" + + // tcp multiplexer + HttpConnectTcpMultiplexer string = "httpconnect" ) diff --git a/models/msg/msg.go b/models/msg/msg.go index 0acce5b..b08f154 100644 --- a/models/msg/msg.go +++ b/models/msg/msg.go @@ -107,6 +107,9 @@ type NewProxy struct { // stcp Sk string `json:"sk"` + + // tcpmux + Multiplexer string `json:"multiplexer"` } type NewProxyResp struct { diff --git a/server/controller/resource.go b/server/controller/resource.go index 91332b5..5098dfb 100644 --- a/server/controller/resource.go +++ b/server/controller/resource.go @@ -18,6 +18,7 @@ import ( "github.com/fatedier/frp/models/nathole" "github.com/fatedier/frp/server/group" "github.com/fatedier/frp/server/ports" + "github.com/fatedier/frp/utils/tcpmux" "github.com/fatedier/frp/utils/vhost" ) @@ -46,4 +47,7 @@ type ResourceController struct { // Controller for nat hole connections NatHoleController *nathole.NatHoleController + + // TcpMux HTTP CONNECT multiplexer + TcpMuxHttpConnectMuxer *tcpmux.HttpConnectTcpMuxer } diff --git a/server/dashboard_api.go b/server/dashboard_api.go index 30ee9a6..bc91ccf 100644 --- a/server/dashboard_api.go +++ b/server/dashboard_api.go @@ -95,6 +95,12 @@ type TcpOutConf struct { RemotePort int `json:"remote_port"` } +type TcpMuxOutConf struct { + BaseOutConf + config.DomainConf + Multiplexer string `json:"multiplexer"` +} + type UdpOutConf struct { BaseOutConf RemotePort int `json:"remote_port"` @@ -124,6 +130,8 @@ func getConfByType(proxyType string) interface{} { switch proxyType { case consts.TcpProxy: return &TcpOutConf{} + case consts.TcpMuxProxy: + return &TcpMuxOutConf{} case consts.UdpProxy: return &UdpOutConf{} case consts.HttpProxy: diff --git a/server/proxy/proxy.go b/server/proxy/proxy.go index d046c9c..ecd9621 100644 --- a/server/proxy/proxy.go +++ b/server/proxy/proxy.go @@ -177,6 +177,11 @@ func NewProxy(ctx context.Context, runId string, rc *controller.ResourceControll BaseProxy: &basePxy, cfg: cfg, } + case *config.TcpMuxProxyConf: + pxy = &TcpMuxProxy{ + BaseProxy: &basePxy, + cfg: cfg, + } case *config.HttpProxyConf: pxy = &HttpProxy{ BaseProxy: &basePxy, diff --git a/server/proxy/tcpmux.go b/server/proxy/tcpmux.go new file mode 100644 index 0000000..108e68d --- /dev/null +++ b/server/proxy/tcpmux.go @@ -0,0 +1,95 @@ +// Copyright 2020 guylewin, guy@lewin.co.il +// +// 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 proxy + +import ( + "fmt" + "strings" + + "github.com/fatedier/frp/models/config" + "github.com/fatedier/frp/models/consts" + "github.com/fatedier/frp/utils/util" + "github.com/fatedier/frp/utils/vhost" +) + +type TcpMuxProxy struct { + *BaseProxy + cfg *config.TcpMuxProxyConf + + realPort int +} + +func (pxy *TcpMuxProxy) httpConnectListen(domain string, addrs []string) ([]string, error) { + routeConfig := &vhost.VhostRouteConfig{ + Domain: domain, + } + l, err := pxy.rc.TcpMuxHttpConnectMuxer.Listen(pxy.ctx, routeConfig) + if err != nil { + return nil, err + } + pxy.xl.Info("tcpmux httpconnect multiplexer listens for host [%s]", routeConfig.Domain) + pxy.listeners = append(pxy.listeners, l) + return append(addrs, util.CanonicalAddr(routeConfig.Domain, pxy.serverCfg.TcpMuxHttpConnectPort)), nil +} + +func (pxy *TcpMuxProxy) httpConnectRun() (remoteAddr string, err error) { + addrs := make([]string, 0) + for _, domain := range pxy.cfg.CustomDomains { + if domain == "" { + continue + } + + addrs, err = pxy.httpConnectListen(domain, addrs) + if err != nil { + return "", err + } + } + + if pxy.cfg.SubDomain != "" { + addrs, err = pxy.httpConnectListen(pxy.cfg.SubDomain+"."+pxy.serverCfg.SubDomainHost, addrs) + if err != nil { + return "", err + } + } + + pxy.startListenHandler(pxy, HandleUserTcpConnection) + remoteAddr = strings.Join(addrs, ",") + return remoteAddr, err +} + +func (pxy *TcpMuxProxy) Run() (remoteAddr string, err error) { + switch pxy.cfg.Multiplexer { + case consts.HttpConnectTcpMultiplexer: + remoteAddr, err = pxy.httpConnectRun() + default: + err = fmt.Errorf("unknown multiplexer [%s]", pxy.cfg.Multiplexer) + } + + if err != nil { + pxy.Close() + } + return remoteAddr, err +} + +func (pxy *TcpMuxProxy) GetConf() config.ProxyConf { + return pxy.cfg +} + +func (pxy *TcpMuxProxy) Close() { + pxy.BaseProxy.Close() + if pxy.cfg.Group == "" { + pxy.rc.TcpPortManager.Release(pxy.realPort) + } +} diff --git a/server/service.go b/server/service.go index d867289..ec015a1 100644 --- a/server/service.go +++ b/server/service.go @@ -42,6 +42,7 @@ import ( "github.com/fatedier/frp/server/stats" "github.com/fatedier/frp/utils/log" frpNet "github.com/fatedier/frp/utils/net" + "github.com/fatedier/frp/utils/tcpmux" "github.com/fatedier/frp/utils/util" "github.com/fatedier/frp/utils/version" "github.com/fatedier/frp/utils/vhost" @@ -52,7 +53,8 @@ import ( ) const ( - connReadTimeout time.Duration = 10 * time.Second + connReadTimeout time.Duration = 10 * time.Second + vhostReadWriteTimeout time.Duration = 30 * time.Second ) // Server service @@ -212,7 +214,7 @@ func NewService(cfg config.ServerCommonConf) (svr *Service, err error) { } } - svr.rc.VhostHttpsMuxer, err = vhost.NewHttpsMuxer(l, 30*time.Second) + svr.rc.VhostHttpsMuxer, err = vhost.NewHttpsMuxer(l, vhostReadWriteTimeout) if err != nil { err = fmt.Errorf("Create vhost httpsMuxer error, %v", err) return @@ -220,6 +222,23 @@ func NewService(cfg config.ServerCommonConf) (svr *Service, err error) { log.Info("https service listen on %s:%d", cfg.ProxyBindAddr, cfg.VhostHttpsPort) } + // Create tcpmux httpconnect multiplexer. + if cfg.TcpMuxHttpConnectPort > 0 { + var l net.Listener + l, err = net.Listen("tcp", fmt.Sprintf("%s:%d", cfg.ProxyBindAddr, cfg.TcpMuxHttpConnectPort)) + if err != nil { + err = fmt.Errorf("Create server listener error, %v", err) + return + } + + svr.rc.TcpMuxHttpConnectMuxer, err = tcpmux.NewHttpConnectTcpMuxer(l, vhostReadWriteTimeout) + if err != nil { + err = fmt.Errorf("Create vhost tcpMuxer error, %v", err) + return + } + log.Info("tcpmux httpconnect multiplexer listen on %s:%d", cfg.ProxyBindAddr, cfg.TcpMuxHttpConnectPort) + } + // frp tls listener svr.tlsListener = svr.muxer.Listen(1, 1, func(data []byte) bool { return int(data[0]) == frpNet.FRP_TLS_HEAD_BYTE diff --git a/tests/ci/auto_test_frpc.ini b/tests/ci/auto_test_frpc.ini index b8d90bf..fbcf971 100644 --- a/tests/ci/auto_test_frpc.ini +++ b/tests/ci/auto_test_frpc.ini @@ -126,6 +126,13 @@ custom_domains = test6.frp.com host_header_rewrite = test6.frp.com header_X-From-Where = frp +[tcpmuxhttpconnect] +type = tcpmux +multiplexer = httpconnect +local_ip = 127.0.0.1 +local_port = 10701 +custom_domains = tunnel1 + [wildcard_http] type = http local_ip = 127.0.0.1 diff --git a/tests/ci/auto_test_frps.ini b/tests/ci/auto_test_frps.ini index 8948c98..25f7d13 100644 --- a/tests/ci/auto_test_frps.ini +++ b/tests/ci/auto_test_frps.ini @@ -2,6 +2,7 @@ bind_addr = 0.0.0.0 bind_port = 10700 vhost_http_port = 10804 +tcpmux_httpconnect_port = 10806 log_level = trace token = 123456 allow_ports = 10000-20000,20002,30000-50000 diff --git a/tests/ci/normal_test.go b/tests/ci/normal_test.go index 4f976c8..572fec0 100644 --- a/tests/ci/normal_test.go +++ b/tests/ci/normal_test.go @@ -212,6 +212,17 @@ func TestHttp(t *testing.T) { } } +func TestTcpMux(t *testing.T) { + assert := assert.New(t) + + conn, err := gnet.DialTcpByProxy(fmt.Sprintf("http://%s:%d", "127.0.0.1", consts.TEST_TCP_MUX_FRP_PORT), "tunnel1") + if assert.NoError(err) { + res, err := util.SendTcpMsgByConn(conn, consts.TEST_TCP_ECHO_STR) + assert.NoError(err) + assert.Equal(consts.TEST_TCP_ECHO_STR, res) + } +} + func TestWebSocket(t *testing.T) { assert := assert.New(t) diff --git a/tests/consts/consts.go b/tests/consts/consts.go index 4e1c1a0..5fc8857 100644 --- a/tests/consts/consts.go +++ b/tests/consts/consts.go @@ -40,6 +40,8 @@ var ( TEST_HTTP_FOO_STR string = "http foo string: " + TEST_STR TEST_HTTP_BAR_STR string = "http bar string: " + TEST_STR + TEST_TCP_MUX_FRP_PORT int = 10806 + TEST_STCP_FRP_PORT int = 10805 TEST_STCP_EC_FRP_PORT int = 10905 TEST_STCP_ECHO_STR string = "stcp type:" + TEST_STR diff --git a/utils/tcpmux/httpconnect.go b/utils/tcpmux/httpconnect.go new file mode 100644 index 0000000..af0a39f --- /dev/null +++ b/utils/tcpmux/httpconnect.go @@ -0,0 +1,68 @@ +// Copyright 2020 guylewin, guy@lewin.co.il +// +// 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 tcpmux + +import ( + "bufio" + "fmt" + "io" + "net" + "net/http" + "time" + + "github.com/fatedier/frp/utils/util" + "github.com/fatedier/frp/utils/vhost" +) + +type HttpConnectTcpMuxer struct { + *vhost.VhostMuxer +} + +func NewHttpConnectTcpMuxer(listener net.Listener, timeout time.Duration) (*HttpConnectTcpMuxer, error) { + mux, err := vhost.NewVhostMuxer(listener, getHostFromHttpConnect, nil, sendHttpOk, nil, timeout) + return &HttpConnectTcpMuxer{mux}, err +} + +func readHttpConnectRequest(rd io.Reader) (host string, err error) { + bufioReader := bufio.NewReader(rd) + + req, err := http.ReadRequest(bufioReader) + if err != nil { + return + } + + if req.Method != "CONNECT" { + err = fmt.Errorf("connections to tcp vhost must be of method CONNECT") + return + } + + host = util.GetHostFromAddr(req.Host) + return +} + +func sendHttpOk(c net.Conn) error { + return util.OkResponse().Write(c) +} + +func getHostFromHttpConnect(c net.Conn) (_ net.Conn, _ map[string]string, err error) { + reqInfoMap := make(map[string]string, 0) + host, err := readHttpConnectRequest(c) + if err != nil { + return nil, reqInfoMap, err + } + reqInfoMap["Host"] = host + reqInfoMap["Scheme"] = "tcp" + return c, reqInfoMap, nil +} diff --git a/utils/util/http.go b/utils/util/http.go new file mode 100644 index 0000000..bbd3f87 --- /dev/null +++ b/utils/util/http.go @@ -0,0 +1,44 @@ +// Copyright 2020 guylewin, guy@lewin.co.il +// +// 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 util + +import ( + "net/http" + "strings" +) + +func OkResponse() *http.Response { + header := make(http.Header) + + res := &http.Response{ + Status: "OK", + StatusCode: 200, + Proto: "HTTP/1.1", + ProtoMajor: 1, + ProtoMinor: 1, + Header: header, + } + return res +} + +func GetHostFromAddr(addr string) (host string) { + strs := strings.Split(addr, ":") + if len(strs) > 1 { + host = strs[0] + } else { + host = addr + } + return +} diff --git a/utils/vhost/http.go b/utils/vhost/http.go index 7c2d7a3..f63920b 100644 --- a/utils/vhost/http.go +++ b/utils/vhost/http.go @@ -26,6 +26,7 @@ import ( "time" frpLog "github.com/fatedier/frp/utils/log" + "github.com/fatedier/frp/utils/util" "github.com/fatedier/golib/pool" ) @@ -34,16 +35,6 @@ var ( ErrNoDomain = errors.New("no such domain") ) -func getHostFromAddr(addr string) (host string) { - strs := strings.Split(addr, ":") - if len(strs) > 1 { - host = strs[0] - } else { - host = addr - } - return -} - type HttpReverseProxyOptions struct { ResponseHeaderTimeoutS int64 } @@ -67,7 +58,7 @@ func NewHttpReverseProxy(option HttpReverseProxyOptions, vhostRouter *VhostRoute Director: func(req *http.Request) { req.URL.Scheme = "http" url := req.Context().Value("url").(string) - oldHost := getHostFromAddr(req.Context().Value("host").(string)) + oldHost := util.GetHostFromAddr(req.Context().Value("host").(string)) host := rp.GetRealHost(oldHost, url) if host != "" { req.Host = host @@ -84,7 +75,7 @@ func NewHttpReverseProxy(option HttpReverseProxyOptions, vhostRouter *VhostRoute DisableKeepAlives: true, DialContext: func(ctx context.Context, network, addr string) (net.Conn, error) { url := ctx.Value("url").(string) - host := getHostFromAddr(ctx.Value("host").(string)) + host := util.GetHostFromAddr(ctx.Value("host").(string)) remote := ctx.Value("remote").(string) return rp.CreateConnection(host, url, remote) }, @@ -187,7 +178,7 @@ func (rp *HttpReverseProxy) getVhost(domain string, location string) (vr *VhostR } func (rp *HttpReverseProxy) ServeHTTP(rw http.ResponseWriter, req *http.Request) { - domain := getHostFromAddr(req.Host) + domain := util.GetHostFromAddr(req.Host) location := req.URL.Path user, passwd, _ := req.BasicAuth() if !rp.CheckAuth(domain, location, user, passwd) { diff --git a/utils/vhost/https.go b/utils/vhost/https.go index 5317701..41bd01c 100644 --- a/utils/vhost/https.go +++ b/utils/vhost/https.go @@ -48,7 +48,7 @@ type HttpsMuxer struct { } func NewHttpsMuxer(listener net.Listener, timeout time.Duration) (*HttpsMuxer, error) { - mux, err := NewVhostMuxer(listener, GetHttpsHostname, nil, nil, timeout) + mux, err := NewVhostMuxer(listener, GetHttpsHostname, nil, nil, nil, timeout) return &HttpsMuxer{mux}, err } diff --git a/utils/vhost/vhost.go b/utils/vhost/vhost.go index 57f8239..ad322be 100644 --- a/utils/vhost/vhost.go +++ b/utils/vhost/vhost.go @@ -29,22 +29,25 @@ import ( type muxFunc func(net.Conn) (net.Conn, map[string]string, error) type httpAuthFunc func(net.Conn, string, string, string) (bool, error) type hostRewriteFunc func(net.Conn, string) (net.Conn, error) +type successFunc func(net.Conn) error type VhostMuxer struct { listener net.Listener timeout time.Duration vhostFunc muxFunc authFunc httpAuthFunc + successFunc successFunc rewriteFunc hostRewriteFunc registryRouter *VhostRouters } -func NewVhostMuxer(listener net.Listener, vhostFunc muxFunc, authFunc httpAuthFunc, rewriteFunc hostRewriteFunc, timeout time.Duration) (mux *VhostMuxer, err error) { +func NewVhostMuxer(listener net.Listener, vhostFunc muxFunc, authFunc httpAuthFunc, successFunc successFunc, rewriteFunc hostRewriteFunc, timeout time.Duration) (mux *VhostMuxer, err error) { mux = &VhostMuxer{ listener: listener, timeout: timeout, vhostFunc: vhostFunc, authFunc: authFunc, + successFunc: successFunc, rewriteFunc: rewriteFunc, registryRouter: NewVhostRouters(), } @@ -149,7 +152,15 @@ func (v *VhostMuxer) handle(c net.Conn) { c.Close() return } + xl := xlog.FromContextSafe(l.ctx) + if v.successFunc != nil { + if err := v.successFunc(c); err != nil { + xl.Info("success func failure on vhost connection: %v", err) + c.Close() + return + } + } // if authFunc is exist and userName/password is set // then verify user access