tcp multiplexing over http connect tunnel

This commit is contained in:
Guy Lewin 2020-02-16 17:49:51 +02:00
parent 83d80857fd
commit 504f565d3f
9 changed files with 188 additions and 26 deletions

View File

@ -530,6 +530,7 @@ func (cfg *HealthCheckConf) checkForCli() error {
type TcpProxyConf struct {
BaseProxyConf
BindInfoConf
DomainConf
}
func (cfg *TcpProxyConf) Compare(cmp ProxyConf) bool {
@ -539,7 +540,8 @@ func (cfg *TcpProxyConf) Compare(cmp ProxyConf) bool {
}
if !cfg.BaseProxyConf.compare(&cmpConf.BaseProxyConf) ||
!cfg.BindInfoConf.compare(&cmpConf.BindInfoConf) {
!cfg.BindInfoConf.compare(&cmpConf.BindInfoConf) ||
!cfg.DomainConf.compare(&cmpConf.DomainConf) {
return false
}
return true
@ -548,6 +550,7 @@ func (cfg *TcpProxyConf) Compare(cmp ProxyConf) bool {
func (cfg *TcpProxyConf) UnmarshalFromMsg(pMsg *msg.NewProxy) {
cfg.BaseProxyConf.UnmarshalFromMsg(pMsg)
cfg.BindInfoConf.UnmarshalFromMsg(pMsg)
cfg.DomainConf.UnmarshalFromMsg(pMsg)
}
func (cfg *TcpProxyConf) UnmarshalFromIni(prefix string, name string, section ini.Section) (err error) {
@ -557,12 +560,16 @@ func (cfg *TcpProxyConf) UnmarshalFromIni(prefix string, name string, section in
if err = cfg.BindInfoConf.UnmarshalFromIni(prefix, name, section); err != nil {
return
}
if err = cfg.DomainConf.UnmarshalFromIni(prefix, name, section); err != nil {
return
}
return
}
func (cfg *TcpProxyConf) MarshalToMsg(pMsg *msg.NewProxy) {
cfg.BaseProxyConf.MarshalToMsg(pMsg)
cfg.BindInfoConf.MarshalToMsg(pMsg)
cfg.DomainConf.MarshalToMsg(pMsg)
}
func (cfg *TcpProxyConf) CheckForCli() (err error) {
@ -572,7 +579,12 @@ func (cfg *TcpProxyConf) CheckForCli() (err error) {
return
}
func (cfg *TcpProxyConf) CheckForSvr(serverCfg ServerCommonConf) error { return nil }
func (cfg *TcpProxyConf) CheckForSvr(serverCfg ServerCommonConf) (err error) {
if len(cfg.CustomDomains) == 0 && cfg.SubDomain == "" && serverCfg.VhostTcpPort != 0 {
return fmt.Errorf("type [tcp] not support when vhost_http_port is on but no custom domain or subdomain configured")
}
return
}
// UDP
type UdpProxyConf struct {

View File

@ -57,6 +57,12 @@ type ServerCommonConf struct {
// requests. By default, this value is 0.
VhostHttpsPort int `json:"vhost_https_port"`
// VhostTcpPort specifies the port that the server listens for TCP Vhost
// 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.
VhostTcpPort int `json:"vhost_tcp_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"`
@ -156,6 +162,7 @@ func GetDefaultServerConf() ServerCommonConf {
ProxyBindAddr: "0.0.0.0",
VhostHttpPort: 0,
VhostHttpsPort: 0,
VhostTcpPort: 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", "vhost_tcp_port"); ok {
if v, err = strconv.ParseInt(tmpStr, 10, 64); err != nil {
err = fmt.Errorf("Parse conf error: invalid vhost_tcp_port")
return
} else {
cfg.VhostTcpPort = int(v)
}
} else {
cfg.VhostTcpPort = 0
}
if tmpStr, ok = conf.Get("common", "vhost_http_timeout"); ok {
v, errRet := strconv.ParseInt(tmpStr, 10, 64)
if errRet != nil || v < 0 {

View File

@ -44,6 +44,9 @@ type ResourceController struct {
// For https proxies, route requests to different clients by hostname and other information
VhostHttpsMuxer *vhost.HttpsMuxer
// For tcp proxies, route requests to different proxies based on HTTP CONNECT header
VhostTcpMuxer *vhost.TcpMuxer
// Controller for nat hole connections
NatHoleController *nathole.NatHoleController
}

View File

@ -19,6 +19,7 @@ import (
"net"
"github.com/fatedier/frp/models/config"
"github.com/fatedier/frp/utils/vhost"
)
type TcpProxy struct {
@ -45,22 +46,52 @@ func (pxy *TcpProxy) Run() (remoteAddr string, err error) {
pxy.listeners = append(pxy.listeners, l)
xl.Info("tcp proxy listen port [%d] in group [%s]", pxy.cfg.RemotePort, pxy.cfg.Group)
} else {
pxy.realPort, err = pxy.rc.TcpPortManager.Acquire(pxy.name, pxy.cfg.RemotePort)
if err != nil {
return
}
defer func() {
if err != nil {
pxy.rc.TcpPortManager.Release(pxy.realPort)
if pxy.serverCfg.VhostTcpPort > 0 {
pxy.realPort = pxy.serverCfg.VhostTcpPort
routeConfig := &vhost.VhostRouteConfig{}
for _, domain := range pxy.cfg.CustomDomains {
if domain == "" {
continue
}
routeConfig.Domain = domain
l, errRet := pxy.rc.VhostTcpMuxer.Listen(pxy.ctx, routeConfig)
if errRet != nil {
err = errRet
return
}
xl.Info("http tunnel server (tcp proxy) listen for host [%s]", routeConfig.Domain)
pxy.listeners = append(pxy.listeners, l)
}
}()
listener, errRet := net.Listen("tcp", fmt.Sprintf("%s:%d", pxy.serverCfg.ProxyBindAddr, pxy.realPort))
if errRet != nil {
err = errRet
return
if pxy.cfg.SubDomain != "" {
routeConfig.Domain = pxy.cfg.SubDomain + "." + pxy.serverCfg.SubDomainHost
l, errRet := pxy.rc.VhostTcpMuxer.Listen(pxy.ctx, routeConfig)
if errRet != nil {
err = errRet
return
}
xl.Info("http tunnel server (tcp proxy) listen for host [%s]", routeConfig.Domain)
pxy.listeners = append(pxy.listeners, l)
}
} else {
pxy.realPort, err = pxy.rc.TcpPortManager.Acquire(pxy.name, pxy.cfg.RemotePort)
if err != nil {
return
}
defer func() {
if err != nil {
pxy.rc.TcpPortManager.Release(pxy.realPort)
}
}()
listener, errRet := net.Listen("tcp", fmt.Sprintf("%s:%d", pxy.serverCfg.ProxyBindAddr, pxy.realPort))
if errRet != nil {
err = errRet
return
}
pxy.listeners = append(pxy.listeners, listener)
xl.Info("tcp proxy listen port [%d]", pxy.cfg.RemotePort)
}
pxy.listeners = append(pxy.listeners, listener)
xl.Info("tcp proxy listen port [%d]", pxy.cfg.RemotePort)
}
pxy.cfg.RemotePort = pxy.realPort

View File

@ -215,6 +215,23 @@ func NewService(cfg config.ServerCommonConf) (svr *Service, err error) {
log.Info("https service listen on %s:%d", cfg.ProxyBindAddr, cfg.VhostHttpsPort)
}
// Create tcp vhost muxer.
if cfg.VhostTcpPort > 0 {
var l net.Listener
l, err = net.Listen("tcp", fmt.Sprintf("%s:%d", cfg.ProxyBindAddr, cfg.VhostTcpPort))
if err != nil {
err = fmt.Errorf("Create server listener error, %v", err)
return
}
svr.rc.VhostTcpMuxer, err = vhost.NewTcpMuxer(l, 30*time.Second)
if err != nil {
err = fmt.Errorf("Create vhost tcpMuxer error, %v", err)
return
}
log.Info("tcp service listen on %s:%d", cfg.ProxyBindAddr, cfg.VhostTcpPort)
}
// frp tls listener
svr.tlsListener = svr.muxer.Listen(1, 1, func(data []byte) bool {
return int(data[0]) == frpNet.FRP_TLS_HEAD_BYTE

View File

@ -34,16 +34,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
}

View File

@ -98,3 +98,17 @@ func noAuthResponse() *http.Response {
}
return res
}
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
}

67
utils/vhost/tcp.go Normal file
View File

@ -0,0 +1,67 @@
// Copyright 2019 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 vhost
import (
"bufio"
"fmt"
"io"
"net"
"net/http"
"time"
)
type TcpMuxer struct {
*VhostMuxer
}
func NewTcpMuxer(listener net.Listener, timeout time.Duration) (*TcpMuxer, error) {
mux, err := NewVhostMuxer(listener, getTcpServiceName, nil, sendHttpOk, timeout)
return &TcpMuxer{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 = getHostFromAddr(req.Host)
return
}
func sendHttpOk(c net.Conn, _ string) (_ net.Conn, err error) {
okResp := okResponse()
err = okResp.Write(c)
return c, err
}
func getTcpServiceName(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
}

View File

@ -225,3 +225,13 @@ func (l *Listener) Name() string {
func (l *Listener) Addr() net.Addr {
return (*net.TCPAddr)(nil)
}
func getHostFromAddr(addr string) (host string) {
strs := strings.Split(addr, ":")
if len(strs) > 1 {
host = strs[0]
} else {
host = addr
}
return
}