Browse Source

support wss between frpc and frps (#3503)

fatedier 1 năm trước cách đây
mục cha
commit
801e8c6742
10 tập tin đã thay đổi với 116 bổ sung42 xóa
  1. 2 13
      Release.md
  2. 21 8
      client/service.go
  3. 5 2
      cmd/frpc/sub/root.go
  4. 1 1
      conf/frpc_full.ini
  5. 4 1
      go.mod
  6. 4 4
      go.sum
  7. 3 2
      pkg/config/client.go
  8. 8 2
      pkg/util/net/dial.go
  9. 13 5
      test/e2e/basic/basic.go
  10. 55 4
      test/e2e/basic/client_server.go

+ 2 - 13
Release.md

@@ -1,18 +1,7 @@
-## Notes
-
-**For enhanced security, the default values for `tls_enable` and `disable_custom_tls_first_byte` have been set to true.**
-
-If you wish to revert to the previous default values, you need to manually set the values of these two parameters to false.
-
 ### Features
 
-* Added support for `allow_users` in stcp, sudp, xtcp. By default, only the same user is allowed to access. Use `*` to allow access from any user. The visitor configuration now supports `server_user` to connect to proxies of other users.
-* Added fallback support to a specified alternative visitor when xtcp connection fails.
-
-### Improvements
-
-* Increased the default value of `MaxStreamWindowSize` for yamux to 6MB, improving traffic forwarding rate in high-latency scenarios.
+* frpc supports connecting to frps via the wss protocol by enabling the configuration `protocol = wss`.
 
 ### Fixes
 
-* Fixed an issue where having proxies with the same name would cause previously working proxies to become ineffective in `xtcp`.
+* Fix an issue caused by a bug in yamux that prevents wss from working properly in certain plugins.

+ 21 - 8
client/service.go

@@ -135,7 +135,7 @@ func (svr *Service) Run() error {
 			if svr.cfg.LoginFailExit {
 				return err
 			}
-			util.RandomSleep(10*time.Second, 0.9, 1.1)
+			util.RandomSleep(5*time.Second, 0.9, 1.1)
 		} else {
 			// login success
 			ctl := NewControl(svr.ctx, svr.runID, conn, cm, svr.cfg, svr.pxyCfgs, svr.visitorCfgs, svr.authSetter)
@@ -427,7 +427,11 @@ func (cm *ConnectionManager) realConnect() (net.Conn, error) {
 	xl := xlog.FromContextSafe(cm.ctx)
 	var tlsConfig *tls.Config
 	var err error
-	if cm.cfg.TLSEnable {
+	tlsEnable := cm.cfg.TLSEnable
+	if cm.cfg.Protocol == "wss" {
+		tlsEnable = true
+	}
+	if tlsEnable {
 		sn := cm.cfg.TLSServerName
 		if sn == "" {
 			sn = cm.cfg.ServerAddr
@@ -451,10 +455,23 @@ func (cm *ConnectionManager) realConnect() (net.Conn, error) {
 	}
 	dialOptions := []libdial.DialOption{}
 	protocol := cm.cfg.Protocol
-	if protocol == "websocket" {
+	switch protocol {
+	case "websocket":
+		protocol = "tcp"
+		dialOptions = append(dialOptions, libdial.WithAfterHook(libdial.AfterHook{Hook: utilnet.DialHookWebsocket(protocol, "")}))
+		dialOptions = append(dialOptions, libdial.WithAfterHook(libdial.AfterHook{
+			Hook: utilnet.DialHookCustomTLSHeadByte(tlsConfig != nil, cm.cfg.DisableCustomTLSFirstByte),
+		}))
+		dialOptions = append(dialOptions, libdial.WithTLSConfig(tlsConfig))
+	case "wss":
 		protocol = "tcp"
-		dialOptions = append(dialOptions, libdial.WithAfterHook(libdial.AfterHook{Hook: utilnet.DialHookWebsocket()}))
+		dialOptions = append(dialOptions, libdial.WithTLSConfigAndPriority(100, tlsConfig))
+		// Make sure that if it is wss, the websocket hook is executed after the tls hook.
+		dialOptions = append(dialOptions, libdial.WithAfterHook(libdial.AfterHook{Hook: utilnet.DialHookWebsocket(protocol, tlsConfig.ServerName), Priority: 110}))
+	default:
+		dialOptions = append(dialOptions, libdial.WithTLSConfig(tlsConfig))
 	}
+
 	if cm.cfg.ConnectServerLocalIP != "" {
 		dialOptions = append(dialOptions, libdial.WithLocalAddr(cm.cfg.ConnectServerLocalIP))
 	}
@@ -464,10 +481,6 @@ func (cm *ConnectionManager) realConnect() (net.Conn, error) {
 		libdial.WithKeepAlive(time.Duration(cm.cfg.DialServerKeepAlive)*time.Second),
 		libdial.WithProxy(proxyType, addr),
 		libdial.WithProxyAuth(auth),
-		libdial.WithTLSConfig(tlsConfig),
-		libdial.WithAfterHook(libdial.AfterHook{
-			Hook: utilnet.DialHookCustomTLSHeadByte(tlsConfig != nil, cm.cfg.DisableCustomTLSFirstByte),
-		}),
 	)
 	conn, err := libdial.DialContext(
 		cm.ctx,

+ 5 - 2
cmd/frpc/sub/root.go

@@ -76,7 +76,8 @@ var (
 	bindAddr           string
 	bindPort           int
 
-	tlsEnable bool
+	tlsEnable     bool
+	tlsServerName string
 )
 
 func init() {
@@ -88,13 +89,14 @@ func init() {
 func RegisterCommonFlags(cmd *cobra.Command) {
 	cmd.PersistentFlags().StringVarP(&serverAddr, "server_addr", "s", "127.0.0.1:7000", "frp server's address")
 	cmd.PersistentFlags().StringVarP(&user, "user", "u", "", "user")
-	cmd.PersistentFlags().StringVarP(&protocol, "protocol", "p", "tcp", "tcp or kcp or websocket")
+	cmd.PersistentFlags().StringVarP(&protocol, "protocol", "p", "tcp", "tcp, kcp, quic, websocket, wss")
 	cmd.PersistentFlags().StringVarP(&token, "token", "t", "", "auth token")
 	cmd.PersistentFlags().StringVarP(&logLevel, "log_level", "", "info", "log level")
 	cmd.PersistentFlags().StringVarP(&logFile, "log_file", "", "console", "console or file path")
 	cmd.PersistentFlags().IntVarP(&logMaxDays, "log_max_days", "", 3, "log file reversed days")
 	cmd.PersistentFlags().BoolVarP(&disableLogColor, "disable_log_color", "", false, "disable log color in console")
 	cmd.PersistentFlags().BoolVarP(&tlsEnable, "tls_enable", "", true, "enable frpc tls")
+	cmd.PersistentFlags().StringVarP(&tlsServerName, "tls_server_name", "", "", "specify the custom server name of tls certificate")
 	cmd.PersistentFlags().StringVarP(&dnsServer, "dns_server", "", "", "specify dns server instead of using system default one")
 }
 
@@ -186,6 +188,7 @@ func parseClientCommonCfgFromCmd() (cfg config.ClientCommonConf, err error) {
 	cfg.ClientConfig = auth.GetDefaultClientConf()
 	cfg.Token = token
 	cfg.TLSEnable = tlsEnable
+	cfg.TLSServerName = tlsServerName
 
 	cfg.Complete()
 	if err = cfg.Validate(); err != nil {

+ 1 - 1
conf/frpc_full.ini

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

+ 4 - 1
go.mod

@@ -6,7 +6,7 @@ require (
 	github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5
 	github.com/coreos/go-oidc/v3 v3.4.0
 	github.com/fatedier/beego v0.0.0-20171024143340-6c6a4f5bd5eb
-	github.com/fatedier/golib v0.1.1-0.20230320133937-a7edcc8c793d
+	github.com/fatedier/golib v0.1.1-0.20230628070619-a1a0c648236a
 	github.com/fatedier/kcp-go v2.0.4-0.20190803094908-fe8645b0a904+incompatible
 	github.com/go-playground/validator/v10 v10.11.0
 	github.com/google/uuid v1.3.0
@@ -75,3 +75,6 @@ require (
 	gopkg.in/yaml.v3 v3.0.1 // indirect
 	k8s.io/utils v0.0.0-20221107191617-1a15be271d1d // indirect
 )
+
+// TODO(fatedier): Temporary use the modified version, update to the official version after merging into the official repository.
+replace github.com/hashicorp/yamux => github.com/fatedier/yamux v0.0.0-20230628132301-7aca4898904d

+ 4 - 4
go.sum

@@ -121,10 +121,12 @@ github.com/envoyproxy/go-control-plane v0.10.2-0.20220325020618-49ff273808a1/go.
 github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
 github.com/fatedier/beego v0.0.0-20171024143340-6c6a4f5bd5eb h1:wCrNShQidLmvVWn/0PikGmpdP0vtQmnvyRg3ZBEhczw=
 github.com/fatedier/beego v0.0.0-20171024143340-6c6a4f5bd5eb/go.mod h1:wx3gB6dbIfBRcucp94PI9Bt3I0F2c/MyNEWuhzpWiwk=
-github.com/fatedier/golib v0.1.1-0.20230320133937-a7edcc8c793d h1:/m9Atycn9uKRwwOkxv4c+zaugxRgkdSG/Eg3IJWOpNs=
-github.com/fatedier/golib v0.1.1-0.20230320133937-a7edcc8c793d/go.mod h1:Wdn1pJ0dHB1lah6FPYwt4AO9NEmWI0OzW13dpzC9g4E=
+github.com/fatedier/golib v0.1.1-0.20230628070619-a1a0c648236a h1:HiRTFdy3ary86Vi2nsoINy2/YgjDPQ+21j3ikwJSD2E=
+github.com/fatedier/golib v0.1.1-0.20230628070619-a1a0c648236a/go.mod h1:Wdn1pJ0dHB1lah6FPYwt4AO9NEmWI0OzW13dpzC9g4E=
 github.com/fatedier/kcp-go v2.0.4-0.20190803094908-fe8645b0a904+incompatible h1:ssXat9YXFvigNge/IkkZvFMn8yeYKFX+uI6wn2mLJ74=
 github.com/fatedier/kcp-go v2.0.4-0.20190803094908-fe8645b0a904+incompatible/go.mod h1:YpCOaxj7vvMThhIQ9AfTOPW2sfztQR5WDfs7AflSy4s=
+github.com/fatedier/yamux v0.0.0-20230628132301-7aca4898904d h1:ynk1ra0RUqDWQfvFi5KtMiSobkVQ3cNc0ODb8CfIETo=
+github.com/fatedier/yamux v0.0.0-20230628132301-7aca4898904d/go.mod h1:CtWFDAQgb7dxtzFs4tWbplKIe2jSi3+5vKbgIO0SLnQ=
 github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4=
 github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
 github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
@@ -270,8 +272,6 @@ github.com/hashicorp/logutils v1.0.0/go.mod h1:QIAnNjmIWmVIIkWDTG1z5v++HQmx9WQRO
 github.com/hashicorp/mdns v1.0.0/go.mod h1:tL+uN++7HEJ6SQLQ2/p+z2pH24WQKWjBPkE0mNTz8vQ=
 github.com/hashicorp/memberlist v0.1.3/go.mod h1:ajVTdAv/9Im8oMAAj5G31PhhMCZJV2pPBoIllUwCN7I=
 github.com/hashicorp/serf v0.8.2/go.mod h1:6hOLApaqBFA1NXqRQAsxw9QxuDEvNxSQRwA/JwenrHc=
-github.com/hashicorp/yamux v0.1.1 h1:yrQxtgseBDrq9Y652vSRDvsKCJKOUD+GzTS4Y0Y8pvE=
-github.com/hashicorp/yamux v0.1.1/go.mod h1:CtWFDAQgb7dxtzFs4tWbplKIe2jSi3+5vKbgIO0SLnQ=
 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=

+ 3 - 2
pkg/config/client.go

@@ -20,6 +20,7 @@ import (
 	"path/filepath"
 	"strings"
 
+	"github.com/samber/lo"
 	"gopkg.in/ini.v1"
 
 	"github.com/fatedier/frp/pkg/auth"
@@ -117,7 +118,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", "quic" and "websocket". By default, this value
+	// Valid values are "tcp", "kcp", "quic", "websocket" and "wss". By default, this value
 	// is "tcp".
 	Protocol string `ini:"protocol" json:"protocol"`
 	// QUIC protocol options
@@ -230,7 +231,7 @@ func (cfg *ClientCommonConf) Validate() error {
 		}
 	}
 
-	if cfg.Protocol != "tcp" && cfg.Protocol != "kcp" && cfg.Protocol != "websocket" && cfg.Protocol != "quic" {
+	if !lo.Contains([]string{"tcp", "kcp", "quic", "websocket", "wss"}, cfg.Protocol) {
 		return fmt.Errorf("invalid protocol")
 	}
 

+ 8 - 2
pkg/util/net/dial.go

@@ -21,9 +21,15 @@ func DialHookCustomTLSHeadByte(enableTLS bool, disableCustomTLSHeadByte bool) li
 	}
 }
 
-func DialHookWebsocket() libdial.AfterHookFunc {
+func DialHookWebsocket(protocol string, host string) libdial.AfterHookFunc {
 	return func(ctx context.Context, c net.Conn, addr string) (context.Context, net.Conn, error) {
-		addr = "ws://" + addr + FrpWebsocketPath
+		if protocol != "wss" {
+			protocol = "ws"
+		}
+		if host == "" {
+			host = addr
+		}
+		addr = protocol + "://" + host + FrpWebsocketPath
 		uri, err := url.Parse(addr)
 		if err != nil {
 			return nil, nil, err

+ 13 - 5
test/e2e/basic/basic.go

@@ -331,24 +331,29 @@ var _ = ginkgo.Describe("[Feature: Basic]", func() {
 					proxyExtraConfig   string
 					visitorExtraConfig string
 					expectError        bool
-					user2              bool
+					deployUser2Client  bool
+					// skipXTCP is used to skip xtcp test case
+					skipXTCP bool
 				}{
 					{
 						proxyName:    "normal",
 						bindPortName: port.GenName("Normal"),
 						visitorSK:    correctSK,
+						skipXTCP:     true,
 					},
 					{
 						proxyName:         "with-encryption",
 						bindPortName:      port.GenName("WithEncryption"),
 						visitorSK:         correctSK,
 						commonExtraConfig: "use_encryption = true",
+						skipXTCP:          true,
 					},
 					{
 						proxyName:         "with-compression",
 						bindPortName:      port.GenName("WithCompression"),
 						visitorSK:         correctSK,
 						commonExtraConfig: "use_compression = true",
+						skipXTCP:          true,
 					},
 					{
 						proxyName:    "with-encryption-and-compression",
@@ -371,7 +376,7 @@ var _ = ginkgo.Describe("[Feature: Basic]", func() {
 						visitorSK:          correctSK,
 						proxyExtraConfig:   "allow_users = another, user2",
 						visitorExtraConfig: "server_user = user1",
-						user2:              true,
+						deployUser2Client:  true,
 					},
 					{
 						proxyName:          "not-allowed-user",
@@ -387,7 +392,7 @@ var _ = ginkgo.Describe("[Feature: Basic]", func() {
 						visitorSK:          correctSK,
 						proxyExtraConfig:   "allow_users = *",
 						visitorExtraConfig: "server_user = user1",
-						user2:              true,
+						deployUser2Client:  true,
 					},
 				}
 
@@ -399,7 +404,7 @@ var _ = ginkgo.Describe("[Feature: Basic]", func() {
 					config := getProxyVisitorConf(
 						test.proxyName, test.bindPortName, test.visitorSK, test.commonExtraConfig+"\n"+test.visitorExtraConfig,
 					) + "\n"
-					if test.user2 {
+					if test.deployUser2Client {
 						clientUser2VisitorConf += config
 					} else {
 						clientVisitorConf += config
@@ -411,7 +416,10 @@ var _ = ginkgo.Describe("[Feature: Basic]", func() {
 				for _, test := range tests {
 					timeout := time.Second
 					if t == "xtcp" {
-						timeout = 4 * time.Second
+						if test.skipXTCP {
+							continue
+						}
+						timeout = 10 * time.Second
 					}
 					framework.NewRequestExpect(f).
 						RequestModify(func(r *request.Request) {

+ 55 - 4
test/e2e/basic/client_server.go

@@ -3,6 +3,7 @@ package basic
 import (
 	"fmt"
 	"strings"
+	"time"
 
 	"github.com/onsi/ginkgo/v2"
 
@@ -13,9 +14,13 @@ import (
 )
 
 type generalTestConfigures struct {
-	server      string
-	client      string
-	expectError bool
+	server        string
+	client        string
+	clientPrefix  string
+	client2       string
+	client2Prefix string
+	testDelay     time.Duration
+	expectError   bool
 }
 
 func renderBindPortConfig(protocol string) string {
@@ -30,6 +35,9 @@ func renderBindPortConfig(protocol string) string {
 func runClientServerTest(f *framework.Framework, configures *generalTestConfigures) {
 	serverConf := consts.DefaultServerConfig
 	clientConf := consts.DefaultClientConfig
+	if configures.clientPrefix != "" {
+		clientConf = configures.clientPrefix
+	}
 
 	serverConf += fmt.Sprintf(`
 				%s
@@ -54,7 +62,23 @@ func runClientServerTest(f *framework.Framework, configures *generalTestConfigur
 		framework.UDPEchoServerPort, udpPortName,
 	)
 
-	f.RunProcesses([]string{serverConf}, []string{clientConf})
+	clientConfs := []string{clientConf}
+	if configures.client2 != "" {
+		client2Conf := consts.DefaultClientConfig
+		if configures.client2Prefix != "" {
+			client2Conf = configures.client2Prefix
+		}
+		client2Conf += fmt.Sprintf(`
+			%s
+		`, configures.client2)
+		clientConfs = append(clientConfs, client2Conf)
+	}
+
+	f.RunProcesses([]string{serverConf}, clientConfs)
+
+	if configures.testDelay > 0 {
+		time.Sleep(configures.testDelay)
+	}
 
 	framework.NewRequestExpect(f).PortName(tcpPortName).ExpectError(configures.expectError).Explain("tcp proxy").Ensure()
 	framework.NewRequestExpect(f).Protocol("udp").
@@ -84,6 +108,33 @@ var _ = ginkgo.Describe("[Feature: Client-Server]", func() {
 		}
 	})
 
+	// wss is special, it needs to be tested separately.
+	// frps only supports ws, so there should be a proxy to terminate TLS before frps.
+	ginkgo.Describe("Protocol wss", func() {
+		wssPort := f.AllocPort()
+		configures := &generalTestConfigures{
+			clientPrefix: fmt.Sprintf(`
+				[common]
+				server_addr = 127.0.0.1
+				server_port = %d
+				protocol = wss
+				log_level = trace
+				login_fail_exit = false
+			`, wssPort),
+			// Due to the fact that frps cannot directly accept wss connections, we use the https2http plugin of another frpc to terminate TLS.
+			client2: fmt.Sprintf(`
+				[wss2ws]
+				type = tcp
+				remote_port = %d
+				plugin = https2http
+				plugin_local_addr = 127.0.0.1:{{ .%s }}
+			`, wssPort, consts.PortServerName),
+			testDelay: 10 * time.Second,
+		}
+
+		defineClientServerTest("wss", f, configures)
+	})
+
 	ginkgo.Describe("Authentication", func() {
 		defineClientServerTest("Token Correct", f, &generalTestConfigures{
 			server: "token = 123456",