Explorar el Código

tcp multiplexing over http connect tunnel

fatedier hace 5 años
padre
commit
1db091b381

+ 46 - 3
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.

+ 34 - 0
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

+ 1 - 0
cmd/frpc/sub/root.go

@@ -66,6 +66,7 @@ var (
 	hostHeaderRewrite string
 	role              string
 	sk                string
+	multiplexer       string
 	serverName        string
 	bindAddr          string
 	bindPort          int

+ 91 - 0
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
+	},
+}

+ 79 - 0
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

+ 18 - 0
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 {

+ 10 - 6
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"
 )

+ 3 - 0
models/msg/msg.go

@@ -107,6 +107,9 @@ type NewProxy struct {
 
 	// stcp
 	Sk string `json:"sk"`
+
+	// tcpmux
+	Multiplexer string `json:"multiplexer"`
 }
 
 type NewProxyResp struct {

+ 4 - 0
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
 }

+ 8 - 0
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:

+ 5 - 0
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,

+ 95 - 0
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)
+	}
+}

+ 21 - 2
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

+ 7 - 0
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

+ 1 - 0
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

+ 11 - 0
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)
 

+ 2 - 0
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

+ 68 - 0
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
+}

+ 44 - 0
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
+}

+ 4 - 13
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) {

+ 1 - 1
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
 }
 

+ 12 - 1
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