tcp multiplexing over http connect tunnel

This commit is contained in:
fatedier 2020-03-05 21:47:49 +08:00 committed by GitHub
parent 0b9124d4fd
commit 1db091b381
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
22 changed files with 565 additions and 26 deletions

View File

@ -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. 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. 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. 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. 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`. 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. 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**. 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 ### 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. 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.

View File

@ -72,6 +72,11 @@ func NewProxy(ctx context.Context, pxyConf config.ProxyConf, clientCfg config.Cl
BaseProxy: &baseProxy, BaseProxy: &baseProxy,
cfg: cfg, cfg: cfg,
} }
case *config.TcpMuxProxyConf:
pxy = &TcpMuxProxy{
BaseProxy: &baseProxy,
cfg: cfg,
}
case *config.UdpProxyConf: case *config.UdpProxyConf:
pxy = &UdpProxy{ pxy = &UdpProxy{
BaseProxy: &baseProxy, BaseProxy: &baseProxy,
@ -141,6 +146,35 @@ func (pxy *TcpProxy) InWorkConn(conn net.Conn, m *msg.StartWorkConn) {
conn, []byte(pxy.clientCfg.Token), m) 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 // HTTP
type HttpProxy struct { type HttpProxy struct {
*BaseProxy *BaseProxy

View File

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

91
cmd/frpc/sub/tcpmux.go Normal file
View File

@ -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
},
}

View File

@ -34,6 +34,7 @@ var (
func init() { func init() {
proxyConfTypeMap = make(map[string]reflect.Type) proxyConfTypeMap = make(map[string]reflect.Type)
proxyConfTypeMap[consts.TcpProxy] = reflect.TypeOf(TcpProxyConf{}) proxyConfTypeMap[consts.TcpProxy] = reflect.TypeOf(TcpProxyConf{})
proxyConfTypeMap[consts.TcpMuxProxy] = reflect.TypeOf(TcpMuxProxyConf{})
proxyConfTypeMap[consts.UdpProxy] = reflect.TypeOf(UdpProxyConf{}) proxyConfTypeMap[consts.UdpProxy] = reflect.TypeOf(UdpProxyConf{})
proxyConfTypeMap[consts.HttpProxy] = reflect.TypeOf(HttpProxyConf{}) proxyConfTypeMap[consts.HttpProxy] = reflect.TypeOf(HttpProxyConf{})
proxyConfTypeMap[consts.HttpsProxy] = reflect.TypeOf(HttpsProxyConf{}) 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 } 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 // UDP
type UdpProxyConf struct { type UdpProxyConf struct {
BaseProxyConf BaseProxyConf

View File

@ -59,6 +59,12 @@ type ServerCommonConf struct {
// requests. By default, this value is 0. // requests. By default, this value is 0.
VhostHttpsPort int `json:"vhost_https_port"` 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 // VhostHttpTimeout specifies the response header timeout for the Vhost
// HTTP server, in seconds. By default, this value is 60. // HTTP server, in seconds. By default, this value is 60.
VhostHttpTimeout int64 `json:"vhost_http_timeout"` VhostHttpTimeout int64 `json:"vhost_http_timeout"`
@ -155,6 +161,7 @@ func GetDefaultServerConf() ServerCommonConf {
ProxyBindAddr: "0.0.0.0", ProxyBindAddr: "0.0.0.0",
VhostHttpPort: 0, VhostHttpPort: 0,
VhostHttpsPort: 0, VhostHttpsPort: 0,
TcpMuxHttpConnectPort: 0,
VhostHttpTimeout: 60, VhostHttpTimeout: 60,
DashboardAddr: "0.0.0.0", DashboardAddr: "0.0.0.0",
DashboardPort: 0, DashboardPort: 0,
@ -259,6 +266,17 @@ func UnmarshalServerConfFromIni(content string) (cfg ServerCommonConf, err error
cfg.VhostHttpsPort = 0 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 { if tmpStr, ok = conf.Get("common", "vhost_http_timeout"); ok {
v, errRet := strconv.ParseInt(tmpStr, 10, 64) v, errRet := strconv.ParseInt(tmpStr, 10, 64)
if errRet != nil || v < 0 { if errRet != nil || v < 0 {

View File

@ -25,6 +25,7 @@ var (
// proxy type // proxy type
TcpProxy string = "tcp" TcpProxy string = "tcp"
UdpProxy string = "udp" UdpProxy string = "udp"
TcpMuxProxy string = "tcpmux"
HttpProxy string = "http" HttpProxy string = "http"
HttpsProxy string = "https" HttpsProxy string = "https"
StcpProxy string = "stcp" StcpProxy string = "stcp"
@ -33,4 +34,7 @@ var (
// authentication method // authentication method
TokenAuthMethod string = "token" TokenAuthMethod string = "token"
OidcAuthMethod string = "oidc" OidcAuthMethod string = "oidc"
// tcp multiplexer
HttpConnectTcpMultiplexer string = "httpconnect"
) )

View File

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

View File

@ -18,6 +18,7 @@ import (
"github.com/fatedier/frp/models/nathole" "github.com/fatedier/frp/models/nathole"
"github.com/fatedier/frp/server/group" "github.com/fatedier/frp/server/group"
"github.com/fatedier/frp/server/ports" "github.com/fatedier/frp/server/ports"
"github.com/fatedier/frp/utils/tcpmux"
"github.com/fatedier/frp/utils/vhost" "github.com/fatedier/frp/utils/vhost"
) )
@ -46,4 +47,7 @@ type ResourceController struct {
// Controller for nat hole connections // Controller for nat hole connections
NatHoleController *nathole.NatHoleController NatHoleController *nathole.NatHoleController
// TcpMux HTTP CONNECT multiplexer
TcpMuxHttpConnectMuxer *tcpmux.HttpConnectTcpMuxer
} }

View File

@ -95,6 +95,12 @@ type TcpOutConf struct {
RemotePort int `json:"remote_port"` RemotePort int `json:"remote_port"`
} }
type TcpMuxOutConf struct {
BaseOutConf
config.DomainConf
Multiplexer string `json:"multiplexer"`
}
type UdpOutConf struct { type UdpOutConf struct {
BaseOutConf BaseOutConf
RemotePort int `json:"remote_port"` RemotePort int `json:"remote_port"`
@ -124,6 +130,8 @@ func getConfByType(proxyType string) interface{} {
switch proxyType { switch proxyType {
case consts.TcpProxy: case consts.TcpProxy:
return &TcpOutConf{} return &TcpOutConf{}
case consts.TcpMuxProxy:
return &TcpMuxOutConf{}
case consts.UdpProxy: case consts.UdpProxy:
return &UdpOutConf{} return &UdpOutConf{}
case consts.HttpProxy: case consts.HttpProxy:

View File

@ -177,6 +177,11 @@ func NewProxy(ctx context.Context, runId string, rc *controller.ResourceControll
BaseProxy: &basePxy, BaseProxy: &basePxy,
cfg: cfg, cfg: cfg,
} }
case *config.TcpMuxProxyConf:
pxy = &TcpMuxProxy{
BaseProxy: &basePxy,
cfg: cfg,
}
case *config.HttpProxyConf: case *config.HttpProxyConf:
pxy = &HttpProxy{ pxy = &HttpProxy{
BaseProxy: &basePxy, BaseProxy: &basePxy,

95
server/proxy/tcpmux.go Normal file
View File

@ -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)
}
}

View File

@ -42,6 +42,7 @@ import (
"github.com/fatedier/frp/server/stats" "github.com/fatedier/frp/server/stats"
"github.com/fatedier/frp/utils/log" "github.com/fatedier/frp/utils/log"
frpNet "github.com/fatedier/frp/utils/net" frpNet "github.com/fatedier/frp/utils/net"
"github.com/fatedier/frp/utils/tcpmux"
"github.com/fatedier/frp/utils/util" "github.com/fatedier/frp/utils/util"
"github.com/fatedier/frp/utils/version" "github.com/fatedier/frp/utils/version"
"github.com/fatedier/frp/utils/vhost" "github.com/fatedier/frp/utils/vhost"
@ -53,6 +54,7 @@ import (
const ( const (
connReadTimeout time.Duration = 10 * time.Second connReadTimeout time.Duration = 10 * time.Second
vhostReadWriteTimeout time.Duration = 30 * time.Second
) )
// Server service // 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 { if err != nil {
err = fmt.Errorf("Create vhost httpsMuxer error, %v", err) err = fmt.Errorf("Create vhost httpsMuxer error, %v", err)
return 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) 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 // frp tls listener
svr.tlsListener = svr.muxer.Listen(1, 1, func(data []byte) bool { svr.tlsListener = svr.muxer.Listen(1, 1, func(data []byte) bool {
return int(data[0]) == frpNet.FRP_TLS_HEAD_BYTE return int(data[0]) == frpNet.FRP_TLS_HEAD_BYTE

View File

@ -126,6 +126,13 @@ custom_domains = test6.frp.com
host_header_rewrite = test6.frp.com host_header_rewrite = test6.frp.com
header_X-From-Where = frp header_X-From-Where = frp
[tcpmuxhttpconnect]
type = tcpmux
multiplexer = httpconnect
local_ip = 127.0.0.1
local_port = 10701
custom_domains = tunnel1
[wildcard_http] [wildcard_http]
type = http type = http
local_ip = 127.0.0.1 local_ip = 127.0.0.1

View File

@ -2,6 +2,7 @@
bind_addr = 0.0.0.0 bind_addr = 0.0.0.0
bind_port = 10700 bind_port = 10700
vhost_http_port = 10804 vhost_http_port = 10804
tcpmux_httpconnect_port = 10806
log_level = trace log_level = trace
token = 123456 token = 123456
allow_ports = 10000-20000,20002,30000-50000 allow_ports = 10000-20000,20002,30000-50000

View File

@ -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) { func TestWebSocket(t *testing.T) {
assert := assert.New(t) assert := assert.New(t)

View File

@ -40,6 +40,8 @@ var (
TEST_HTTP_FOO_STR string = "http foo string: " + TEST_STR TEST_HTTP_FOO_STR string = "http foo string: " + TEST_STR
TEST_HTTP_BAR_STR string = "http bar 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_FRP_PORT int = 10805
TEST_STCP_EC_FRP_PORT int = 10905 TEST_STCP_EC_FRP_PORT int = 10905
TEST_STCP_ECHO_STR string = "stcp type:" + TEST_STR TEST_STCP_ECHO_STR string = "stcp type:" + TEST_STR

View File

@ -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
utils/util/http.go Normal file
View File

@ -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
}

View File

@ -26,6 +26,7 @@ import (
"time" "time"
frpLog "github.com/fatedier/frp/utils/log" frpLog "github.com/fatedier/frp/utils/log"
"github.com/fatedier/frp/utils/util"
"github.com/fatedier/golib/pool" "github.com/fatedier/golib/pool"
) )
@ -34,16 +35,6 @@ var (
ErrNoDomain = errors.New("no such domain") 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 { type HttpReverseProxyOptions struct {
ResponseHeaderTimeoutS int64 ResponseHeaderTimeoutS int64
} }
@ -67,7 +58,7 @@ func NewHttpReverseProxy(option HttpReverseProxyOptions, vhostRouter *VhostRoute
Director: func(req *http.Request) { Director: func(req *http.Request) {
req.URL.Scheme = "http" req.URL.Scheme = "http"
url := req.Context().Value("url").(string) 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) host := rp.GetRealHost(oldHost, url)
if host != "" { if host != "" {
req.Host = host req.Host = host
@ -84,7 +75,7 @@ func NewHttpReverseProxy(option HttpReverseProxyOptions, vhostRouter *VhostRoute
DisableKeepAlives: true, DisableKeepAlives: true,
DialContext: func(ctx context.Context, network, addr string) (net.Conn, error) { DialContext: func(ctx context.Context, network, addr string) (net.Conn, error) {
url := ctx.Value("url").(string) url := ctx.Value("url").(string)
host := getHostFromAddr(ctx.Value("host").(string)) host := util.GetHostFromAddr(ctx.Value("host").(string))
remote := ctx.Value("remote").(string) remote := ctx.Value("remote").(string)
return rp.CreateConnection(host, url, remote) 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) { func (rp *HttpReverseProxy) ServeHTTP(rw http.ResponseWriter, req *http.Request) {
domain := getHostFromAddr(req.Host) domain := util.GetHostFromAddr(req.Host)
location := req.URL.Path location := req.URL.Path
user, passwd, _ := req.BasicAuth() user, passwd, _ := req.BasicAuth()
if !rp.CheckAuth(domain, location, user, passwd) { if !rp.CheckAuth(domain, location, user, passwd) {

View File

@ -48,7 +48,7 @@ type HttpsMuxer struct {
} }
func NewHttpsMuxer(listener net.Listener, timeout time.Duration) (*HttpsMuxer, error) { 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 return &HttpsMuxer{mux}, err
} }

View File

@ -29,22 +29,25 @@ import (
type muxFunc func(net.Conn) (net.Conn, map[string]string, error) type muxFunc func(net.Conn) (net.Conn, map[string]string, error)
type httpAuthFunc func(net.Conn, string, string, string) (bool, error) type httpAuthFunc func(net.Conn, string, string, string) (bool, error)
type hostRewriteFunc func(net.Conn, string) (net.Conn, error) type hostRewriteFunc func(net.Conn, string) (net.Conn, error)
type successFunc func(net.Conn) error
type VhostMuxer struct { type VhostMuxer struct {
listener net.Listener listener net.Listener
timeout time.Duration timeout time.Duration
vhostFunc muxFunc vhostFunc muxFunc
authFunc httpAuthFunc authFunc httpAuthFunc
successFunc successFunc
rewriteFunc hostRewriteFunc rewriteFunc hostRewriteFunc
registryRouter *VhostRouters 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{ mux = &VhostMuxer{
listener: listener, listener: listener,
timeout: timeout, timeout: timeout,
vhostFunc: vhostFunc, vhostFunc: vhostFunc,
authFunc: authFunc, authFunc: authFunc,
successFunc: successFunc,
rewriteFunc: rewriteFunc, rewriteFunc: rewriteFunc,
registryRouter: NewVhostRouters(), registryRouter: NewVhostRouters(),
} }
@ -149,7 +152,15 @@ func (v *VhostMuxer) handle(c net.Conn) {
c.Close() c.Close()
return return
} }
xl := xlog.FromContextSafe(l.ctx) 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 // if authFunc is exist and userName/password is set
// then verify user access // then verify user access