mirror of https://github.com/fatedier/frp
feat: add HTTPS load balancing support
- Add HTTPS group controller for load balancing across multiple HTTPS backends - Implement HTTPS reverse proxy with SNI-based routing - Enhance HTTPS proxy with group registration and load balancing - Add configuration examples for HTTPS load balancing - Update documentation with HTTPS load balancing examples - Support health checks for HTTPS load balancing groups This feature enables high availability and horizontal scaling for HTTPS services by distributing traffic across multiple backend HTTPS endpoints using round-robin load balancing, similar to existing HTTP load balancing functionality. Closes #[ISSUE_NUMBER]pull/4939/head
parent
024c334d9d
commit
5b4aea6454
26
README.md
26
README.md
|
@ -932,7 +932,7 @@ This feature is suitable for a large number of short connections.
|
||||||
|
|
||||||
Load balancing is supported by `group`.
|
Load balancing is supported by `group`.
|
||||||
|
|
||||||
This feature is only available for types `tcp`, `http`, `tcpmux` now.
|
This feature is available for types `tcp`, `http`, `https`, `tcpmux` now.
|
||||||
|
|
||||||
```toml
|
```toml
|
||||||
# frpc.toml
|
# frpc.toml
|
||||||
|
@ -954,6 +954,28 @@ loadBalancer.group = "web"
|
||||||
loadBalancer.groupKey = "123"
|
loadBalancer.groupKey = "123"
|
||||||
```
|
```
|
||||||
|
|
||||||
|
For HTTPS load balancing:
|
||||||
|
|
||||||
|
```toml
|
||||||
|
# frpc.toml
|
||||||
|
|
||||||
|
[[proxies]]
|
||||||
|
name = "web1"
|
||||||
|
type = "https"
|
||||||
|
localPort = 443
|
||||||
|
customDomains = ["example.com"]
|
||||||
|
loadBalancer.group = "web"
|
||||||
|
loadBalancer.groupKey = "123"
|
||||||
|
|
||||||
|
[[proxies]]
|
||||||
|
name = "web2"
|
||||||
|
type = "https"
|
||||||
|
localPort = 443
|
||||||
|
customDomains = ["example.com"]
|
||||||
|
loadBalancer.group = "web"
|
||||||
|
loadBalancer.groupKey = "123"
|
||||||
|
```
|
||||||
|
|
||||||
`loadBalancer.groupKey` is used for authentication.
|
`loadBalancer.groupKey` is used for authentication.
|
||||||
|
|
||||||
Connections to port 80 will be dispatched to proxies in the same group randomly.
|
Connections to port 80 will be dispatched to proxies in the same group randomly.
|
||||||
|
@ -962,6 +984,8 @@ For type `tcp`, `remotePort` in the same group should be the same.
|
||||||
|
|
||||||
For type `http`, `customDomains`, `subdomain`, `locations` should be the same.
|
For type `http`, `customDomains`, `subdomain`, `locations` should be the same.
|
||||||
|
|
||||||
|
For type `https`, `customDomains`, `subdomain` should be the same.
|
||||||
|
|
||||||
### Service Health Check
|
### Service Health Check
|
||||||
|
|
||||||
Health check feature can help you achieve high availability with load balancing.
|
Health check feature can help you achieve high availability with load balancing.
|
||||||
|
|
|
@ -247,6 +247,35 @@ customDomains = ["web02.yourdomain.com"]
|
||||||
# v1 or v2 or empty
|
# v1 or v2 or empty
|
||||||
transport.proxyProtocolVersion = "v2"
|
transport.proxyProtocolVersion = "v2"
|
||||||
|
|
||||||
|
# HTTPS load balancing example
|
||||||
|
[[proxies]]
|
||||||
|
name = "web_lb_1"
|
||||||
|
type = "https"
|
||||||
|
localIP = "127.0.0.1"
|
||||||
|
localPort = 443
|
||||||
|
customDomains = ["app.yourdomain.com"]
|
||||||
|
loadBalancer.group = "web"
|
||||||
|
loadBalancer.groupKey = "123"
|
||||||
|
# Enable health check for load balancing
|
||||||
|
healthCheck.type = "tcp"
|
||||||
|
healthCheck.timeoutSeconds = 3
|
||||||
|
healthCheck.maxFailed = 3
|
||||||
|
healthCheck.intervalSeconds = 10
|
||||||
|
|
||||||
|
[[proxies]]
|
||||||
|
name = "web_lb_2"
|
||||||
|
type = "https"
|
||||||
|
localIP = "127.0.0.1"
|
||||||
|
localPort = 8443
|
||||||
|
customDomains = ["app.yourdomain.com"]
|
||||||
|
loadBalancer.group = "web"
|
||||||
|
loadBalancer.groupKey = "123"
|
||||||
|
# Enable health check for load balancing
|
||||||
|
healthCheck.type = "tcp"
|
||||||
|
healthCheck.timeoutSeconds = 3
|
||||||
|
healthCheck.maxFailed = 3
|
||||||
|
healthCheck.intervalSeconds = 10
|
||||||
|
|
||||||
[[proxies]]
|
[[proxies]]
|
||||||
name = "tcpmuxhttpconnect"
|
name = "tcpmuxhttpconnect"
|
||||||
type = "tcpmux"
|
type = "tcpmux"
|
||||||
|
|
|
@ -18,22 +18,174 @@ import (
|
||||||
"crypto/tls"
|
"crypto/tls"
|
||||||
"io"
|
"io"
|
||||||
"net"
|
"net"
|
||||||
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/fatedier/golib/errors"
|
||||||
|
libio "github.com/fatedier/golib/io"
|
||||||
libnet "github.com/fatedier/golib/net"
|
libnet "github.com/fatedier/golib/net"
|
||||||
|
|
||||||
|
httppkg "github.com/fatedier/frp/pkg/util/http"
|
||||||
|
"github.com/fatedier/frp/pkg/util/log"
|
||||||
|
"github.com/fatedier/frp/pkg/util/xlog"
|
||||||
)
|
)
|
||||||
|
|
||||||
type HTTPSMuxer struct {
|
type HTTPSMuxer struct {
|
||||||
*Muxer
|
*Muxer
|
||||||
|
httpsReverseProxy *HTTPSReverseProxy
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewHTTPSMuxer(listener net.Listener, timeout time.Duration) (*HTTPSMuxer, error) {
|
func NewHTTPSMuxer(listener net.Listener, timeout time.Duration) (*HTTPSMuxer, error) {
|
||||||
mux, err := NewMuxer(listener, GetHTTPSHostname, timeout)
|
// Create muxer without auto-starting run method
|
||||||
mux.SetFailHookFunc(vhostFailed)
|
mux := &Muxer{
|
||||||
if err != nil {
|
listener: listener,
|
||||||
return nil, err
|
timeout: timeout,
|
||||||
|
vhostFunc: GetHTTPSHostname,
|
||||||
|
registryRouter: NewRouters(),
|
||||||
|
}
|
||||||
|
mux.SetFailHookFunc(vhostFailed)
|
||||||
|
|
||||||
|
httpsMux := &HTTPSMuxer{Muxer: mux}
|
||||||
|
// Start our custom handler
|
||||||
|
go httpsMux.run()
|
||||||
|
return httpsMux, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *HTTPSMuxer) SetHTTPSReverseProxy(rp *HTTPSReverseProxy) {
|
||||||
|
h.httpsReverseProxy = rp
|
||||||
|
}
|
||||||
|
|
||||||
|
// Override the base muxer's run method to handle group routing
|
||||||
|
func (h *HTTPSMuxer) run() {
|
||||||
|
for {
|
||||||
|
conn, err := h.listener.Accept()
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
go h.handleHTTPS(conn)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *HTTPSMuxer) handleHTTPS(c net.Conn) {
|
||||||
|
if err := c.SetDeadline(time.Now().Add(h.timeout)); err != nil {
|
||||||
|
_ = c.Close()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
sConn, reqInfoMap, err := h.vhostFunc(c)
|
||||||
|
if err != nil {
|
||||||
|
log.Debugf("get hostname from https request error: %v", err)
|
||||||
|
_ = c.Close()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
hostname := strings.ToLower(reqInfoMap["Host"])
|
||||||
|
|
||||||
|
// Validate and canonicalize hostname for security
|
||||||
|
canonicalHostname, err := httppkg.CanonicalHost(hostname)
|
||||||
|
if err != nil {
|
||||||
|
log.Debugf("invalid hostname [%s]: %v", hostname, err)
|
||||||
|
h.failHook(sConn)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// First check if there's a group route for this domain
|
||||||
|
if h.httpsReverseProxy != nil {
|
||||||
|
if routeConfig := h.httpsReverseProxy.GetRouteConfig(canonicalHostname); routeConfig != nil {
|
||||||
|
log.Debugf("routing https request for host [%s] to group", hostname)
|
||||||
|
|
||||||
|
// SECURITY: Apply authentication check before group routing
|
||||||
|
if routeConfig.Username != "" && routeConfig.Password != "" {
|
||||||
|
if h.checkAuth != nil {
|
||||||
|
ok, err := h.checkAuth(c, routeConfig.Username, routeConfig.Password, reqInfoMap)
|
||||||
|
if !ok || err != nil {
|
||||||
|
log.Debugf("auth failed for group route user: %s", routeConfig.Username)
|
||||||
|
h.failHook(sConn)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// SECURITY: Apply success hook for group routing
|
||||||
|
if h.successHook != nil {
|
||||||
|
if err := h.successHook(c, reqInfoMap); err != nil {
|
||||||
|
log.Infof("success func failure on group vhost connection: %v", err)
|
||||||
|
_ = c.Close()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if err = sConn.SetDeadline(time.Time{}); err != nil {
|
||||||
|
_ = c.Close()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create connection to backend through group routing
|
||||||
|
remoteConn, err := h.httpsReverseProxy.CreateConnection(canonicalHostname)
|
||||||
|
if err != nil {
|
||||||
|
log.Debugf("failed to create connection through group: %v", err)
|
||||||
|
h.failHook(sConn)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Start proxying data between client and remote
|
||||||
|
go func() {
|
||||||
|
defer func() {
|
||||||
|
if err := recover(); err != nil {
|
||||||
|
log.Warnf("panic in HTTPS proxy goroutine: %v", err)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
defer sConn.Close()
|
||||||
|
defer remoteConn.Close()
|
||||||
|
libio.Join(sConn, remoteConn)
|
||||||
|
}()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fall back to direct listener routing (existing behavior)
|
||||||
|
path := strings.ToLower(reqInfoMap["Path"])
|
||||||
|
httpUser := reqInfoMap["HTTPUser"]
|
||||||
|
l, ok := h.getListener(canonicalHostname, path, httpUser)
|
||||||
|
if !ok {
|
||||||
|
log.Debugf("https request for host [%s] path [%s] httpUser [%s] not found", hostname, path, httpUser)
|
||||||
|
h.failHook(sConn)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
xl := xlog.FromContextSafe(l.ctx)
|
||||||
|
if h.successHook != nil {
|
||||||
|
if err := h.successHook(c, reqInfoMap); err != nil {
|
||||||
|
xl.Infof("success func failure on vhost connection: %v", err)
|
||||||
|
_ = c.Close()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// if checkAuth func is exist and username/password is set
|
||||||
|
// then verify user access
|
||||||
|
if l.mux.checkAuth != nil && l.username != "" {
|
||||||
|
ok, err := l.mux.checkAuth(c, l.username, l.password, reqInfoMap)
|
||||||
|
if !ok || err != nil {
|
||||||
|
xl.Debugf("auth failed for user: %s", l.username)
|
||||||
|
_ = c.Close()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if err = sConn.SetDeadline(time.Time{}); err != nil {
|
||||||
|
_ = c.Close()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
c = sConn
|
||||||
|
|
||||||
|
xl.Debugf("new https request host [%s] path [%s] httpUser [%s]", hostname, path, httpUser)
|
||||||
|
err = errors.PanicToError(func() {
|
||||||
|
l.accept <- c
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
xl.Warnf("listener is already closed, ignore this request")
|
||||||
}
|
}
|
||||||
return &HTTPSMuxer{mux}, err
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func GetHTTPSHostname(c net.Conn) (_ net.Conn, _ map[string]string, err error) {
|
func GetHTTPSHostname(c net.Conn) (_ net.Conn, _ map[string]string, err error) {
|
||||||
|
|
|
@ -0,0 +1,143 @@
|
||||||
|
// Copyright 2024 fatedier, fatedier@gmail.com
|
||||||
|
//
|
||||||
|
// 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 (
|
||||||
|
"fmt"
|
||||||
|
"net"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
libio "github.com/fatedier/golib/io"
|
||||||
|
|
||||||
|
httppkg "github.com/fatedier/frp/pkg/util/http"
|
||||||
|
"github.com/fatedier/frp/pkg/util/log"
|
||||||
|
)
|
||||||
|
|
||||||
|
type HTTPSReverseProxy struct {
|
||||||
|
vhostRouter *Routers
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewHTTPSReverseProxy(vhostRouter *Routers) *HTTPSReverseProxy {
|
||||||
|
return &HTTPSReverseProxy{
|
||||||
|
vhostRouter: vhostRouter,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Register register the route config to reverse proxy
|
||||||
|
// reverse proxy will use CreateConnFn from routeCfg to create a connection to the remote service
|
||||||
|
func (rp *HTTPSReverseProxy) Register(routeCfg RouteConfig) error {
|
||||||
|
err := rp.vhostRouter.Add(routeCfg.Domain, "", "", &routeCfg)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// UnRegister unregister route config by domain
|
||||||
|
func (rp *HTTPSReverseProxy) UnRegister(routeCfg RouteConfig) {
|
||||||
|
rp.vhostRouter.Del(routeCfg.Domain, "", "")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (rp *HTTPSReverseProxy) GetRouteConfig(domain string) *RouteConfig {
|
||||||
|
// Validate and canonicalize hostname for security
|
||||||
|
canonicalDomain, err := httppkg.CanonicalHost(domain)
|
||||||
|
if err != nil {
|
||||||
|
log.Debugf("invalid hostname [%s]: %v", domain, err)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
vr, ok := rp.getVhost(canonicalDomain)
|
||||||
|
if ok {
|
||||||
|
log.Debugf("get new https request host [%s]", canonicalDomain)
|
||||||
|
return vr.payload.(*RouteConfig)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// CreateConnection create a new connection by route config
|
||||||
|
func (rp *HTTPSReverseProxy) CreateConnection(domain string) (net.Conn, error) {
|
||||||
|
// Validate and canonicalize hostname for security
|
||||||
|
canonicalDomain, err := httppkg.CanonicalHost(domain)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("invalid hostname: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
vr, ok := rp.getVhost(canonicalDomain)
|
||||||
|
if ok {
|
||||||
|
fn := vr.payload.(*RouteConfig).CreateConnFn
|
||||||
|
if fn != nil {
|
||||||
|
return fn("")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil, fmt.Errorf("%v: %s", ErrNoRouteFound, canonicalDomain)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ProxyConn proxy connection for HTTPS
|
||||||
|
func (rp *HTTPSReverseProxy) ProxyConn(clientConn net.Conn, domain string) error {
|
||||||
|
remoteConn, err := rp.CreateConnection(domain)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Start proxying data between client and remote
|
||||||
|
go func() {
|
||||||
|
defer clientConn.Close()
|
||||||
|
defer remoteConn.Close()
|
||||||
|
libio.Join(clientConn, remoteConn)
|
||||||
|
}()
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// getVhost tries to get vhost router by domain.
|
||||||
|
func (rp *HTTPSReverseProxy) getVhost(domain string) (*Router, bool) {
|
||||||
|
findRouter := func(inDomain string) (*Router, bool) {
|
||||||
|
vr, ok := rp.vhostRouter.Get(inDomain, "", "")
|
||||||
|
if ok {
|
||||||
|
return vr, ok
|
||||||
|
}
|
||||||
|
return nil, false
|
||||||
|
}
|
||||||
|
|
||||||
|
domain = strings.ToLower(domain)
|
||||||
|
|
||||||
|
// First we check the full hostname
|
||||||
|
// if not exist, then check the wildcard_domain such as *.example.com
|
||||||
|
vr, ok := findRouter(domain)
|
||||||
|
if ok {
|
||||||
|
return vr, ok
|
||||||
|
}
|
||||||
|
|
||||||
|
// e.g. domain = test.example.com, try to match wildcard domains.
|
||||||
|
// *.example.com
|
||||||
|
// *.com
|
||||||
|
domainSplit := strings.Split(domain, ".")
|
||||||
|
for len(domainSplit) >= 3 {
|
||||||
|
domainSplit[0] = "*"
|
||||||
|
wildcardDomain := strings.Join(domainSplit, ".")
|
||||||
|
vr, ok = findRouter(wildcardDomain)
|
||||||
|
if ok {
|
||||||
|
return vr, true
|
||||||
|
}
|
||||||
|
domainSplit = domainSplit[1:]
|
||||||
|
}
|
||||||
|
|
||||||
|
// Finally, try to check if there is one proxy that domain is "*" means match all domains.
|
||||||
|
vr, ok = findRouter("*")
|
||||||
|
if ok {
|
||||||
|
return vr, true
|
||||||
|
}
|
||||||
|
return nil, false
|
||||||
|
}
|
|
@ -35,6 +35,9 @@ type ResourceController struct {
|
||||||
// HTTP Group Controller
|
// HTTP Group Controller
|
||||||
HTTPGroupCtl *group.HTTPGroupController
|
HTTPGroupCtl *group.HTTPGroupController
|
||||||
|
|
||||||
|
// HTTPS Group Controller
|
||||||
|
HTTPSGroupCtl *group.HTTPSGroupController
|
||||||
|
|
||||||
// TCP Mux Group Controller
|
// TCP Mux Group Controller
|
||||||
TCPMuxGroupCtl *group.TCPMuxGroupCtl
|
TCPMuxGroupCtl *group.TCPMuxGroupCtl
|
||||||
|
|
||||||
|
@ -47,6 +50,9 @@ type ResourceController struct {
|
||||||
// For HTTP proxies, forwarding HTTP requests
|
// For HTTP proxies, forwarding HTTP requests
|
||||||
HTTPReverseProxy *vhost.HTTPReverseProxy
|
HTTPReverseProxy *vhost.HTTPReverseProxy
|
||||||
|
|
||||||
|
// For HTTPS proxies, forwarding HTTPS requests (for group load balancing)
|
||||||
|
HTTPSReverseProxy *vhost.HTTPSReverseProxy
|
||||||
|
|
||||||
// For HTTPS proxies, route requests to different clients by hostname and other information
|
// For HTTPS proxies, route requests to different clients by hostname and other information
|
||||||
VhostHTTPSMuxer *vhost.HTTPSMuxer
|
VhostHTTPSMuxer *vhost.HTTPSMuxer
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,189 @@
|
||||||
|
package group
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"net"
|
||||||
|
"sync"
|
||||||
|
"sync/atomic"
|
||||||
|
|
||||||
|
"github.com/fatedier/frp/pkg/util/vhost"
|
||||||
|
)
|
||||||
|
|
||||||
|
type HTTPSGroupController struct {
|
||||||
|
// groups indexed by group name
|
||||||
|
groups map[string]*HTTPSGroup
|
||||||
|
|
||||||
|
// register createConn for each group to vhostRouter.
|
||||||
|
// createConn will get a connection from one proxy of the group
|
||||||
|
vhostRouter *vhost.Routers
|
||||||
|
|
||||||
|
mu sync.Mutex
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewHTTPSGroupController(vhostRouter *vhost.Routers) *HTTPSGroupController {
|
||||||
|
return &HTTPSGroupController{
|
||||||
|
groups: make(map[string]*HTTPSGroup),
|
||||||
|
vhostRouter: vhostRouter,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ctl *HTTPSGroupController) Register(
|
||||||
|
proxyName, group, groupKey string,
|
||||||
|
routeConfig vhost.RouteConfig,
|
||||||
|
) (err error) {
|
||||||
|
indexKey := group
|
||||||
|
ctl.mu.Lock()
|
||||||
|
g, ok := ctl.groups[indexKey]
|
||||||
|
if !ok {
|
||||||
|
g = NewHTTPSGroup(ctl)
|
||||||
|
ctl.groups[indexKey] = g
|
||||||
|
}
|
||||||
|
ctl.mu.Unlock()
|
||||||
|
|
||||||
|
return g.Register(proxyName, group, groupKey, routeConfig)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ctl *HTTPSGroupController) UnRegister(proxyName, group string, _ vhost.RouteConfig) {
|
||||||
|
indexKey := group
|
||||||
|
ctl.mu.Lock()
|
||||||
|
defer ctl.mu.Unlock()
|
||||||
|
g, ok := ctl.groups[indexKey]
|
||||||
|
if !ok {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
isEmpty := g.UnRegister(proxyName)
|
||||||
|
if isEmpty {
|
||||||
|
delete(ctl.groups, indexKey)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type HTTPSGroup struct {
|
||||||
|
group string
|
||||||
|
groupKey string
|
||||||
|
domain string
|
||||||
|
|
||||||
|
// CreateConnFuncs indexed by proxy name
|
||||||
|
createFuncs map[string]vhost.CreateConnFunc
|
||||||
|
pxyNames []string
|
||||||
|
index uint64
|
||||||
|
ctl *HTTPSGroupController
|
||||||
|
mu sync.RWMutex
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewHTTPSGroup(ctl *HTTPSGroupController) *HTTPSGroup {
|
||||||
|
return &HTTPSGroup{
|
||||||
|
createFuncs: make(map[string]vhost.CreateConnFunc),
|
||||||
|
pxyNames: make([]string, 0),
|
||||||
|
ctl: ctl,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (g *HTTPSGroup) Register(
|
||||||
|
proxyName, group, groupKey string,
|
||||||
|
routeConfig vhost.RouteConfig,
|
||||||
|
) (err error) {
|
||||||
|
g.mu.Lock()
|
||||||
|
defer g.mu.Unlock()
|
||||||
|
if len(g.createFuncs) == 0 {
|
||||||
|
// the first proxy in this group
|
||||||
|
tmp := routeConfig // copy object
|
||||||
|
tmp.CreateConnFn = g.createConn
|
||||||
|
tmp.ChooseEndpointFn = g.chooseEndpoint
|
||||||
|
tmp.CreateConnByEndpointFn = g.createConnByEndpoint
|
||||||
|
err = g.ctl.vhostRouter.Add(routeConfig.Domain, "", "", &tmp)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
g.group = group
|
||||||
|
g.groupKey = groupKey
|
||||||
|
g.domain = routeConfig.Domain
|
||||||
|
} else {
|
||||||
|
if g.group != group || g.domain != routeConfig.Domain {
|
||||||
|
err = ErrGroupParamsInvalid
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if g.groupKey != groupKey {
|
||||||
|
err = ErrGroupAuthFailed
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if _, ok := g.createFuncs[proxyName]; ok {
|
||||||
|
err = ErrProxyRepeated
|
||||||
|
return
|
||||||
|
}
|
||||||
|
g.createFuncs[proxyName] = routeConfig.CreateConnFn
|
||||||
|
g.pxyNames = append(g.pxyNames, proxyName)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (g *HTTPSGroup) UnRegister(proxyName string) (isEmpty bool) {
|
||||||
|
g.mu.Lock()
|
||||||
|
defer g.mu.Unlock()
|
||||||
|
delete(g.createFuncs, proxyName)
|
||||||
|
for i, name := range g.pxyNames {
|
||||||
|
if name == proxyName {
|
||||||
|
g.pxyNames = append(g.pxyNames[:i], g.pxyNames[i+1:]...)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(g.createFuncs) == 0 {
|
||||||
|
isEmpty = true
|
||||||
|
g.ctl.vhostRouter.Del(g.domain, "", "")
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func (g *HTTPSGroup) createConn(remoteAddr string) (net.Conn, error) {
|
||||||
|
var f vhost.CreateConnFunc
|
||||||
|
newIndex := atomic.AddUint64(&g.index, 1)
|
||||||
|
|
||||||
|
g.mu.RLock()
|
||||||
|
group := g.group
|
||||||
|
domain := g.domain
|
||||||
|
if len(g.pxyNames) > 0 {
|
||||||
|
name := g.pxyNames[int(newIndex)%len(g.pxyNames)]
|
||||||
|
f = g.createFuncs[name]
|
||||||
|
}
|
||||||
|
g.mu.RUnlock()
|
||||||
|
|
||||||
|
if f == nil {
|
||||||
|
return nil, fmt.Errorf("no CreateConnFunc for https group [%s], domain [%s]",
|
||||||
|
group, domain)
|
||||||
|
}
|
||||||
|
|
||||||
|
return f(remoteAddr)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (g *HTTPSGroup) chooseEndpoint() (string, error) {
|
||||||
|
newIndex := atomic.AddUint64(&g.index, 1)
|
||||||
|
name := ""
|
||||||
|
|
||||||
|
g.mu.RLock()
|
||||||
|
group := g.group
|
||||||
|
domain := g.domain
|
||||||
|
if len(g.pxyNames) > 0 {
|
||||||
|
name = g.pxyNames[int(newIndex)%len(g.pxyNames)]
|
||||||
|
}
|
||||||
|
g.mu.RUnlock()
|
||||||
|
|
||||||
|
if name == "" {
|
||||||
|
return "", fmt.Errorf("no healthy endpoint for https group [%s], domain [%s]",
|
||||||
|
group, domain)
|
||||||
|
}
|
||||||
|
return name, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (g *HTTPSGroup) createConnByEndpoint(endpoint, remoteAddr string) (net.Conn, error) {
|
||||||
|
var f vhost.CreateConnFunc
|
||||||
|
g.mu.RLock()
|
||||||
|
f = g.createFuncs[endpoint]
|
||||||
|
g.mu.RUnlock()
|
||||||
|
|
||||||
|
if f == nil {
|
||||||
|
return nil, fmt.Errorf("no CreateConnFunc for endpoint [%s] in group [%s]", endpoint, g.group)
|
||||||
|
}
|
||||||
|
return f(remoteAddr)
|
||||||
|
}
|
|
@ -15,12 +15,19 @@
|
||||||
package proxy
|
package proxy
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"io"
|
||||||
|
"net"
|
||||||
"reflect"
|
"reflect"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
|
libio "github.com/fatedier/golib/io"
|
||||||
|
|
||||||
v1 "github.com/fatedier/frp/pkg/config/v1"
|
v1 "github.com/fatedier/frp/pkg/config/v1"
|
||||||
|
"github.com/fatedier/frp/pkg/util/limit"
|
||||||
|
netpkg "github.com/fatedier/frp/pkg/util/net"
|
||||||
"github.com/fatedier/frp/pkg/util/util"
|
"github.com/fatedier/frp/pkg/util/util"
|
||||||
"github.com/fatedier/frp/pkg/util/vhost"
|
"github.com/fatedier/frp/pkg/util/vhost"
|
||||||
|
"github.com/fatedier/frp/server/metrics"
|
||||||
)
|
)
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
|
@ -30,6 +37,8 @@ func init() {
|
||||||
type HTTPSProxy struct {
|
type HTTPSProxy struct {
|
||||||
*BaseProxy
|
*BaseProxy
|
||||||
cfg *v1.HTTPSProxyConfig
|
cfg *v1.HTTPSProxyConfig
|
||||||
|
|
||||||
|
closeFuncs []func()
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewHTTPSProxy(baseProxy *BaseProxy) Proxy {
|
func NewHTTPSProxy(baseProxy *BaseProxy) Proxy {
|
||||||
|
@ -45,13 +54,16 @@ func NewHTTPSProxy(baseProxy *BaseProxy) Proxy {
|
||||||
|
|
||||||
func (pxy *HTTPSProxy) Run() (remoteAddr string, err error) {
|
func (pxy *HTTPSProxy) Run() (remoteAddr string, err error) {
|
||||||
xl := pxy.xl
|
xl := pxy.xl
|
||||||
routeConfig := &vhost.RouteConfig{}
|
routeConfig := vhost.RouteConfig{
|
||||||
|
CreateConnFn: pxy.GetRealConn,
|
||||||
|
}
|
||||||
|
|
||||||
defer func() {
|
defer func() {
|
||||||
if err != nil {
|
if err != nil {
|
||||||
pxy.Close()
|
pxy.Close()
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
|
|
||||||
addrs := make([]string, 0)
|
addrs := make([]string, 0)
|
||||||
for _, domain := range pxy.cfg.CustomDomains {
|
for _, domain := range pxy.cfg.CustomDomains {
|
||||||
if domain == "" {
|
if domain == "" {
|
||||||
|
@ -59,26 +71,63 @@ func (pxy *HTTPSProxy) Run() (remoteAddr string, err error) {
|
||||||
}
|
}
|
||||||
|
|
||||||
routeConfig.Domain = domain
|
routeConfig.Domain = domain
|
||||||
l, errRet := pxy.rc.VhostHTTPSMuxer.Listen(pxy.ctx, routeConfig)
|
|
||||||
if errRet != nil {
|
tmpRouteConfig := routeConfig
|
||||||
err = errRet
|
|
||||||
return
|
// handle group
|
||||||
|
if pxy.cfg.LoadBalancer.Group != "" {
|
||||||
|
err = pxy.rc.HTTPSGroupCtl.Register(pxy.name, pxy.cfg.LoadBalancer.Group, pxy.cfg.LoadBalancer.GroupKey, routeConfig)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
pxy.closeFuncs = append(pxy.closeFuncs, func() {
|
||||||
|
pxy.rc.HTTPSGroupCtl.UnRegister(pxy.name, pxy.cfg.LoadBalancer.Group, tmpRouteConfig)
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
// no group - use direct muxer
|
||||||
|
l, errRet := pxy.rc.VhostHTTPSMuxer.Listen(pxy.ctx, &routeConfig)
|
||||||
|
if errRet != nil {
|
||||||
|
err = errRet
|
||||||
|
return
|
||||||
|
}
|
||||||
|
xl.Infof("https proxy listen for host [%s]", routeConfig.Domain)
|
||||||
|
pxy.listeners = append(pxy.listeners, l)
|
||||||
}
|
}
|
||||||
xl.Infof("https proxy listen for host [%s]", routeConfig.Domain)
|
|
||||||
pxy.listeners = append(pxy.listeners, l)
|
|
||||||
addrs = append(addrs, util.CanonicalAddr(routeConfig.Domain, pxy.serverCfg.VhostHTTPSPort))
|
addrs = append(addrs, util.CanonicalAddr(routeConfig.Domain, pxy.serverCfg.VhostHTTPSPort))
|
||||||
|
xl.Infof("https proxy listen for host [%s] group [%s]",
|
||||||
|
routeConfig.Domain, pxy.cfg.LoadBalancer.Group)
|
||||||
}
|
}
|
||||||
|
|
||||||
if pxy.cfg.SubDomain != "" {
|
if pxy.cfg.SubDomain != "" {
|
||||||
routeConfig.Domain = pxy.cfg.SubDomain + "." + pxy.serverCfg.SubDomainHost
|
routeConfig.Domain = pxy.cfg.SubDomain + "." + pxy.serverCfg.SubDomainHost
|
||||||
l, errRet := pxy.rc.VhostHTTPSMuxer.Listen(pxy.ctx, routeConfig)
|
|
||||||
if errRet != nil {
|
tmpRouteConfig := routeConfig
|
||||||
err = errRet
|
|
||||||
return
|
// handle group
|
||||||
|
if pxy.cfg.LoadBalancer.Group != "" {
|
||||||
|
err = pxy.rc.HTTPSGroupCtl.Register(pxy.name, pxy.cfg.LoadBalancer.Group, pxy.cfg.LoadBalancer.GroupKey, routeConfig)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
pxy.closeFuncs = append(pxy.closeFuncs, func() {
|
||||||
|
pxy.rc.HTTPSGroupCtl.UnRegister(pxy.name, pxy.cfg.LoadBalancer.Group, tmpRouteConfig)
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
// no group - use direct muxer
|
||||||
|
l, errRet := pxy.rc.VhostHTTPSMuxer.Listen(pxy.ctx, &routeConfig)
|
||||||
|
if errRet != nil {
|
||||||
|
err = errRet
|
||||||
|
return
|
||||||
|
}
|
||||||
|
xl.Infof("https proxy listen for host [%s]", routeConfig.Domain)
|
||||||
|
pxy.listeners = append(pxy.listeners, l)
|
||||||
}
|
}
|
||||||
xl.Infof("https proxy listen for host [%s]", routeConfig.Domain)
|
|
||||||
pxy.listeners = append(pxy.listeners, l)
|
|
||||||
addrs = append(addrs, util.CanonicalAddr(routeConfig.Domain, pxy.serverCfg.VhostHTTPSPort))
|
addrs = append(addrs, util.CanonicalAddr(routeConfig.Domain, pxy.serverCfg.VhostHTTPSPort))
|
||||||
|
|
||||||
|
xl.Infof("https proxy listen for host [%s] group [%s]",
|
||||||
|
routeConfig.Domain, pxy.cfg.LoadBalancer.Group)
|
||||||
}
|
}
|
||||||
|
|
||||||
pxy.startCommonTCPListenersHandler()
|
pxy.startCommonTCPListenersHandler()
|
||||||
|
@ -86,6 +135,55 @@ func (pxy *HTTPSProxy) Run() (remoteAddr string, err error) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (pxy *HTTPSProxy) GetRealConn(remoteAddr string) (workConn net.Conn, err error) {
|
||||||
|
xl := pxy.xl
|
||||||
|
rAddr, errRet := net.ResolveTCPAddr("tcp", remoteAddr)
|
||||||
|
if errRet != nil {
|
||||||
|
xl.Warnf("resolve TCP addr [%s] error: %v", remoteAddr, errRet)
|
||||||
|
// we do not return error here since remoteAddr is not necessary for proxies without proxy protocol enabled
|
||||||
|
}
|
||||||
|
|
||||||
|
tmpConn, errRet := pxy.GetWorkConnFromPool(rAddr, nil)
|
||||||
|
if errRet != nil {
|
||||||
|
err = errRet
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var rwc io.ReadWriteCloser = tmpConn
|
||||||
|
if pxy.cfg.Transport.UseEncryption {
|
||||||
|
rwc, err = libio.WithEncryption(rwc, []byte(pxy.serverCfg.Auth.Token))
|
||||||
|
if err != nil {
|
||||||
|
xl.Errorf("create encryption stream error: %v", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if pxy.cfg.Transport.UseCompression {
|
||||||
|
rwc = libio.WithCompression(rwc)
|
||||||
|
}
|
||||||
|
|
||||||
|
if pxy.GetLimiter() != nil {
|
||||||
|
rwc = libio.WrapReadWriteCloser(limit.NewReader(rwc, pxy.GetLimiter()), limit.NewWriter(rwc, pxy.GetLimiter()), func() error {
|
||||||
|
return rwc.Close()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
workConn = netpkg.WrapReadWriteCloserToConn(rwc, tmpConn)
|
||||||
|
workConn = netpkg.WrapStatsConn(workConn, pxy.updateStatsAfterClosedConn)
|
||||||
|
metrics.Server.OpenConnection(pxy.GetName(), pxy.GetConfigurer().GetBaseConfig().Type)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func (pxy *HTTPSProxy) updateStatsAfterClosedConn(totalRead, totalWrite int64) {
|
||||||
|
name := pxy.GetName()
|
||||||
|
proxyType := pxy.GetConfigurer().GetBaseConfig().Type
|
||||||
|
metrics.Server.CloseConnection(name, proxyType)
|
||||||
|
metrics.Server.AddTrafficIn(name, proxyType, totalWrite)
|
||||||
|
metrics.Server.AddTrafficOut(name, proxyType, totalRead)
|
||||||
|
}
|
||||||
|
|
||||||
func (pxy *HTTPSProxy) Close() {
|
func (pxy *HTTPSProxy) Close() {
|
||||||
pxy.BaseProxy.Close()
|
pxy.BaseProxy.Close()
|
||||||
|
for _, closeFn := range pxy.closeFuncs {
|
||||||
|
closeFn()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -106,6 +106,9 @@ type Service struct {
|
||||||
// HTTP vhost router
|
// HTTP vhost router
|
||||||
httpVhostRouter *vhost.Routers
|
httpVhostRouter *vhost.Routers
|
||||||
|
|
||||||
|
// HTTPS vhost router
|
||||||
|
httpsVhostRouter *vhost.Routers
|
||||||
|
|
||||||
// All resource managers and controllers
|
// All resource managers and controllers
|
||||||
rc *controller.ResourceController
|
rc *controller.ResourceController
|
||||||
|
|
||||||
|
@ -161,6 +164,7 @@ func NewService(cfg *v1.ServerConfig) (*Service, error) {
|
||||||
},
|
},
|
||||||
sshTunnelListener: netpkg.NewInternalListener(),
|
sshTunnelListener: netpkg.NewInternalListener(),
|
||||||
httpVhostRouter: vhost.NewRouters(),
|
httpVhostRouter: vhost.NewRouters(),
|
||||||
|
httpsVhostRouter: vhost.NewRouters(),
|
||||||
authVerifier: auth.NewAuthVerifier(cfg.Auth),
|
authVerifier: auth.NewAuthVerifier(cfg.Auth),
|
||||||
webServer: webServer,
|
webServer: webServer,
|
||||||
tlsConfig: tlsConfig,
|
tlsConfig: tlsConfig,
|
||||||
|
@ -200,6 +204,9 @@ func NewService(cfg *v1.ServerConfig) (*Service, error) {
|
||||||
// Init HTTP group controller
|
// Init HTTP group controller
|
||||||
svr.rc.HTTPGroupCtl = group.NewHTTPGroupController(svr.httpVhostRouter)
|
svr.rc.HTTPGroupCtl = group.NewHTTPGroupController(svr.httpVhostRouter)
|
||||||
|
|
||||||
|
// Init HTTPS group controller
|
||||||
|
svr.rc.HTTPSGroupCtl = group.NewHTTPSGroupController(svr.httpsVhostRouter)
|
||||||
|
|
||||||
// Init TCP mux group controller
|
// Init TCP mux group controller
|
||||||
svr.rc.TCPMuxGroupCtl = group.NewTCPMuxGroupCtl(svr.rc.TCPMuxHTTPConnectMuxer)
|
svr.rc.TCPMuxGroupCtl = group.NewTCPMuxGroupCtl(svr.rc.TCPMuxHTTPConnectMuxer)
|
||||||
|
|
||||||
|
@ -323,6 +330,13 @@ func NewService(cfg *v1.ServerConfig) (*Service, error) {
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("create vhost httpsMuxer error, %v", err)
|
return nil, fmt.Errorf("create vhost httpsMuxer error, %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Init HTTPS reverse proxy for group routing
|
||||||
|
httpsReverseProxy := vhost.NewHTTPSReverseProxy(svr.httpsVhostRouter)
|
||||||
|
svr.rc.HTTPSReverseProxy = httpsReverseProxy
|
||||||
|
|
||||||
|
// Set the reverse proxy on the muxer for group routing
|
||||||
|
svr.rc.VhostHTTPSMuxer.SetHTTPSReverseProxy(httpsReverseProxy)
|
||||||
}
|
}
|
||||||
|
|
||||||
// frp tls listener
|
// frp tls listener
|
||||||
|
|
Loading…
Reference in New Issue