Skip to content

Commit

Permalink
tcp multiplexing over http connect tunnel
Browse files Browse the repository at this point in the history
  • Loading branch information
fatedier authored Mar 5, 2020
1 parent 0b9124d commit 1db091b
Show file tree
Hide file tree
Showing 22 changed files with 565 additions and 26 deletions.
49 changes: 46 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Expand All @@ -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.

Expand All @@ -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.

Expand All @@ -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.
Expand Down
34 changes: 34 additions & 0 deletions client/proxy/proxy.go
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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
Expand Down
1 change: 1 addition & 0 deletions cmd/frpc/sub/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,7 @@ var (
hostHeaderRewrite string
role string
sk string
multiplexer string
serverName string
bindAddr string
bindPort int
Expand Down
91 changes: 91 additions & 0 deletions cmd/frpc/sub/tcpmux.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
// Copyright 2020 guylewin, [email protected]
//
// 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 changes: 79 additions & 0 deletions models/config/proxy.go
Original file line number Diff line number Diff line change
Expand Up @@ -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{})
Expand Down Expand Up @@ -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
Expand Down
18 changes: 18 additions & 0 deletions models/config/server_common.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"`
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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 {
Expand Down
16 changes: 10 additions & 6 deletions models/consts/consts.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 changes: 3 additions & 0 deletions models/msg/msg.go
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,9 @@ type NewProxy struct {

// stcp
Sk string `json:"sk"`

// tcpmux
Multiplexer string `json:"multiplexer"`
}

type NewProxyResp struct {
Expand Down
Loading

0 comments on commit 1db091b

Please sign in to comment.