Compare commits

...

6 Commits

Author SHA1 Message Date
fatedier
c23894f156 fix: validate CA cert parsing and add missing ReadHeaderTimeout (#5205)
- pkg/transport/tls.go: check AppendCertsFromPEM return value and
  return clear error when CA file contains no valid PEM certificates
- pkg/plugin/client/http2http.go: set ReadHeaderTimeout to 60s to
  match other plugins and prevent slow header attacks
- pkg/plugin/client/http2https.go: same ReadHeaderTimeout fix
2026-03-06 17:59:41 +08:00
fatedier
cb459b02b6 fix: WebsocketListener nil panic and OIDC auth data race (#5204)
- pkg/util/net/websocket.go: store ln parameter in struct to prevent
  nil pointer panic when Addr() is called
- pkg/auth/oidc.go: replace unsynchronized []string with map + RWMutex
  for subjectsFromLogin to fix data race across concurrent connections
2026-03-06 16:51:52 +08:00
fatedier
8f633fe363 fix: return buffers to pool on error paths to reduce GC pressure (#5203)
- pkg/nathole/nathole.go: add pool.PutBuf(buf) on ReadFromUDP error
  and DecodeMessageInto error paths in waitDetectMessage
- pkg/proto/udp/udp.go: add defer pool.PutBuf(buf) in writerFn to
  ensure buffer is returned when the goroutine exits
2026-03-06 15:55:22 +08:00
fatedier
c62a1da161 fix: close connections on error paths to prevent resource leaks (#5202)
Fix connection leaks in multiple error paths across client and server:
- server/proxy/http: close tmpConn when WithEncryption fails
- client/proxy: close localConn when ProxyProtocol WriteTo fails
- client/visitor/sudp: close visitorConn on all error paths in getNewVisitorConn
- client/visitor/xtcp: close tunnelConn when WithEncryption fails
- client/visitor/xtcp: close lConn when NewKCPConnFromUDP fails
- pkg/plugin/client/unix_domain_socket: close localConn and connInfo.Conn when WriteTo fails, close connInfo.Conn when DialUnix fails
- pkg/plugin/client/tls2raw: close tlsConn when Handshake or Dial fails
2026-03-06 15:18:38 +08:00
fatedier
f22f7d539c server/group: fix port leak and incorrect Listen port in TCPGroup (#5200)
Fix two bugs in TCPGroup.Listen():
- Release acquired port when net.Listen fails to prevent port leak
- Use realPort instead of port for net.Listen to ensure consistency
  between port manager records and actual listening port
2026-03-06 02:25:47 +08:00
fatedier
462c987f6d pkg/msg: change UDPPacket.Content from string to []byte to avoid redundant base64 encode/decode (#5198) 2026-03-06 01:38:24 +08:00
17 changed files with 52 additions and 25 deletions

View File

@@ -209,6 +209,7 @@ func (pxy *BaseProxy) HandleTCPWorkConnection(workConn net.Conn, m *msg.StartWor
if connInfo.ProxyProtocolHeader != nil { if connInfo.ProxyProtocolHeader != nil {
if _, err := connInfo.ProxyProtocolHeader.WriteTo(localConn); err != nil { if _, err := connInfo.ProxyProtocolHeader.WriteTo(localConn); err != nil {
workConn.Close() workConn.Close()
localConn.Close()
xl.Errorf("write proxy protocol header to local conn error: %v", err) xl.Errorf("write proxy protocol header to local conn error: %v", err)
return return
} }

View File

@@ -129,7 +129,7 @@ func (pxy *UDPProxy) InWorkConn(conn net.Conn, _ *msg.StartWorkConn) {
return return
} }
if errRet := errors.PanicToError(func() { if errRet := errors.PanicToError(func() {
xl.Tracef("get udp package from workConn: %s", udpMsg.Content) xl.Tracef("get udp package from workConn, len: %d", len(udpMsg.Content))
readCh <- &udpMsg readCh <- &udpMsg
}); errRet != nil { }); errRet != nil {
xl.Infof("reader goroutine for udp work connection closed: %v", errRet) xl.Infof("reader goroutine for udp work connection closed: %v", errRet)
@@ -145,7 +145,7 @@ func (pxy *UDPProxy) InWorkConn(conn net.Conn, _ *msg.StartWorkConn) {
for rawMsg := range sendCh { for rawMsg := range sendCh {
switch m := rawMsg.(type) { switch m := rawMsg.(type) {
case *msg.UDPPacket: case *msg.UDPPacket:
xl.Tracef("send udp package to workConn: %s", m.Content) xl.Tracef("send udp package to workConn, len: %d", len(m.Content))
case *msg.Ping: case *msg.Ping:
xl.Tracef("send ping message to udp workConn") xl.Tracef("send ping message to udp workConn")
} }

View File

@@ -147,7 +147,7 @@ func (sv *SUDPVisitor) worker(workConn net.Conn, firstPacket *msg.UDPPacket) {
case *msg.UDPPacket: case *msg.UDPPacket:
if errRet := errors.PanicToError(func() { if errRet := errors.PanicToError(func() {
sv.readCh <- m sv.readCh <- m
xl.Tracef("frpc visitor get udp packet from workConn: %s", m.Content) xl.Tracef("frpc visitor get udp packet from workConn, len: %d", len(m.Content))
}); errRet != nil { }); errRet != nil {
xl.Infof("reader goroutine for udp work connection closed") xl.Infof("reader goroutine for udp work connection closed")
return return
@@ -169,7 +169,7 @@ func (sv *SUDPVisitor) worker(workConn net.Conn, firstPacket *msg.UDPPacket) {
xl.Warnf("sender goroutine for udp work connection closed: %v", errRet) xl.Warnf("sender goroutine for udp work connection closed: %v", errRet)
return return
} }
xl.Tracef("send udp package to workConn: %s", firstPacket.Content) xl.Tracef("send udp package to workConn, len: %d", len(firstPacket.Content))
} }
for { for {
@@ -184,7 +184,7 @@ func (sv *SUDPVisitor) worker(workConn net.Conn, firstPacket *msg.UDPPacket) {
xl.Warnf("sender goroutine for udp work connection closed: %v", errRet) xl.Warnf("sender goroutine for udp work connection closed: %v", errRet)
return return
} }
xl.Tracef("send udp package to workConn: %s", udpMsg.Content) xl.Tracef("send udp package to workConn, len: %d", len(udpMsg.Content))
case <-closeCh: case <-closeCh:
return return
} }
@@ -217,6 +217,7 @@ func (sv *SUDPVisitor) getNewVisitorConn() (net.Conn, error) {
} }
err = msg.WriteMsg(visitorConn, newVisitorConnMsg) err = msg.WriteMsg(visitorConn, newVisitorConnMsg)
if err != nil { if err != nil {
visitorConn.Close()
return nil, fmt.Errorf("frpc send newVisitorConnMsg to frps error: %v", err) return nil, fmt.Errorf("frpc send newVisitorConnMsg to frps error: %v", err)
} }
@@ -224,11 +225,13 @@ func (sv *SUDPVisitor) getNewVisitorConn() (net.Conn, error) {
_ = visitorConn.SetReadDeadline(time.Now().Add(10 * time.Second)) _ = visitorConn.SetReadDeadline(time.Now().Add(10 * time.Second))
err = msg.ReadMsgInto(visitorConn, &newVisitorConnRespMsg) err = msg.ReadMsgInto(visitorConn, &newVisitorConnRespMsg)
if err != nil { if err != nil {
visitorConn.Close()
return nil, fmt.Errorf("frpc read newVisitorConnRespMsg error: %v", err) return nil, fmt.Errorf("frpc read newVisitorConnRespMsg error: %v", err)
} }
_ = visitorConn.SetReadDeadline(time.Time{}) _ = visitorConn.SetReadDeadline(time.Time{})
if newVisitorConnRespMsg.Error != "" { if newVisitorConnRespMsg.Error != "" {
visitorConn.Close()
return nil, fmt.Errorf("start new visitor connection error: %s", newVisitorConnRespMsg.Error) return nil, fmt.Errorf("start new visitor connection error: %s", newVisitorConnRespMsg.Error)
} }
@@ -238,6 +241,7 @@ func (sv *SUDPVisitor) getNewVisitorConn() (net.Conn, error) {
remote, err = libio.WithEncryption(remote, []byte(sv.cfg.SecretKey)) remote, err = libio.WithEncryption(remote, []byte(sv.cfg.SecretKey))
if err != nil { if err != nil {
xl.Errorf("create encryption stream error: %v", err) xl.Errorf("create encryption stream error: %v", err)
visitorConn.Close()
return nil, err return nil, err
} }
} }

View File

@@ -211,6 +211,7 @@ func (sv *XTCPVisitor) handleConn(userConn net.Conn) {
muxConnRWCloser, err = libio.WithEncryption(muxConnRWCloser, []byte(sv.cfg.SecretKey)) muxConnRWCloser, err = libio.WithEncryption(muxConnRWCloser, []byte(sv.cfg.SecretKey))
if err != nil { if err != nil {
xl.Errorf("create encryption stream error: %v", err) xl.Errorf("create encryption stream error: %v", err)
tunnelConn.Close()
tunnelErr = err tunnelErr = err
return return
} }
@@ -373,6 +374,7 @@ func (ks *KCPTunnelSession) Init(listenConn *net.UDPConn, raddr *net.UDPAddr) er
} }
remote, err := netpkg.NewKCPConnFromUDP(lConn, true, raddr.String()) remote, err := netpkg.NewKCPConnFromUDP(lConn, true, raddr.String())
if err != nil { if err != nil {
lConn.Close()
return fmt.Errorf("create kcp connection from udp connection error: %v", err) return fmt.Errorf("create kcp connection from udp connection error: %v", err)
} }

View File

@@ -23,6 +23,7 @@ import (
"net/url" "net/url"
"os" "os"
"slices" "slices"
"sync"
"github.com/coreos/go-oidc/v3/oidc" "github.com/coreos/go-oidc/v3/oidc"
"golang.org/x/oauth2" "golang.org/x/oauth2"
@@ -205,7 +206,8 @@ type OidcAuthConsumer struct {
additionalAuthScopes []v1.AuthScope additionalAuthScopes []v1.AuthScope
verifier TokenVerifier verifier TokenVerifier
subjectsFromLogin []string mu sync.RWMutex
subjectsFromLogin map[string]struct{}
} }
func NewTokenVerifier(cfg v1.AuthOIDCServerConfig) TokenVerifier { func NewTokenVerifier(cfg v1.AuthOIDCServerConfig) TokenVerifier {
@@ -226,7 +228,7 @@ func NewOidcAuthVerifier(additionalAuthScopes []v1.AuthScope, verifier TokenVeri
return &OidcAuthConsumer{ return &OidcAuthConsumer{
additionalAuthScopes: additionalAuthScopes, additionalAuthScopes: additionalAuthScopes,
verifier: verifier, verifier: verifier,
subjectsFromLogin: []string{}, subjectsFromLogin: make(map[string]struct{}),
} }
} }
@@ -235,9 +237,9 @@ func (auth *OidcAuthConsumer) VerifyLogin(loginMsg *msg.Login) (err error) {
if err != nil { if err != nil {
return fmt.Errorf("invalid OIDC token in login: %v", err) return fmt.Errorf("invalid OIDC token in login: %v", err)
} }
if !slices.Contains(auth.subjectsFromLogin, token.Subject) { auth.mu.Lock()
auth.subjectsFromLogin = append(auth.subjectsFromLogin, token.Subject) auth.subjectsFromLogin[token.Subject] = struct{}{}
} auth.mu.Unlock()
return nil return nil
} }
@@ -246,11 +248,13 @@ func (auth *OidcAuthConsumer) verifyPostLoginToken(privilegeKey string) (err err
if err != nil { if err != nil {
return fmt.Errorf("invalid OIDC token in ping: %v", err) return fmt.Errorf("invalid OIDC token in ping: %v", err)
} }
if !slices.Contains(auth.subjectsFromLogin, token.Subject) { auth.mu.RLock()
_, ok := auth.subjectsFromLogin[token.Subject]
auth.mu.RUnlock()
if !ok {
return fmt.Errorf("received different OIDC subject in login and ping. "+ return fmt.Errorf("received different OIDC subject in login and ping. "+
"original subjects: %s, "+
"new subject: %s", "new subject: %s",
auth.subjectsFromLogin, token.Subject) token.Subject)
} }
return nil return nil
} }

View File

@@ -184,7 +184,7 @@ type Pong struct {
} }
type UDPPacket struct { type UDPPacket struct {
Content string `json:"c,omitempty"` Content []byte `json:"c,omitempty"`
LocalAddr *net.UDPAddr `json:"l,omitempty"` LocalAddr *net.UDPAddr `json:"l,omitempty"`
RemoteAddr *net.UDPAddr `json:"r,omitempty"` RemoteAddr *net.UDPAddr `json:"r,omitempty"`
} }

View File

@@ -298,11 +298,13 @@ func waitDetectMessage(
n, raddr, err := conn.ReadFromUDP(buf) n, raddr, err := conn.ReadFromUDP(buf)
_ = conn.SetReadDeadline(time.Time{}) _ = conn.SetReadDeadline(time.Time{})
if err != nil { if err != nil {
pool.PutBuf(buf)
return nil, err return nil, err
} }
xl.Debugf("get udp message local %s, from %s", conn.LocalAddr(), raddr) xl.Debugf("get udp message local %s, from %s", conn.LocalAddr(), raddr)
var m msg.NatHoleSid var m msg.NatHoleSid
if err := DecodeMessageInto(buf[:n], key, &m); err != nil { if err := DecodeMessageInto(buf[:n], key, &m); err != nil {
pool.PutBuf(buf)
xl.Warnf("decode sid message error: %v", err) xl.Warnf("decode sid message error: %v", err)
continue continue
} }

View File

@@ -21,6 +21,7 @@ import (
stdlog "log" stdlog "log"
"net/http" "net/http"
"net/http/httputil" "net/http/httputil"
"time"
"github.com/fatedier/golib/pool" "github.com/fatedier/golib/pool"
@@ -68,7 +69,7 @@ func NewHTTP2HTTPPlugin(_ PluginContext, options v1.ClientPluginOptions) (Plugin
p.s = &http.Server{ p.s = &http.Server{
Handler: rp, Handler: rp,
ReadHeaderTimeout: 0, ReadHeaderTimeout: 60 * time.Second,
} }
go func() { go func() {

View File

@@ -22,6 +22,7 @@ import (
stdlog "log" stdlog "log"
"net/http" "net/http"
"net/http/httputil" "net/http/httputil"
"time"
"github.com/fatedier/golib/pool" "github.com/fatedier/golib/pool"
@@ -77,7 +78,7 @@ func NewHTTP2HTTPSPlugin(_ PluginContext, options v1.ClientPluginOptions) (Plugi
p.s = &http.Server{ p.s = &http.Server{
Handler: rp, Handler: rp,
ReadHeaderTimeout: 0, ReadHeaderTimeout: 60 * time.Second,
} }
go func() { go func() {

View File

@@ -62,11 +62,13 @@ func (p *TLS2RawPlugin) Handle(ctx context.Context, connInfo *ConnectionInfo) {
if err := tlsConn.Handshake(); err != nil { if err := tlsConn.Handshake(); err != nil {
xl.Warnf("tls handshake error: %v", err) xl.Warnf("tls handshake error: %v", err)
tlsConn.Close()
return return
} }
rawConn, err := net.Dial("tcp", p.opts.LocalAddr) rawConn, err := net.Dial("tcp", p.opts.LocalAddr)
if err != nil { if err != nil {
xl.Warnf("dial to local addr error: %v", err) xl.Warnf("dial to local addr error: %v", err)
tlsConn.Close()
return return
} }

View File

@@ -54,10 +54,13 @@ func (uds *UnixDomainSocketPlugin) Handle(ctx context.Context, connInfo *Connect
localConn, err := net.DialUnix("unix", nil, uds.UnixAddr) localConn, err := net.DialUnix("unix", nil, uds.UnixAddr)
if err != nil { if err != nil {
xl.Warnf("dial to uds %s error: %v", uds.UnixAddr, err) xl.Warnf("dial to uds %s error: %v", uds.UnixAddr, err)
connInfo.Conn.Close()
return return
} }
if connInfo.ProxyProtocolHeader != nil { if connInfo.ProxyProtocolHeader != nil {
if _, err := connInfo.ProxyProtocolHeader.WriteTo(localConn); err != nil { if _, err := connInfo.ProxyProtocolHeader.WriteTo(localConn); err != nil {
localConn.Close()
connInfo.Conn.Close()
return return
} }
} }

View File

@@ -15,7 +15,6 @@
package udp package udp
import ( import (
"encoding/base64"
"net" "net"
"sync" "sync"
"time" "time"
@@ -28,16 +27,17 @@ import (
) )
func NewUDPPacket(buf []byte, laddr, raddr *net.UDPAddr) *msg.UDPPacket { func NewUDPPacket(buf []byte, laddr, raddr *net.UDPAddr) *msg.UDPPacket {
content := make([]byte, len(buf))
copy(content, buf)
return &msg.UDPPacket{ return &msg.UDPPacket{
Content: base64.StdEncoding.EncodeToString(buf), Content: content,
LocalAddr: laddr, LocalAddr: laddr,
RemoteAddr: raddr, RemoteAddr: raddr,
} }
} }
func GetContent(m *msg.UDPPacket) (buf []byte, err error) { func GetContent(m *msg.UDPPacket) (buf []byte, err error) {
buf, err = base64.StdEncoding.DecodeString(m.Content) return m.Content, nil
return
} }
func ForwardUserConn(udpConn *net.UDPConn, readCh <-chan *msg.UDPPacket, sendCh chan<- *msg.UDPPacket, bufSize int) { func ForwardUserConn(udpConn *net.UDPConn, readCh <-chan *msg.UDPPacket, sendCh chan<- *msg.UDPPacket, bufSize int) {
@@ -60,7 +60,7 @@ func ForwardUserConn(udpConn *net.UDPConn, readCh <-chan *msg.UDPPacket, sendCh
if err != nil { if err != nil {
return return
} }
// buf[:n] will be encoded to string, so the bytes can be reused // NewUDPPacket copies buf[:n], so the read buffer can be reused
udpMsg := NewUDPPacket(buf[:n], nil, remoteAddr) udpMsg := NewUDPPacket(buf[:n], nil, remoteAddr)
select { select {
@@ -85,6 +85,7 @@ func Forwarder(dstAddr *net.UDPAddr, readCh <-chan *msg.UDPPacket, sendCh chan<-
}() }()
buf := pool.GetBuf(bufSize) buf := pool.GetBuf(bufSize)
defer pool.PutBuf(buf)
for { for {
_ = udpConn.SetReadDeadline(time.Now().Add(30 * time.Second)) _ = udpConn.SetReadDeadline(time.Now().Add(30 * time.Second))
n, _, err := udpConn.ReadFromUDP(buf) n, _, err := udpConn.ReadFromUDP(buf)

View File

@@ -20,6 +20,7 @@ import (
"crypto/tls" "crypto/tls"
"crypto/x509" "crypto/x509"
"encoding/pem" "encoding/pem"
"fmt"
"math/big" "math/big"
"os" "os"
"time" "time"
@@ -85,7 +86,9 @@ func newCertPool(caPath string) (*x509.CertPool, error) {
return nil, err return nil, err
} }
pool.AppendCertsFromPEM(caCrt) if !pool.AppendCertsFromPEM(caCrt) {
return nil, fmt.Errorf("failed to parse CA certificate from file %q: no valid PEM certificates found", caPath)
}
return pool, nil return pool, nil
} }

View File

@@ -26,6 +26,7 @@ type WebsocketListener struct {
// ln: tcp listener for websocket connections // ln: tcp listener for websocket connections
func NewWebsocketListener(ln net.Listener) (wl *WebsocketListener) { func NewWebsocketListener(ln net.Listener) (wl *WebsocketListener) {
wl = &WebsocketListener{ wl = &WebsocketListener{
ln: ln,
acceptCh: make(chan net.Conn), acceptCh: make(chan net.Conn),
} }

View File

@@ -100,8 +100,9 @@ func (tg *TCPGroup) Listen(proxyName string, group string, groupKey string, addr
if err != nil { if err != nil {
return return
} }
tcpLn, errRet := net.Listen("tcp", net.JoinHostPort(addr, strconv.Itoa(port))) tcpLn, errRet := net.Listen("tcp", net.JoinHostPort(addr, strconv.Itoa(realPort)))
if errRet != nil { if errRet != nil {
tg.ctl.portManager.Release(realPort)
err = errRet err = errRet
return return
} }

View File

@@ -168,6 +168,7 @@ func (pxy *HTTPProxy) GetRealConn(remoteAddr string) (workConn net.Conn, err err
rwc, err = libio.WithEncryption(rwc, pxy.encryptionKey) rwc, err = libio.WithEncryption(rwc, pxy.encryptionKey)
if err != nil { if err != nil {
xl.Errorf("create encryption stream error: %v", err) xl.Errorf("create encryption stream error: %v", err)
tmpConn.Close()
return return
} }
} }

View File

@@ -136,7 +136,7 @@ func (pxy *UDPProxy) Run() (remoteAddr string, err error) {
continue continue
case *msg.UDPPacket: case *msg.UDPPacket:
if errRet := errors.PanicToError(func() { if errRet := errors.PanicToError(func() {
xl.Tracef("get udp message from workConn: %s", m.Content) xl.Tracef("get udp message from workConn, len: %d", len(m.Content))
pxy.readCh <- m pxy.readCh <- m
metrics.Server.AddTrafficOut( metrics.Server.AddTrafficOut(
pxy.GetName(), pxy.GetName(),
@@ -167,7 +167,7 @@ func (pxy *UDPProxy) Run() (remoteAddr string, err error) {
conn.Close() conn.Close()
return return
} }
xl.Tracef("send message to udp workConn: %s", udpMsg.Content) xl.Tracef("send message to udp workConn, len: %d", len(udpMsg.Content))
metrics.Server.AddTrafficIn( metrics.Server.AddTrafficIn(
pxy.GetName(), pxy.GetName(),
pxy.GetConfigurer().GetBaseConfig().Type, pxy.GetConfigurer().GetBaseConfig().Type,