diff --git a/Release.md b/Release.md index ab58d8d..5d9623f 100644 --- a/Release.md +++ b/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. diff --git a/client/service.go b/client/service.go index df85c05..2f7782d 100644 --- a/client/service.go +++ b/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()})) + 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.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, diff --git a/cmd/frpc/sub/root.go b/cmd/frpc/sub/root.go index 515a193..82c6172 100644 --- a/cmd/frpc/sub/root.go +++ b/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 { diff --git a/conf/frpc_full.ini b/conf/frpc_full.ini index 4982f4f..f8eca6b 100644 --- a/conf/frpc_full.ini +++ b/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. diff --git a/go.mod b/go.mod index 8680184..b42d880 100644 --- a/go.mod +++ b/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 diff --git a/go.sum b/go.sum index 7950ad6..c259697 100644 --- a/go.sum +++ b/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= diff --git a/pkg/config/client.go b/pkg/config/client.go index b60e480..029470d 100644 --- a/pkg/config/client.go +++ b/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") } diff --git a/pkg/util/net/dial.go b/pkg/util/net/dial.go index 251ebbf..bc67064 100644 --- a/pkg/util/net/dial.go +++ b/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 diff --git a/test/e2e/basic/basic.go b/test/e2e/basic/basic.go index d9deafb..d032a0d 100644 --- a/test/e2e/basic/basic.go +++ b/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) { diff --git a/test/e2e/basic/client_server.go b/test/e2e/basic/client_server.go index a02e454..e7730f4 100644 --- a/test/e2e/basic/client_server.go +++ b/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",