Files
Mxmilu 71b31978b3 fix(server): close underlying work conn when server bandwidth limiter is enabled (#5553)
When bandwidthLimitMode is "server", the work connection is wrapped by
libio.WrapReadWriteCloser with a close callback that captured the
variable being reassigned (local/rwc). At call time the callback
referenced the wrapper itself, whose closed guard was already set, so
Close returned nil without closing the underlying work connection.

For TCP proxies this left libio.Join blocked on the other copy
goroutine after the user disconnected, keeping the work connection and
the backend connection open. HTTP and UDP proxies had the same pattern.

Bind the Close method value before reassignment, and add an e2e test
verifying that the local connection is closed after the user
disconnects.
2026-10-10 10:45:34 +08:00

273 lines
7.4 KiB
Go

// Copyright 2019 fatedier, [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 proxy
import (
"context"
"fmt"
"io"
"net"
"reflect"
"strconv"
"time"
"github.com/fatedier/golib/errors"
libio "github.com/fatedier/golib/io"
v1 "github.com/fatedier/frp/pkg/config/v1"
"github.com/fatedier/frp/pkg/msg"
"github.com/fatedier/frp/pkg/proto/udp"
"github.com/fatedier/frp/pkg/util/limit"
netpkg "github.com/fatedier/frp/pkg/util/net"
"github.com/fatedier/frp/server/metrics"
)
func init() {
RegisterProxyFactory(reflect.TypeFor[*v1.UDPProxyConfig](), NewUDPProxy)
}
type UDPProxy struct {
*BaseProxy
cfg *v1.UDPProxyConfig
realBindPort int
// udpConn is the listener of udp packages
udpConn *net.UDPConn
// there are always only one workConn at the same time
// get another one if it closed
workConn net.Conn
// sendCh is used for sending packages to workConn
sendCh chan *msg.UDPPacket
// readCh is used for reading packages from workConn
readCh chan *msg.UDPPacket
// checkCloseCh is used for watching if workConn is closed
checkCloseCh chan int
isClosed bool
}
func NewUDPProxy(baseProxy *BaseProxy) Proxy {
unwrapped, ok := baseProxy.GetConfigurer().(*v1.UDPProxyConfig)
if !ok {
return nil
}
baseProxy.usedPortsNum = 1
return &UDPProxy{
BaseProxy: baseProxy,
cfg: unwrapped,
}
}
func (pxy *UDPProxy) Run() (remoteAddr string, err error) {
xl := pxy.xl
pxy.realBindPort, err = pxy.rc.UDPPortManager.Acquire(pxy.name, pxy.cfg.RemotePort)
if err != nil {
return "", fmt.Errorf("acquire port %d error: %v", pxy.cfg.RemotePort, err)
}
defer func() {
if err != nil {
pxy.rc.UDPPortManager.Release(pxy.realBindPort)
}
}()
remoteAddr = fmt.Sprintf(":%d", pxy.realBindPort)
pxy.cfg.RemotePort = pxy.realBindPort
addr, errRet := net.ResolveUDPAddr("udp", net.JoinHostPort(pxy.serverCfg.ProxyBindAddr, strconv.Itoa(pxy.realBindPort)))
if errRet != nil {
err = errRet
return
}
udpConn, errRet := net.ListenUDP("udp", addr)
if errRet != nil {
err = errRet
xl.Warnf("listen udp port error: %v", err)
return
}
xl.Infof("udp proxy listen port [%d]", pxy.cfg.RemotePort)
pxy.udpConn = udpConn
pxy.sendCh = make(chan *msg.UDPPacket, 1024)
pxy.readCh = make(chan *msg.UDPPacket, 1024)
pxy.checkCloseCh = make(chan int)
// read message from workConn, if it returns any error, notify proxy to start a new workConn
workConnReaderFn := func(payloadConn *msg.Conn) {
for {
var (
rawMsg msg.Message
errRet error
)
xl.Tracef("loop waiting message from udp workConn")
// client will send heartbeat in workConn for keeping alive
_ = payloadConn.SetReadDeadline(time.Now().Add(time.Duration(60) * time.Second))
if rawMsg, errRet = payloadConn.ReadMsg(); errRet != nil {
xl.Warnf("read from workConn for udp error: %v", errRet)
_ = payloadConn.Close()
// notify proxy to start a new work connection
// ignore error here, it means the proxy is closed
_ = errors.PanicToError(func() {
pxy.checkCloseCh <- 1
})
return
}
if err := payloadConn.SetReadDeadline(time.Time{}); err != nil {
xl.Warnf("set read deadline error: %v", err)
}
switch m := rawMsg.(type) {
case *msg.Ping:
xl.Tracef("udp work conn get ping message")
continue
case *msg.UDPPacket:
if errRet := errors.PanicToError(func() {
xl.Tracef("get udp message from workConn, len: %d", len(m.Content))
pxy.readCh <- m
metrics.Server.AddTrafficOut(
pxy.GetName(),
pxy.GetConfigurer().GetBaseConfig().Type,
int64(len(m.Content)),
)
}); errRet != nil {
_ = payloadConn.Close()
xl.Infof("reader goroutine for udp work connection closed")
return
}
}
}
}
// send message to workConn
workConnSenderFn := func(payloadConn *msg.Conn, ctx context.Context) {
var errRet error
for {
select {
case udpMsg, ok := <-pxy.sendCh:
if !ok {
xl.Infof("sender goroutine for udp work connection closed")
return
}
if errRet = payloadConn.WriteMsg(udpMsg); errRet != nil {
xl.Infof("sender goroutine for udp work connection closed: %v", errRet)
_ = payloadConn.Close()
return
}
xl.Tracef("send message to udp workConn, len: %d", len(udpMsg.Content))
metrics.Server.AddTrafficIn(
pxy.GetName(),
pxy.GetConfigurer().GetBaseConfig().Type,
int64(len(udpMsg.Content)),
)
continue
case <-ctx.Done():
xl.Infof("sender goroutine for udp work connection closed")
return
}
}
}
go func() {
// Sleep a while for waiting control send the NewProxyResp to client.
time.Sleep(500 * time.Millisecond)
for {
workConn, err := pxy.GetWorkConnFromPool(nil, nil)
if err != nil {
time.Sleep(1 * time.Second)
// check if proxy is closed
select {
case _, ok := <-pxy.checkCloseCh:
if !ok {
return
}
default:
}
continue
}
// close the old workConn and replace it with a new one
if pxy.workConn != nil {
pxy.workConn.Close()
}
var rwc io.ReadWriteCloser = workConn
if pxy.cfg.Transport.UseEncryption {
rwc, err = libio.WithEncryption(rwc, pxy.encryptionKey)
if err != nil {
xl.Errorf("create encryption stream error: %v", err)
workConn.Close()
continue
}
}
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()), rwc.Close)
}
pxy.workConn = netpkg.WrapReadWriteCloserToConn(rwc, workConn)
// Plain UDP payload follows the negotiated wire protocol for message framing.
payloadRW, err := msg.NewUDPPacketReadWriter(pxy.workConn, pxy.wireProtocol, pxy.udpPacketCodec)
if err != nil {
xl.Errorf("create UDP packet read writer: %v", err)
pxy.workConn.Close()
continue
}
payloadConn := msg.NewConn(pxy.workConn, payloadRW)
ctx, cancel := context.WithCancel(context.Background())
go workConnReaderFn(payloadConn)
go workConnSenderFn(payloadConn, ctx)
_, ok := <-pxy.checkCloseCh
cancel()
if !ok {
return
}
}
}()
// Read from user connections and send wrapped udp message to sendCh (forwarded by workConn).
// Client will transfor udp message to local udp service and waiting for response for a while.
// Response will be wrapped to be forwarded by work connection to server.
// Close readCh and sendCh at the end.
go func() {
udp.ForwardUserConn(udpConn, pxy.readCh, pxy.sendCh, int(pxy.serverCfg.UDPPacketSize))
pxy.Close()
}()
return remoteAddr, nil
}
func (pxy *UDPProxy) Close() {
pxy.mu.Lock()
defer pxy.mu.Unlock()
if !pxy.isClosed {
pxy.isClosed = true
pxy.BaseProxy.Close()
if pxy.workConn != nil {
pxy.workConn.Close()
}
pxy.udpConn.Close()
// all channels only closed here
close(pxy.checkCloseCh)
close(pxy.readCh)
close(pxy.sendCh)
}
pxy.rc.UDPPortManager.Release(pxy.realBindPort)
}