Files
lolia-frp/pkg/util/vhost/vhost.go
T
fatedier e6e0aaf9d4 fix(vhost): close pending connections when a route listener closes (#5592)
When an HTTPS or tcpmux route listener is closed after a connection has been
resolved to it but before the handoff, Muxer.handle clears the deadline and
then sends on the closed accept channel. The recovered panic returns without
closing the connection, so the peer waits until its own read timeout.

Add a done channel and sync.Once to Listener. Close closes done and is
idempotent; handle and Accept select on accept/done, and a pending connection
is closed when done is closed. Connections already handed to Accept keep their
current ownership. Remove the PanicToError-based control flow.

Cover the pending handoff with net.Pipe and real TCP tests, plus an idempotent
Close test.

Fixes #5573
2026-11-02 14:56:32 +08:00

280 lines
6.8 KiB
Go

// 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 (
"context"
"fmt"
"net"
"strings"
"sync"
"time"
"github.com/fatedier/frp/pkg/util/log"
netpkg "github.com/fatedier/frp/pkg/util/net"
"github.com/fatedier/frp/pkg/util/xlog"
)
type RouteInfo string
const (
RouteInfoKey RouteInfo = "routeInfo"
RouteConfigKey RouteInfo = "routeConfig"
)
type RequestRouteInfo struct {
URL string
Host string
HTTPUser string
RemoteAddr string
URLHost string
Endpoint string
}
type (
muxFunc func(net.Conn) (net.Conn, map[string]string, error)
authFunc func(conn net.Conn, username, password string, reqInfoMap map[string]string) (bool, error)
hostRewriteFunc func(net.Conn, string) (net.Conn, error)
successHookFunc func(net.Conn, map[string]string) error
failHookFunc func(net.Conn)
)
// Muxer is a functional component used for https and tcpmux proxies.
// It accepts connections and extracts vhost information from the beginning of the connection data.
// It then routes the connection to its appropriate listener.
type Muxer struct {
listener net.Listener
timeout time.Duration
vhostFunc muxFunc
checkAuth authFunc
successHook successHookFunc
failHook failHookFunc
rewriteHost hostRewriteFunc
registryRouter *Routers
}
func NewMuxer(
listener net.Listener,
vhostFunc muxFunc,
timeout time.Duration,
) (mux *Muxer, err error) {
mux = &Muxer{
listener: listener,
timeout: timeout,
vhostFunc: vhostFunc,
registryRouter: NewRouters(),
}
go mux.run()
return mux, nil
}
func (v *Muxer) SetCheckAuthFunc(f authFunc) *Muxer {
v.checkAuth = f
return v
}
func (v *Muxer) SetSuccessHookFunc(f successHookFunc) *Muxer {
v.successHook = f
return v
}
func (v *Muxer) SetFailHookFunc(f failHookFunc) *Muxer {
v.failHook = f
return v
}
func (v *Muxer) SetRewriteHostFunc(f hostRewriteFunc) *Muxer {
v.rewriteHost = f
return v
}
func (v *Muxer) Close() error {
return v.listener.Close()
}
type ChooseEndpointFunc func() (string, error)
type CreateConnFunc func(remoteAddr string) (net.Conn, error)
type CreateConnByEndpointFunc func(endpoint, remoteAddr string) (net.Conn, error)
// RouteConfig is the params used to match HTTP requests
type RouteConfig struct {
Domain string
Location string
RewriteHost string
Username string
Password string
Headers map[string]string
ResponseHeaders map[string]string
RouteByHTTPUser string
CreateConnFn CreateConnFunc
ChooseEndpointFn ChooseEndpointFunc
CreateConnByEndpointFn CreateConnByEndpointFunc
}
// listen for a new domain name, if rewriteHost is not empty and rewriteHost func is not nil,
// then rewrite the host header to rewriteHost
func (v *Muxer) Listen(ctx context.Context, cfg *RouteConfig) (l *Listener, err error) {
l = &Listener{
name: cfg.Domain,
location: cfg.Location,
routeByHTTPUser: cfg.RouteByHTTPUser,
rewriteHost: cfg.RewriteHost,
username: cfg.Username,
password: cfg.Password,
mux: v,
accept: make(chan net.Conn),
done: make(chan struct{}),
ctx: ctx,
}
err = v.registryRouter.Add(cfg.Domain, cfg.Location, cfg.RouteByHTTPUser, l)
if err != nil {
return
}
return l, nil
}
func (v *Muxer) getListener(name, path, httpUser string) (*Listener, bool) {
vr, ok := v.registryRouter.getByRoute(name, path, httpUser)
if ok {
return vr.payload.(*Listener), true
}
return nil, false
}
func (v *Muxer) run() {
for {
conn, err := v.listener.Accept()
if err != nil {
return
}
go v.handle(conn)
}
}
func (v *Muxer) handle(c net.Conn) {
if err := c.SetDeadline(time.Now().Add(v.timeout)); err != nil {
_ = c.Close()
return
}
sConn, reqInfoMap, err := v.vhostFunc(c)
if err != nil {
log.Debugf("get hostname from http/https request error: %v", err)
_ = c.Close()
return
}
name := strings.ToLower(reqInfoMap["Host"])
path := strings.ToLower(reqInfoMap["Path"])
httpUser := reqInfoMap["HTTPUser"]
l, ok := v.getListener(name, path, httpUser)
if !ok {
log.Debugf("http request for host [%s] path [%s] httpUser [%s] not found", name, path, httpUser)
v.failHook(sConn)
return
}
xl := xlog.FromContextSafe(l.ctx)
if v.successHook != nil {
if err := v.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 request host [%s] path [%s] httpUser [%s]", name, path, httpUser)
select {
case l.accept <- c:
// The connection is now owned by the Accept caller.
case <-l.done:
xl.Warnf("listener is already closed, ignore this request")
_ = c.Close()
}
}
type Listener struct {
name string
location string
routeByHTTPUser string
rewriteHost string
username string
password string
mux *Muxer // for closing Muxer
accept chan net.Conn
done chan struct{}
closeOnce sync.Once
ctx context.Context
}
func (l *Listener) Accept() (net.Conn, error) {
xl := xlog.FromContextSafe(l.ctx)
var conn net.Conn
select {
case conn = <-l.accept:
case <-l.done:
return nil, fmt.Errorf("listener closed")
}
// if rewriteHost func is exist
// rewrite http requests with a modified host header
// if l.rewriteHost is empty, nothing to do
if l.mux.rewriteHost != nil {
sConn, err := l.mux.rewriteHost(conn, l.rewriteHost)
if err != nil {
xl.Warnf("host header rewrite failed: %v", err)
return nil, fmt.Errorf("host header rewrite failed")
}
xl.Debugf("rewrite host to [%s] success", l.rewriteHost)
conn = sConn
}
return netpkg.NewContextConn(l.ctx, conn), nil
}
func (l *Listener) Close() error {
l.closeOnce.Do(func() {
l.mux.registryRouter.Del(l.name, l.location, l.routeByHTTPUser)
close(l.done)
})
return nil
}
func (l *Listener) Name() string {
return l.name
}
func (l *Listener) Addr() net.Addr {
return (*net.TCPAddr)(nil)
}