Browse Source

support protocol quic between frpc and frps (#3198)

fatedier 2 years ago
parent
commit
2f66dc3e99

+ 8 - 80
client/control.go

@@ -16,24 +16,18 @@ package client
 
 import (
 	"context"
-	"crypto/tls"
 	"io"
 	"net"
 	"runtime/debug"
-	"strconv"
 	"time"
 
 	"github.com/fatedier/golib/control/shutdown"
 	"github.com/fatedier/golib/crypto"
-	libdial "github.com/fatedier/golib/net/dial"
-	fmux "github.com/hashicorp/yamux"
 
 	"github.com/fatedier/frp/client/proxy"
 	"github.com/fatedier/frp/pkg/auth"
 	"github.com/fatedier/frp/pkg/config"
 	"github.com/fatedier/frp/pkg/msg"
-	"github.com/fatedier/frp/pkg/transport"
-	frpNet "github.com/fatedier/frp/pkg/util/net"
 	"github.com/fatedier/frp/pkg/util/xlog"
 )
 
@@ -51,8 +45,7 @@ type Control struct {
 	// control connection
 	conn net.Conn
 
-	// tcp stream multiplexing, if enabled
-	session *fmux.Session
+	cm *ConnectionManager
 
 	// put a message in this channel to send it over control connection to server
 	sendCh chan (msg.Message)
@@ -87,7 +80,8 @@ type Control struct {
 	authSetter auth.Setter
 }
 
-func NewControl(ctx context.Context, runID string, conn net.Conn, session *fmux.Session,
+func NewControl(
+	ctx context.Context, runID string, conn net.Conn, cm *ConnectionManager,
 	clientCfg config.ClientCommonConf,
 	pxyCfgs map[string]config.ProxyConf,
 	visitorCfgs map[string]config.VisitorConf,
@@ -98,7 +92,7 @@ func NewControl(ctx context.Context, runID string, conn net.Conn, session *fmux.
 	ctl := &Control{
 		runID:              runID,
 		conn:               conn,
-		session:            session,
+		cm:                 cm,
 		pxyCfgs:            pxyCfgs,
 		sendCh:             make(chan msg.Message, 100),
 		readCh:             make(chan msg.Message, 100),
@@ -134,6 +128,7 @@ func (ctl *Control) HandleReqWorkConn(inMsg *msg.ReqWorkConn) {
 	xl := ctl.xl
 	workConn, err := ctl.connectServer()
 	if err != nil {
+		xl.Warn("start new connection to server error: %v", err)
 		return
 	}
 
@@ -189,9 +184,7 @@ func (ctl *Control) GracefulClose(d time.Duration) error {
 	time.Sleep(d)
 
 	ctl.conn.Close()
-	if ctl.session != nil {
-		ctl.session.Close()
-	}
+	ctl.cm.Close()
 	return nil
 }
 
@@ -202,70 +195,7 @@ func (ctl *Control) ClosedDoneCh() <-chan struct{} {
 
 // connectServer return a new connection to frps
 func (ctl *Control) connectServer() (conn net.Conn, err error) {
-	xl := ctl.xl
-	if ctl.clientCfg.TCPMux {
-		stream, errRet := ctl.session.OpenStream()
-		if errRet != nil {
-			err = errRet
-			xl.Warn("start new connection to server error: %v", err)
-			return
-		}
-		conn = stream
-	} else {
-		var tlsConfig *tls.Config
-		sn := ctl.clientCfg.TLSServerName
-		if sn == "" {
-			sn = ctl.clientCfg.ServerAddr
-		}
-
-		if ctl.clientCfg.TLSEnable {
-			tlsConfig, err = transport.NewClientTLSConfig(
-				ctl.clientCfg.TLSCertFile,
-				ctl.clientCfg.TLSKeyFile,
-				ctl.clientCfg.TLSTrustedCaFile,
-				sn)
-
-			if err != nil {
-				xl.Warn("fail to build tls configuration when connecting to server, err: %v", err)
-				return
-			}
-		}
-
-		proxyType, addr, auth, err := libdial.ParseProxyURL(ctl.clientCfg.HTTPProxy)
-		if err != nil {
-			xl.Error("fail to parse proxy url")
-			return nil, err
-		}
-		dialOptions := []libdial.DialOption{}
-		protocol := ctl.clientCfg.Protocol
-		if protocol == "websocket" {
-			protocol = "tcp"
-			dialOptions = append(dialOptions, libdial.WithAfterHook(libdial.AfterHook{Hook: frpNet.DialHookWebsocket()}))
-		}
-		if ctl.clientCfg.ConnectServerLocalIP != "" {
-			dialOptions = append(dialOptions, libdial.WithLocalAddr(ctl.clientCfg.ConnectServerLocalIP))
-		}
-		dialOptions = append(dialOptions,
-			libdial.WithProtocol(protocol),
-			libdial.WithTimeout(time.Duration(ctl.clientCfg.DialServerTimeout)*time.Second),
-			libdial.WithKeepAlive(time.Duration(ctl.clientCfg.DialServerKeepAlive)*time.Second),
-			libdial.WithProxy(proxyType, addr),
-			libdial.WithProxyAuth(auth),
-			libdial.WithTLSConfig(tlsConfig),
-			libdial.WithAfterHook(libdial.AfterHook{
-				Hook: frpNet.DialHookCustomTLSHeadByte(tlsConfig != nil, ctl.clientCfg.DisableCustomTLSFirstByte),
-			}),
-		)
-		conn, err = libdial.Dial(
-			net.JoinHostPort(ctl.clientCfg.ServerAddr, strconv.Itoa(ctl.clientCfg.ServerPort)),
-			dialOptions...,
-		)
-		if err != nil {
-			xl.Warn("start new connection to server error: %v", err)
-			return nil, err
-		}
-	}
-	return
+	return ctl.cm.Connect()
 }
 
 // reader read all messages from frps and send to readCh
@@ -409,9 +339,7 @@ func (ctl *Control) worker() {
 	ctl.vm.Close()
 
 	close(ctl.closedDoneCh)
-	if ctl.session != nil {
-		ctl.session.Close()
-	}
+	ctl.cm.Close()
 }
 
 func (ctl *Control) ReloadConf(pxyCfgs map[string]config.ProxyConf, visitorCfgs map[string]config.VisitorConf) error {

+ 165 - 72
client/service.go

@@ -31,6 +31,7 @@ import (
 	"github.com/fatedier/golib/crypto"
 	libdial "github.com/fatedier/golib/net/dial"
 	fmux "github.com/hashicorp/yamux"
+	quic "github.com/lucas-clemente/quic-go"
 
 	"github.com/fatedier/frp/assets"
 	"github.com/fatedier/frp/pkg/auth"
@@ -127,7 +128,7 @@ func (svr *Service) Run() error {
 
 	// login to frps
 	for {
-		conn, session, err := svr.login()
+		conn, cm, err := svr.login()
 		if err != nil {
 			xl.Warn("login to server failed: %v", err)
 
@@ -139,7 +140,7 @@ func (svr *Service) Run() error {
 			util.RandomSleep(10*time.Second, 0.9, 1.1)
 		} else {
 			// login success
-			ctl := NewControl(svr.ctx, svr.runID, conn, session, svr.cfg, svr.pxyCfgs, svr.visitorCfgs, svr.serverUDPPort, svr.authSetter)
+			ctl := NewControl(svr.ctx, svr.runID, conn, cm, svr.cfg, svr.pxyCfgs, svr.visitorCfgs, svr.serverUDPPort, svr.authSetter)
 			ctl.Run()
 			svr.ctlMu.Lock()
 			svr.ctl = ctl
@@ -207,7 +208,7 @@ func (svr *Service) keepControllerWorking() {
 			}
 
 			xl.Info("try to reconnect to server...")
-			conn, session, err := svr.login()
+			conn, cm, err := svr.login()
 			if err != nil {
 				xl.Warn("reconnect to server error: %v, wait %v for another retry", err, delayTime)
 				util.RandomSleep(delayTime, 0.9, 1.1)
@@ -221,7 +222,7 @@ func (svr *Service) keepControllerWorking() {
 			// reconnect success, init delayTime
 			delayTime = time.Second
 
-			ctl := NewControl(svr.ctx, svr.runID, conn, session, svr.cfg, svr.pxyCfgs, svr.visitorCfgs, svr.serverUDPPort, svr.authSetter)
+			ctl := NewControl(svr.ctx, svr.runID, conn, cm, svr.cfg, svr.pxyCfgs, svr.visitorCfgs, svr.serverUDPPort, svr.authSetter)
 			ctl.Run()
 			svr.ctlMu.Lock()
 			if svr.ctl != nil {
@@ -237,83 +238,23 @@ func (svr *Service) keepControllerWorking() {
 // login creates a connection to frps and registers it self as a client
 // conn: control connection
 // session: if it's not nil, using tcp mux
-func (svr *Service) login() (conn net.Conn, session *fmux.Session, err error) {
+func (svr *Service) login() (conn net.Conn, cm *ConnectionManager, err error) {
 	xl := xlog.FromContextSafe(svr.ctx)
-	var tlsConfig *tls.Config
-	if svr.cfg.TLSEnable {
-		sn := svr.cfg.TLSServerName
-		if sn == "" {
-			sn = svr.cfg.ServerAddr
-		}
+	cm = NewConnectionManager(svr.ctx, &svr.cfg)
 
-		tlsConfig, err = transport.NewClientTLSConfig(
-			svr.cfg.TLSCertFile,
-			svr.cfg.TLSKeyFile,
-			svr.cfg.TLSTrustedCaFile,
-			sn)
-		if err != nil {
-			xl.Warn("fail to build tls configuration when service login, err: %v", err)
-			return
-		}
-	}
-
-	proxyType, addr, auth, err := libdial.ParseProxyURL(svr.cfg.HTTPProxy)
-	if err != nil {
-		xl.Error("fail to parse proxy url")
-		return
-	}
-	dialOptions := []libdial.DialOption{}
-	protocol := svr.cfg.Protocol
-	if protocol == "websocket" {
-		protocol = "tcp"
-		dialOptions = append(dialOptions, libdial.WithAfterHook(libdial.AfterHook{Hook: frpNet.DialHookWebsocket()}))
-	}
-	if svr.cfg.ConnectServerLocalIP != "" {
-		dialOptions = append(dialOptions, libdial.WithLocalAddr(svr.cfg.ConnectServerLocalIP))
-	}
-	dialOptions = append(dialOptions,
-		libdial.WithProtocol(protocol),
-		libdial.WithTimeout(time.Duration(svr.cfg.DialServerTimeout)*time.Second),
-		libdial.WithKeepAlive(time.Duration(svr.cfg.DialServerKeepAlive)*time.Second),
-		libdial.WithProxy(proxyType, addr),
-		libdial.WithProxyAuth(auth),
-		libdial.WithTLSConfig(tlsConfig),
-		libdial.WithAfterHook(libdial.AfterHook{
-			Hook: frpNet.DialHookCustomTLSHeadByte(tlsConfig != nil, svr.cfg.DisableCustomTLSFirstByte),
-		}),
-	)
-	conn, err = libdial.Dial(
-		net.JoinHostPort(svr.cfg.ServerAddr, strconv.Itoa(svr.cfg.ServerPort)),
-		dialOptions...,
-	)
-	if err != nil {
-		return
+	if err = cm.OpenConnection(); err != nil {
+		return nil, nil, err
 	}
 
 	defer func() {
 		if err != nil {
-			conn.Close()
-			if session != nil {
-				session.Close()
-			}
+			cm.Close()
 		}
 	}()
 
-	if svr.cfg.TCPMux {
-		fmuxCfg := fmux.DefaultConfig()
-		fmuxCfg.KeepAliveInterval = time.Duration(svr.cfg.TCPMuxKeepaliveInterval) * time.Second
-		fmuxCfg.LogOutput = io.Discard
-		session, err = fmux.Client(conn, fmuxCfg)
-		if err != nil {
-			return
-		}
-		stream, errRet := session.OpenStream()
-		if errRet != nil {
-			session.Close()
-			err = errRet
-			return
-		}
-		conn = stream
+	conn, err = cm.Connect()
+	if err != nil {
+		return
 	}
 
 	loginMsg := &msg.Login{
@@ -389,3 +330,155 @@ func (svr *Service) GracefulClose(d time.Duration) {
 
 	svr.cancel()
 }
+
+type ConnectionManager struct {
+	ctx context.Context
+	cfg *config.ClientCommonConf
+
+	muxSession *fmux.Session
+	quicConn   quic.Connection
+}
+
+func NewConnectionManager(ctx context.Context, cfg *config.ClientCommonConf) *ConnectionManager {
+	return &ConnectionManager{
+		ctx: ctx,
+		cfg: cfg,
+	}
+}
+
+func (cm *ConnectionManager) OpenConnection() error {
+	xl := xlog.FromContextSafe(cm.ctx)
+
+	// special for quic
+	if strings.EqualFold(cm.cfg.Protocol, "quic") {
+		var tlsConfig *tls.Config
+		var err error
+		sn := cm.cfg.TLSServerName
+		if sn == "" {
+			sn = cm.cfg.ServerAddr
+		}
+		if cm.cfg.TLSEnable {
+			tlsConfig, err = transport.NewClientTLSConfig(
+				cm.cfg.TLSCertFile,
+				cm.cfg.TLSKeyFile,
+				cm.cfg.TLSTrustedCaFile,
+				sn)
+		} else {
+			tlsConfig, err = transport.NewClientTLSConfig("", "", "", sn)
+		}
+		if err != nil {
+			xl.Warn("fail to build tls configuration, err: %v", err)
+			return err
+		}
+		tlsConfig.NextProtos = []string{"frp"}
+
+		conn, err := quic.DialAddr(
+			net.JoinHostPort(cm.cfg.ServerAddr, strconv.Itoa(cm.cfg.ServerPort)),
+			tlsConfig, nil)
+		if err != nil {
+			return err
+		}
+		cm.quicConn = conn
+		return nil
+	}
+
+	if !cm.cfg.TCPMux {
+		return nil
+	}
+
+	conn, err := cm.realConnect()
+	if err != nil {
+		return err
+	}
+
+	fmuxCfg := fmux.DefaultConfig()
+	fmuxCfg.KeepAliveInterval = time.Duration(cm.cfg.TCPMuxKeepaliveInterval) * time.Second
+	fmuxCfg.LogOutput = io.Discard
+	session, err := fmux.Client(conn, fmuxCfg)
+	if err != nil {
+		return err
+	}
+	cm.muxSession = session
+	return nil
+}
+
+func (cm *ConnectionManager) Connect() (net.Conn, error) {
+	if cm.quicConn != nil {
+		stream, err := cm.quicConn.OpenStreamSync(context.Background())
+		if err != nil {
+			return nil, err
+		}
+		return frpNet.QuicStreamToNetConn(stream, cm.quicConn), nil
+	} else if cm.muxSession != nil {
+		stream, err := cm.muxSession.OpenStream()
+		if err != nil {
+			return nil, err
+		}
+		return stream, nil
+	}
+
+	return cm.realConnect()
+}
+
+func (cm *ConnectionManager) realConnect() (net.Conn, error) {
+	xl := xlog.FromContextSafe(cm.ctx)
+	var tlsConfig *tls.Config
+	var err error
+	if cm.cfg.TLSEnable {
+		sn := cm.cfg.TLSServerName
+		if sn == "" {
+			sn = cm.cfg.ServerAddr
+		}
+
+		tlsConfig, err = transport.NewClientTLSConfig(
+			cm.cfg.TLSCertFile,
+			cm.cfg.TLSKeyFile,
+			cm.cfg.TLSTrustedCaFile,
+			sn)
+		if err != nil {
+			xl.Warn("fail to build tls configuration, err: %v", err)
+			return nil, err
+		}
+	}
+
+	proxyType, addr, auth, err := libdial.ParseProxyURL(cm.cfg.HTTPProxy)
+	if err != nil {
+		xl.Error("fail to parse proxy url")
+		return nil, err
+	}
+	dialOptions := []libdial.DialOption{}
+	protocol := cm.cfg.Protocol
+	if protocol == "websocket" {
+		protocol = "tcp"
+		dialOptions = append(dialOptions, libdial.WithAfterHook(libdial.AfterHook{Hook: frpNet.DialHookWebsocket()}))
+	}
+	if cm.cfg.ConnectServerLocalIP != "" {
+		dialOptions = append(dialOptions, libdial.WithLocalAddr(cm.cfg.ConnectServerLocalIP))
+	}
+	dialOptions = append(dialOptions,
+		libdial.WithProtocol(protocol),
+		libdial.WithTimeout(time.Duration(cm.cfg.DialServerTimeout)*time.Second),
+		libdial.WithKeepAlive(time.Duration(cm.cfg.DialServerKeepAlive)*time.Second),
+		libdial.WithProxy(proxyType, addr),
+		libdial.WithProxyAuth(auth),
+		libdial.WithTLSConfig(tlsConfig),
+		libdial.WithAfterHook(libdial.AfterHook{
+			Hook: frpNet.DialHookCustomTLSHeadByte(tlsConfig != nil, cm.cfg.DisableCustomTLSFirstByte),
+		}),
+	)
+	conn, err := libdial.Dial(
+		net.JoinHostPort(cm.cfg.ServerAddr, strconv.Itoa(cm.cfg.ServerPort)),
+		dialOptions...,
+	)
+	return conn, err
+}
+
+func (cm *ConnectionManager) Close() error {
+	if cm.quicConn != nil {
+		_ = cm.quicConn.CloseWithError(0, "")
+	}
+	if cm.muxSession != nil {
+		_ = cm.muxSession.Close()
+	}
+	return nil
+}

+ 1 - 1
conf/frpc_full.ini

@@ -87,7 +87,7 @@ user = your_name
 login_fail_exit = true
 
 # communication protocol used to connect to server
-# now it supports tcp, kcp and websocket, default is tcp
+# supports tcp, kcp, quic and websocket now, default is tcp
 protocol = tcp
 
 # set client binding ip when connect server, default is empty.

+ 6 - 2
conf/frps_full.ini

@@ -9,10 +9,14 @@ bind_port = 7000
 # udp port to help make udp hole to penetrate nat
 bind_udp_port = 7001
 
-# udp port used for kcp protocol, it can be same with 'bind_port'
-# if not set, kcp is disabled in frps
+# udp port used for kcp protocol, it can be same with 'bind_port'.
+# if not set, kcp is disabled in frps.
 kcp_bind_port = 7000
 
+# udp port used for quic protocol.
+# if not set, quic is disabled in frps.
+# quic_bind_port = 7002
+
 # specify which address proxy will listen for, default value is same with bind_addr
 # proxy_bind_addr = 127.0.0.1
 

+ 12 - 2
go.mod

@@ -1,6 +1,6 @@
 module github.com/fatedier/frp
 
-go 1.18
+go 1.19
 
 require (
 	github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5
@@ -13,6 +13,7 @@ require (
 	github.com/gorilla/mux v1.8.0
 	github.com/gorilla/websocket v1.5.0
 	github.com/hashicorp/yamux v0.1.1
+	github.com/lucas-clemente/quic-go v0.31.0
 	github.com/onsi/ginkgo v1.16.4
 	github.com/onsi/gomega v1.20.2
 	github.com/pires/go-proxyproto v0.6.2
@@ -36,15 +37,21 @@ require (
 	github.com/fsnotify/fsnotify v1.4.9 // indirect
 	github.com/go-playground/locales v0.14.0 // indirect
 	github.com/go-playground/universal-translator v0.18.0 // indirect
+	github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0 // indirect
+	github.com/golang/mock v1.6.0 // indirect
 	github.com/golang/protobuf v1.5.2 // indirect
 	github.com/golang/snappy v0.0.1 // indirect
 	github.com/google/go-cmp v0.5.8 // indirect
+	github.com/google/pprof v0.0.0-20210407192527-94a9f03dee38 // indirect
 	github.com/inconshreveable/mousetrap v1.0.0 // indirect
 	github.com/klauspost/cpuid/v2 v2.0.6 // indirect
 	github.com/klauspost/reedsolomon v1.9.15 // indirect
 	github.com/leodido/go-urn v1.2.1 // indirect
+	github.com/marten-seemann/qtls-go1-18 v0.1.3 // indirect
+	github.com/marten-seemann/qtls-go1-19 v0.1.1 // indirect
 	github.com/matttproud/golang_protobuf_extensions v1.0.1 // indirect
 	github.com/nxadm/tail v1.4.8 // indirect
+	github.com/onsi/ginkgo/v2 v2.2.0 // indirect
 	github.com/pkg/errors v0.9.1 // indirect
 	github.com/pmezard/go-difflib v1.0.0 // indirect
 	github.com/pquerna/cachecontrol v0.0.0-20180517163645-1555304b9b35 // indirect
@@ -56,8 +63,11 @@ require (
 	github.com/templexxx/xor v0.0.0-20191217153810-f85b25db303b // indirect
 	github.com/tjfoc/gmsm v1.4.1 // indirect
 	golang.org/x/crypto v0.0.0-20220315160706-3147a52a75dd // indirect
-	golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f // indirect
+	golang.org/x/exp v0.0.0-20220722155223-a9213eeb770e // indirect
+	golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4 // indirect
+	golang.org/x/sys v0.1.1-0.20221102194838-fc697a31fa06 // indirect
 	golang.org/x/text v0.3.7 // indirect
+	golang.org/x/tools v0.1.12 // indirect
 	google.golang.org/appengine v1.6.7 // indirect
 	google.golang.org/protobuf v1.28.1 // indirect
 	gopkg.in/square/go-jose.v2 v2.4.1 // indirect

+ 29 - 3
go.sum

@@ -113,6 +113,7 @@ github.com/go-playground/universal-translator v0.18.0/go.mod h1:UvRDBj+xPUEGrFYl
 github.com/go-playground/validator/v10 v10.11.0 h1:0W+xRM511GY47Yy3bZUbJVitCNg2BOGlCyvTqsp/xIw=
 github.com/go-playground/validator/v10 v10.11.0/go.mod h1:i+3WkQ1FvaUjjxh1kSvIA4dMGDBiPU55YFDl0WbKdWU=
 github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
+github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0 h1:p104kn46Q8WdvHunIJ9dAyjPVtrBPhSr3KT2yUst43I=
 github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0/go.mod h1:fyg7847qk6SyHyPtNmDHnmrv/HOrqktSC+C9fM+CJOE=
 github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
 github.com/gogo/protobuf v1.2.1/go.mod h1:hp+jE20tsWTFYpLwKvXlhS1hjn+gTNwPg2I6zVXpSg4=
@@ -128,6 +129,8 @@ github.com/golang/mock v1.4.0/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt
 github.com/golang/mock v1.4.1/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw=
 github.com/golang/mock v1.4.3/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw=
 github.com/golang/mock v1.4.4/go.mod h1:l3mdAwkq5BuhzHwde/uurv3sEJeZMXNpwsxVWU71h+4=
+github.com/golang/mock v1.6.0 h1:ErTB+efbowRARo13NNdxyJji2egdxLGQhRaY+DUumQc=
+github.com/golang/mock v1.6.0/go.mod h1:p6yTPP+5HYm5mzsMV8JkE6ZKdX+/wYM6Hr+LicevLPs=
 github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
 github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
 github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
@@ -170,6 +173,8 @@ github.com/google/pprof v0.0.0-20200212024743-f11f1df84d12/go.mod h1:ZgVRPoUq/hf
 github.com/google/pprof v0.0.0-20200229191704-1ebb73c60ed3/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
 github.com/google/pprof v0.0.0-20200430221834-fc25d7d30c6d/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
 github.com/google/pprof v0.0.0-20200708004538-1a94d8640e99/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
+github.com/google/pprof v0.0.0-20210407192527-94a9f03dee38 h1:yAJXTCF9TqKcTiHJAE8dj7HMvPfh66eeA2JYW7eFpSE=
+github.com/google/pprof v0.0.0-20210407192527-94a9f03dee38/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
 github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI=
 github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I=
 github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
@@ -208,6 +213,7 @@ github.com/hashicorp/yamux v0.1.1 h1:yrQxtgseBDrq9Y652vSRDvsKCJKOUD+GzTS4Y0Y8pvE
 github.com/hashicorp/yamux v0.1.1/go.mod h1:CtWFDAQgb7dxtzFs4tWbplKIe2jSi3+5vKbgIO0SLnQ=
 github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
 github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
+github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
 github.com/inconshreveable/mousetrap v1.0.0 h1:Z8tu5sraLXCXIcARxBp/8cbvlwVa7Z1NHg9XEKhtSvM=
 github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8=
 github.com/jonboulle/clockwork v0.1.0/go.mod h1:Ii8DK3G1RaLaWxj9trq07+26W01tbo22gdxWY5EU2bo=
@@ -240,7 +246,13 @@ github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
 github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
 github.com/leodido/go-urn v1.2.1 h1:BqpAaACuzVSgi/VLzGZIobT2z4v53pjosyNd9Yv6n/w=
 github.com/leodido/go-urn v1.2.1/go.mod h1:zt4jvISO2HfUBqxjfIshjdMTYS56ZS/qv49ictyFfxY=
+github.com/lucas-clemente/quic-go v0.31.0 h1:MfNp3fk0wjWRajw6quMFA3ap1AVtlU+2mtwmbVogB2M=
+github.com/lucas-clemente/quic-go v0.31.0/go.mod h1:0wFbizLgYzqHqtlyxyCaJKlE7bYgE6JQ+54TLd/Dq2g=
 github.com/magiconair/properties v1.8.1/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ=
+github.com/marten-seemann/qtls-go1-18 v0.1.3 h1:R4H2Ks8P6pAtUagjFty2p7BVHn3XiwDAl7TTQf5h7TI=
+github.com/marten-seemann/qtls-go1-18 v0.1.3/go.mod h1:mJttiymBAByA49mhlNZZGrH5u1uXYZJ+RW28Py7f4m4=
+github.com/marten-seemann/qtls-go1-19 v0.1.1 h1:mnbxeq3oEyQxQXwI4ReCgW9DPoPR94sNlqWoDZnjRIE=
+github.com/marten-seemann/qtls-go1-19 v0.1.1/go.mod h1:5HTDWtVudo/WFsHKRNuOhWlbdjrfs5JHrYb0wIJqGpI=
 github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU=
 github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4=
 github.com/mattn/go-runewidth v0.0.9 h1:Lm995f3rfxdpd6TSmuVCHVb/QhupuXlYr8sCI/QdE+0=
@@ -271,7 +283,8 @@ github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+W
 github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk=
 github.com/onsi/ginkgo v1.16.4 h1:29JGrr5oVBm5ulCWet69zQkzWipVXIol6ygQUe/EzNc=
 github.com/onsi/ginkgo v1.16.4/go.mod h1:dX+/inL/fNMqNlz0e9LfyB9TswhZpCVdJM/Z6Vvnwo0=
-github.com/onsi/ginkgo/v2 v2.1.6 h1:Fx2POJZfKRQcM1pH49qSZiYeu319wji004qX+GDovrU=
+github.com/onsi/ginkgo/v2 v2.2.0 h1:3ZNA3L1c5FYDFTTxbFeVGGD8jYvjYauHD30YgLxVsNI=
+github.com/onsi/ginkgo/v2 v2.2.0/go.mod h1:MEH45j8TBi6u9BMogfbp0stKC5cdGjumZj5Y7AG4VIk=
 github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY=
 github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo=
 github.com/onsi/gomega v1.20.2 h1:8uQq0zMgLEfa0vRrrBgaJF2gyW9Da9BmfGV+OyUzfkY=
@@ -371,6 +384,7 @@ github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9de
 github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
 github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
 github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
+github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
 go.etcd.io/bbolt v1.3.2/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU=
 go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU=
 go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8=
@@ -401,6 +415,8 @@ golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u0
 golang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4=
 golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM=
 golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU=
+golang.org/x/exp v0.0.0-20220722155223-a9213eeb770e h1:+WEEuIdZHnUeJJmEUjyYC2gfUMj69yZXw17EnHg/otA=
+golang.org/x/exp v0.0.0-20220722155223-a9213eeb770e/go.mod h1:Kr81I6Kryrl9sr8s2FK3vxD90NdsKWRuOIl2O4CvYbA=
 golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js=
 golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
 golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
@@ -421,6 +437,9 @@ golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzB
 golang.org/x/mod v0.1.1-0.20191107180719-034126e5016b/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
 golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
 golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
+golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
+golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4 h1:6zppjxzCulZykYSLyVDYbneBfbaBIQPYMevg0bEwv2s=
+golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
 golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
 golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
 golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
@@ -456,6 +475,7 @@ golang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81R
 golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
 golang.org/x/net v0.0.0-20201010224723-4f7140c49acb/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
 golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
+golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM=
 golang.org/x/net v0.0.0-20210525063256-abc453219eb5/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
 golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
 golang.org/x/net v0.0.0-20220127200216-cd36cc0744dd/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk=
@@ -480,6 +500,7 @@ golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a/go.mod h1:RxMgew5VJxzue5/jJ
 golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
 golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
 golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
 golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
 golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
 golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
@@ -522,14 +543,16 @@ golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7w
 golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20210112080510-489259a85091/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.0.0-20210603081109-ebe580a85c40/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.0.0-20210806184541-e5e7981a1069/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.0.0-20220114195835-da31bd327af9/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
-golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f h1:v4INt8xihDGvnrfjMDVXGxw9wrfxYyCjk0KbXjhR55s=
-golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.1.1-0.20221102194838-fc697a31fa06 h1:E1pm64FqQa4v8dHd/bAneyMkR4hk8LTJhoSlc5mc1cM=
+golang.org/x/sys v0.1.1-0.20221102194838-fc697a31fa06/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
 golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
 golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
@@ -589,6 +612,9 @@ golang.org/x/tools v0.0.0-20200729194436-6467de6f59a7/go.mod h1:njjCfa9FT2d7l9Bc
 golang.org/x/tools v0.0.0-20200804011535-6c149bb5ef0d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
 golang.org/x/tools v0.0.0-20200825202427-b303f430e36d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
 golang.org/x/tools v0.0.0-20201224043029-2b0845dc783e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
+golang.org/x/tools v0.1.1/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
+golang.org/x/tools v0.1.12 h1:VveCTK38A2rkS8ZqFY25HIDFscX5X9OoEhJd3quQmXU=
+golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
 golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
 golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
 golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=

+ 2 - 2
pkg/config/client.go

@@ -115,7 +115,7 @@ type ClientCommonConf struct {
 	Start []string `ini:"start" json:"start"`
 	// Start map[string]struct{} `json:"start"`
 	// Protocol specifies the protocol to use when interacting with the server.
-	// Valid values are "tcp", "kcp" and "websocket". By default, this value
+	// Valid values are "tcp", "kcp", "quic" and "websocket". By default, this value
 	// is "tcp".
 	Protocol string `ini:"protocol" json:"protocol"`
 	// TLSEnable specifies whether or not TLS should be used when communicating
@@ -228,7 +228,7 @@ func (cfg *ClientCommonConf) Validate() error {
 		}
 	}
 
-	if cfg.Protocol != "tcp" && cfg.Protocol != "kcp" && cfg.Protocol != "websocket" {
+	if cfg.Protocol != "tcp" && cfg.Protocol != "kcp" && cfg.Protocol != "websocket" && cfg.Protocol != "quic" {
 		return fmt.Errorf("invalid protocol")
 	}
 

+ 4 - 0
pkg/config/server.go

@@ -46,6 +46,10 @@ type ServerCommonConf struct {
 	// value is 0, the server will not listen for KCP connections. By default,
 	// this value is 0.
 	KCPBindPort int `ini:"kcp_bind_port" json:"kcp_bind_port" validate:"gte=0,lte=65535"`
+	// QUICBindPort specifies the QUIC port that the server listens on.
+	// Set this value to 0 will disable this feature.
+	// By default, the value is 0.
+	QUICBindPort int `ini:"quic_bind_port" json:"quic_bind_port" validate:"gte=0,lte=65535"`
 	// ProxyBindAddr specifies the address that the proxy binds to. This value
 	// may be the same as BindAddr.
 	ProxyBindAddr string `ini:"proxy_bind_addr" json:"proxy_bind_addr"`

+ 28 - 0
pkg/util/net/conn.go

@@ -22,6 +22,8 @@ import (
 	"sync/atomic"
 	"time"
 
+	quic "github.com/lucas-clemente/quic-go"
+
 	"github.com/fatedier/frp/pkg/util/xlog"
 )
 
@@ -183,3 +185,29 @@ func (statsConn *StatsConn) Close() (err error) {
 	}
 	return
 }
+
+type wrapQuicStream struct {
+	quic.Stream
+	c quic.Connection
+}
+
+func QuicStreamToNetConn(s quic.Stream, c quic.Connection) net.Conn {
+	return &wrapQuicStream{
+		Stream: s,
+		c:      c,
+	}
+}
+
+func (conn *wrapQuicStream) LocalAddr() net.Addr {
+	if conn.c != nil {
+		return conn.c.LocalAddr()
+	}
+	return (*net.TCPAddr)(nil)
+}
+
+func (conn *wrapQuicStream) RemoteAddr() net.Addr {
+	if conn.c != nil {
+		return conn.c.RemoteAddr()
+	}
+	return (*net.TCPAddr)(nil)
+}

+ 44 - 2
server/service.go

@@ -28,6 +28,7 @@ import (
 
 	"github.com/fatedier/golib/net/mux"
 	fmux "github.com/hashicorp/yamux"
+	quic "github.com/lucas-clemente/quic-go"
 
 	"github.com/fatedier/frp/assets"
 	"github.com/fatedier/frp/pkg/auth"
@@ -68,6 +69,9 @@ type Service struct {
 	// Accept connections using kcp
 	kcpListener net.Listener
 
+	// Accept connections using quic
+	quicListener quic.Listener
+
 	// Accept connections using websocket
 	websocketListener net.Listener
 
@@ -200,12 +204,24 @@ func NewService(cfg config.ServerCommonConf) (svr *Service, err error) {
 		address := net.JoinHostPort(cfg.BindAddr, strconv.Itoa(cfg.KCPBindPort))
 		svr.kcpListener, err = frpNet.ListenKcp(address)
 		if err != nil {
-			err = fmt.Errorf("listen on kcp address udp %s error: %v", address, err)
+			err = fmt.Errorf("listen on kcp udp address %s error: %v", address, err)
 			return
 		}
 		log.Info("frps kcp listen on udp %s", address)
 	}
 
+	if cfg.QUICBindPort > 0 {
+		address := net.JoinHostPort(cfg.BindAddr, strconv.Itoa(cfg.QUICBindPort))
+		quicTLSCfg := tlsConfig.Clone()
+		quicTLSCfg.NextProtos = []string{"frp"}
+		svr.quicListener, err = quic.ListenAddr(address, quicTLSCfg, nil)
+		if err != nil {
+			err = fmt.Errorf("listen on quic udp address %s error: %v", address, err)
+			return
+		}
+		log.Info("frps quic listen on quic %s", address)
+	}
+
 	// Listen for accepting connections from client using websocket protocol.
 	websocketPrefix := []byte("GET " + frpNet.FrpWebsocketPath)
 	websocketLn := svr.muxer.Listen(0, uint32(len(websocketPrefix)), func(data []byte) bool {
@@ -310,9 +326,12 @@ func (svr *Service) Run() {
 	if svr.rc.NatHoleController != nil {
 		go svr.rc.NatHoleController.Run()
 	}
-	if svr.cfg.KCPBindPort > 0 {
+	if svr.kcpListener != nil {
 		go svr.HandleListener(svr.kcpListener)
 	}
+	if svr.quicListener != nil {
+		go svr.HandleQUICListener(svr.quicListener)
+	}
 
 	go svr.HandleListener(svr.websocketListener)
 	go svr.HandleListener(svr.tlsListener)
@@ -437,6 +456,29 @@ func (svr *Service) HandleListener(l net.Listener) {
 	}
 }
 
+func (svr *Service) HandleQUICListener(l quic.Listener) {
+	// Listen for incoming connections from client.
+	for {
+		c, err := l.Accept(context.Background())
+		if err != nil {
+			log.Warn("QUICListener for incoming connections from client closed")
+			return
+		}
+		// Start a new goroutine to handle connection.
+		go func(ctx context.Context, frpConn quic.Connection) {
+			for {
+				stream, err := frpConn.AcceptStream(context.Background())
+				if err != nil {
+					log.Debug("Accept new quic mux stream error: %v", err)
+					_ = frpConn.CloseWithError(0, "")
+					return
+				}
+				go svr.handleConnection(ctx, frpNet.QuicStreamToNetConn(stream, frpConn))
+			}
+		}(context.Background(), c)
+	}
+}
+
 func (svr *Service) RegisterControl(ctlConn net.Conn, loginMsg *msg.Login) (err error) {
 	// If client's RunID is empty, it's a new client, we just create a new controller.
 	// Otherwise, we check if there is one controller has the same run id. If so, we release previous controller and start new one.

+ 28 - 25
test/e2e/basic/client_server.go

@@ -18,6 +18,15 @@ type generalTestConfigures struct {
 	expectError bool
 }
 
+func renderBindPortConfig(protocol string) string {
+	if protocol == "kcp" {
+		return fmt.Sprintf(`kcp_bind_port = {{ .%s }}`, consts.PortServerName)
+	} else if protocol == "quic" {
+		return fmt.Sprintf(`quic_bind_port = {{ .%s }}`, consts.PortServerName)
+	}
+	return ""
+}
+
 func runClientServerTest(f *framework.Framework, configures *generalTestConfigures) {
 	serverConf := consts.DefaultServerConfig
 	clientConf := consts.DefaultClientConfig
@@ -63,13 +72,12 @@ var _ = ginkgo.Describe("[Feature: Client-Server]", func() {
 	f := framework.NewDefaultFramework()
 
 	ginkgo.Describe("Protocol", func() {
-		supportProtocols := []string{"tcp", "kcp", "websocket"}
+		supportProtocols := []string{"tcp", "kcp", "quic", "websocket"}
 		for _, protocol := range supportProtocols {
 			configures := &generalTestConfigures{
 				server: fmt.Sprintf(`
-				kcp_bind_port = {{ .%s }}
-				protocol = %s"
-				`, consts.PortServerName, protocol),
+				%s
+				`, renderBindPortConfig(protocol)),
 				client: "protocol = " + protocol,
 			}
 			defineClientServerTest(protocol, f, configures)
@@ -90,14 +98,13 @@ var _ = ginkgo.Describe("[Feature: Client-Server]", func() {
 	})
 
 	ginkgo.Describe("TLS", func() {
-		supportProtocols := []string{"tcp", "kcp", "websocket"}
+		supportProtocols := []string{"tcp", "kcp", "quic", "websocket"}
 		for _, protocol := range supportProtocols {
 			tmp := protocol
 			defineClientServerTest("TLS over "+strings.ToUpper(tmp), f, &generalTestConfigures{
 				server: fmt.Sprintf(`
-				kcp_bind_port = {{ .%s }}
-				protocol = %s
-				`, consts.PortServerName, protocol),
+				%s
+				`, renderBindPortConfig(protocol)),
 				client: fmt.Sprintf(`tls_enable = true
 				protocol = %s
 				`, protocol),
@@ -115,7 +122,7 @@ var _ = ginkgo.Describe("[Feature: Client-Server]", func() {
 	})
 
 	ginkgo.Describe("TLS with custom certificate", func() {
-		supportProtocols := []string{"tcp", "kcp", "websocket"}
+		supportProtocols := []string{"tcp", "kcp", "quic", "websocket"}
 
 		var (
 			caCrtPath                    string
@@ -124,14 +131,14 @@ var _ = ginkgo.Describe("[Feature: Client-Server]", func() {
 		)
 		ginkgo.JustBeforeEach(func() {
 			generator := &cert.SelfSignedCertGenerator{}
-			artifacts, err := generator.Generate("0.0.0.0")
+			artifacts, err := generator.Generate("127.0.0.1")
 			framework.ExpectNoError(err)
 
 			caCrtPath = f.WriteTempFile("ca.crt", string(artifacts.CACert))
 			serverCrtPath = f.WriteTempFile("server.crt", string(artifacts.Cert))
 			serverKeyPath = f.WriteTempFile("server.key", string(artifacts.Key))
 			generator.SetCA(artifacts.CACert, artifacts.CAKey)
-			_, err = generator.Generate("0.0.0.0")
+			_, err = generator.Generate("127.0.0.1")
 			framework.ExpectNoError(err)
 			clientCrtPath = f.WriteTempFile("client.crt", string(artifacts.Cert))
 			clientKeyPath = f.WriteTempFile("client.key", string(artifacts.Key))
@@ -143,10 +150,9 @@ var _ = ginkgo.Describe("[Feature: Client-Server]", func() {
 			ginkgo.It("one-way authentication: "+tmp, func() {
 				runClientServerTest(f, &generalTestConfigures{
 					server: fmt.Sprintf(`
-						protocol = %s
-						kcp_bind_port = {{ .%s }}
+						%s
 						tls_trusted_ca_file = %s
-					`, tmp, consts.PortServerName, caCrtPath),
+					`, renderBindPortConfig(tmp), caCrtPath),
 					client: fmt.Sprintf(`
 						protocol = %s
 						tls_enable = true
@@ -159,12 +165,11 @@ var _ = ginkgo.Describe("[Feature: Client-Server]", func() {
 			ginkgo.It("mutual authentication: "+tmp, func() {
 				runClientServerTest(f, &generalTestConfigures{
 					server: fmt.Sprintf(`
-						protocol = %s
-						kcp_bind_port = {{ .%s }}
+						%s
 						tls_cert_file = %s
 						tls_key_file = %s
 						tls_trusted_ca_file = %s
-					`, tmp, consts.PortServerName, serverCrtPath, serverKeyPath, caCrtPath),
+					`, renderBindPortConfig(tmp), serverCrtPath, serverKeyPath, caCrtPath),
 					client: fmt.Sprintf(`
 						protocol = %s
 						tls_enable = true
@@ -235,14 +240,13 @@ var _ = ginkgo.Describe("[Feature: Client-Server]", func() {
 	})
 
 	ginkgo.Describe("TLS with disable_custom_tls_first_byte", func() {
-		supportProtocols := []string{"tcp", "kcp", "websocket"}
+		supportProtocols := []string{"tcp", "kcp", "quic", "websocket"}
 		for _, protocol := range supportProtocols {
 			tmp := protocol
 			defineClientServerTest("TLS over "+strings.ToUpper(tmp), f, &generalTestConfigures{
 				server: fmt.Sprintf(`
-					kcp_bind_port = {{ .%s }}
-					protocol = %s
-					`, consts.PortServerName, protocol),
+					%s
+					`, renderBindPortConfig(protocol)),
 				client: fmt.Sprintf(`
 					tls_enable = true
 					protocol = %s
@@ -253,15 +257,14 @@ var _ = ginkgo.Describe("[Feature: Client-Server]", func() {
 	})
 
 	ginkgo.Describe("IPv6 bind address", func() {
-		supportProtocols := []string{"tcp", "kcp", "websocket"}
+		supportProtocols := []string{"tcp", "kcp", "quic", "websocket"}
 		for _, protocol := range supportProtocols {
 			tmp := protocol
 			defineClientServerTest("IPv6 bind address: "+strings.ToUpper(tmp), f, &generalTestConfigures{
 				server: fmt.Sprintf(`
 					bind_addr = ::
-					kcp_bind_port = {{ .%s }}
-					protocol = %s
-					`, consts.PortServerName, protocol),
+					%s
+					`, renderBindPortConfig(protocol)),
 				client: fmt.Sprintf(`
 					tls_enable = true
 					protocol = %s

+ 1 - 0
test/e2e/framework/consts/consts.go

@@ -25,6 +25,7 @@ var (
 
 	DefaultClientConfig = `
 	[common]
+	server_addr = 127.0.0.1
 	server_port = {{ .%s }}
 	log_level = trace
 	`