Browse Source

Merge pull request #4205 from fatedier/dev

bump version
fatedier 8 months ago
parent
commit
4e8e9e1dec
48 changed files with 478 additions and 228 deletions
  1. 1 5
      .golangci.yml
  2. 22 10
      Makefile.cross-compiles
  3. 11 5
      README.md
  4. 6 5
      README_zh.md
  5. 9 2
      Release.md
  6. 1 1
      assets/frps/static/index-82-40HIG.js
  7. 1 1
      assets/frps/static/index.html
  8. 1 1
      client/admin_api.go
  9. 17 17
      client/connector.go
  10. 3 9
      client/control.go
  11. 3 3
      client/proxy/proxy.go
  12. 20 7
      client/service.go
  13. 1 0
      conf/frpc_full_example.toml
  14. 3 3
      go.mod
  15. 6 6
      go.sum
  16. 42 35
      package.sh
  17. 8 2
      pkg/config/v1/client.go
  18. 3 0
      pkg/config/v1/proxy.go
  19. 6 1
      pkg/config/v1/server.go
  20. 1 0
      pkg/msg/msg.go
  21. 7 1
      pkg/plugin/client/http2https.go
  22. 2 1
      pkg/plugin/client/http_proxy.go
  23. 9 1
      pkg/plugin/client/https2http.go
  24. 10 2
      pkg/plugin/client/https2https.go
  25. 2 1
      pkg/plugin/client/static_file.go
  26. 4 2
      pkg/ssh/server.go
  27. 30 0
      pkg/util/log/log.go
  28. 3 3
      pkg/util/net/dial.go
  29. 4 2
      pkg/util/net/websocket.go
  30. 1 1
      pkg/util/version/version.go
  31. 26 32
      pkg/util/vhost/http.go
  32. 1 1
      pkg/util/vhost/https_test.go
  33. 3 1
      pkg/util/vhost/vhost.go
  34. 11 13
      server/control.go
  35. 2 3
      server/dashboard_api.go
  36. 1 0
      server/proxy/http.go
  37. 5 4
      server/service.go
  38. 1 1
      test/e2e/framework/framework.go
  39. 3 3
      test/e2e/framework/process.go
  40. 3 3
      test/e2e/pkg/request/request.go
  41. 80 2
      test/e2e/v1/basic/http.go
  42. 1 0
      web/frps/components.d.ts
  43. 1 0
      web/frps/src/App.vue
  44. 38 0
      web/frps/src/components/ProxiesTCPMux.vue
  45. 34 36
      web/frps/src/components/ProxyViewExpand.vue
  46. 2 2
      web/frps/src/components/ServerOverview.vue
  47. 6 0
      web/frps/src/router/index.ts
  48. 23 0
      web/frps/src/utils/proxy.ts

+ 1 - 5
.golangci.yml

@@ -1,6 +1,6 @@
 service:
   golangci-lint-version: 1.57.x # use the fixed version to not introduce new linters unexpectedly
-  
+
 run:
   concurrency: 4
   # timeout for analysis, e.g. 30s, 5m, default is 1m
@@ -86,12 +86,8 @@ linters-settings:
     severity: "low"
     confidence: "low"
     excludes:
-    - G102
-    - G112
-    - G306
     - G401
     - G402
-    - G404
     - G501
 
 issues:

+ 22 - 10
Makefile.cross-compiles

@@ -2,22 +2,34 @@ export PATH := $(PATH):`go env GOPATH`/bin
 export GO111MODULE=on
 LDFLAGS := -s -w
 
-os-archs=darwin:amd64 darwin:arm64 freebsd:amd64 linux:amd64 linux:arm linux:arm64 windows:amd64 windows:arm64 linux:mips64 linux:mips64le linux:mips:softfloat linux:mipsle:softfloat linux:riscv64 android:arm64
+os-archs=darwin:amd64 darwin:arm64 freebsd:amd64 linux:amd64 linux:arm:7 linux:arm:5 linux:arm64 windows:amd64 windows:arm64 linux:mips64 linux:mips64le linux:mips:softfloat linux:mipsle:softfloat linux:riscv64 android:arm64
 
 all: build
 
 build: app
 
 app:
-	@$(foreach n, $(os-archs),\
-		os=$(shell echo "$(n)" | cut -d : -f 1);\
-		arch=$(shell echo "$(n)" | cut -d : -f 2);\
-		gomips=$(shell echo "$(n)" | cut -d : -f 3);\
-		target_suffix=$${os}_$${arch};\
-		echo "Build $${os}-$${arch}...";\
-		env CGO_ENABLED=0 GOOS=$${os} GOARCH=$${arch} GOMIPS=$${gomips} go build -trimpath -ldflags "$(LDFLAGS)" -tags frpc -o ./release/frpc_$${target_suffix} ./cmd/frpc;\
-		env CGO_ENABLED=0 GOOS=$${os} GOARCH=$${arch} GOMIPS=$${gomips} go build -trimpath -ldflags "$(LDFLAGS)" -tags frps -o ./release/frps_$${target_suffix} ./cmd/frps;\
-		echo "Build $${os}-$${arch} done";\
+	@$(foreach n, $(os-archs), \
+		os=$(shell echo "$(n)" | cut -d : -f 1); \
+		arch=$(shell echo "$(n)" | cut -d : -f 2); \
+		extra=$(shell echo "$(n)" | cut -d : -f 3); \
+		flags=''; \
+		target_suffix=$${os}_$${arch}; \
+		if [ "$${os}" = "linux" ] && [ "$${arch}" = "arm" ] && [ "$${extra}" != "" ] ; then \
+			if [ "$${extra}" = "7" ]; then \
+				flags=GOARM=7; \
+				target_suffix=$${os}_arm_hf; \
+			elif [ "$${extra}" = "5" ]; then \
+				flags=GOARM=5; \
+				target_suffix=$${os}_arm; \
+			fi; \
+		elif [ "$${os}" = "linux" ] && ([ "$${arch}" = "mips" ] || [ "$${arch}" = "mipsle" ]) && [ "$${extra}" != "" ] ; then \
+		    flags=GOMIPS=$${extra}; \
+		fi; \
+		echo "Build $${os}-$${arch}$${extra:+ ($${extra})}..."; \
+		env CGO_ENABLED=0 GOOS=$${os} GOARCH=$${arch} $${flags} go build -trimpath -ldflags "$(LDFLAGS)" -tags frpc -o ./release/frpc_$${target_suffix} ./cmd/frpc; \
+		env CGO_ENABLED=0 GOOS=$${os} GOARCH=$${arch} $${flags} go build -trimpath -ldflags "$(LDFLAGS)" -tags frps -o ./release/frps_$${target_suffix} ./cmd/frps; \
+		echo "Build $${os}-$${arch}$${extra:+ ($${extra})} done"; \
 	)
 	@mv ./release/frpc_windows_amd64 ./release/frpc_windows_amd64.exe
 	@mv ./release/frps_windows_amd64 ./release/frps_windows_amd64.exe

+ 11 - 5
README.md

@@ -82,6 +82,7 @@ frp also offers a P2P connect mode.
     * [Client Plugins](#client-plugins)
     * [Server Manage Plugins](#server-manage-plugins)
     * [SSH Tunnel Gateway](#ssh-tunnel-gateway)
+* [Releated Projects](#releated-projects)
 * [Contributing](#contributing)
 * [Donation](#donation)
     * [GitHub Sponsors](#github-sponsors)
@@ -351,7 +352,6 @@ You may substitute `https2https` for the plugin, and point the `localAddr` to a
   # frpc.toml
   serverAddr = "x.x.x.x"
   serverPort = 7000
-  vhostHTTPSPort = 443
 
   [[proxies]]
   name = "test_https2http"
@@ -804,7 +804,7 @@ You can disable this feature by modify `frps.toml` and `frpc.toml`:
 
 ```toml
 # frps.toml and frpc.toml, must be same
-tcpMux = false
+transport.tcpMux = false
 ```
 
 ### Support KCP Protocol
@@ -983,7 +983,7 @@ The HTTP request will have the `Host` header rewritten to `Host: dev.example.com
 
 ### Setting other HTTP Headers
 
-Similar to `Host`, You can override other HTTP request headers with proxy type `http`.
+Similar to `Host`, You can override other HTTP request and response headers with proxy type `http`.
 
 ```toml
 # frpc.toml
@@ -995,15 +995,16 @@ localPort = 80
 customDomains = ["test.example.com"]
 hostHeaderRewrite = "dev.example.com"
 requestHeaders.set.x-from-where = "frp"
+responseHeaders.set.foo = "bar"
 ```
 
-In this example, it will set header `x-from-where: frp` in the HTTP request.
+In this example, it will set header `x-from-where: frp` in the HTTP request and `foo: bar` in the HTTP response.
 
 ### Get Real IP
 
 #### HTTP X-Forwarded-For
 
-This feature is for http proxy only.
+This feature is for `http` proxies or proxies with the `https2http` and `https2https` plugins enabled.
 
 You can get user's real IP from HTTP request headers `X-Forwarded-For`.
 
@@ -1244,6 +1245,11 @@ frpc tcp --proxy_name "test-tcp" --local_ip 127.0.0.1 --local_port 8080 --remote
 
 Please refer to this [document](/doc/ssh_tunnel_gateway.md) for more information.
 
+## Releated Projects
+
+* [gofrp/plugin](https://github.com/gofrp/plugin) - A repository for frp plugins that contains a variety of plugins implemented based on the frp extension mechanism, meeting the customization needs of different scenarios.
+* [gofrp/tiny-frpc](https://github.com/gofrp/tiny-frpc) - A lightweight version of the frp client (around 3.5MB at minimum) implemented using the ssh protocol, supporting some of the most commonly used features, suitable for devices with limited resources.
+
 ## Contributing
 
 Interested in getting involved? We would like to help you!

+ 6 - 5
README_zh.md

@@ -72,7 +72,12 @@ frp 是一个免费且开源的项目,我们欢迎任何人为其开发和进
 * 贡献代码请提交 PR 至 dev 分支,master 分支仅用于发布稳定可用版本。
 * 如果你有任何其他方面的问题或合作,欢迎发送邮件至 fatedier@gmail.com 。
 
-**提醒:和项目相关的问题最好在 [issues](https://github.com/fatedier/frp/issues) 中反馈,这样方便其他有类似问题的人可以快速查找解决方法,并且也避免了我们重复回答一些问题。**
+**提醒:和项目相关的问题请在 [issues](https://github.com/fatedier/frp/issues) 中反馈,这样方便其他有类似问题的人可以快速查找解决方法,并且也避免了我们重复回答一些问题。**
+
+## 关联项目
+
+* [gofrp/plugin](https://github.com/gofrp/plugin) - frp 插件仓库,收录了基于 frp 扩展机制实现的各种插件,满足各种场景下的定制化需求。
+* [gofrp/tiny-frpc](https://github.com/gofrp/tiny-frpc) - 基于 ssh 协议实现的 frp 客户端的精简版本(最低约 3.5MB 左右),支持常用的部分功能,适用于资源有限的设备。
 
 ## 赞助
 
@@ -93,7 +98,3 @@ frp 是一个免费且开源的项目,我们欢迎任何人为其开发和进
 如果您想了解更多 frp 相关技术以及更新详解,或者寻求任何 frp 使用方面的帮助,都可以通过微信扫描下方的二维码付费加入知识星球的官方社群:
 
 ![zsxq](/doc/pic/zsxq.jpg)
-
-### 微信支付捐赠
-
-![donate-wechatpay](/doc/pic/donate-wechatpay.png)

+ 9 - 2
Release.md

@@ -1,7 +1,14 @@
+### Notable Changes
+
+We have optimized the heartbeat mechanism when tcpmux is enabled (enabled by default). The default value of `heartbeatInterval` has been adjusted to -1. This update ensures that when tcpmux is active, the client does not send additional heartbeats to the server. Since tcpmux incorporates its own heartbeat system, this change effectively reduces unnecessary data consumption, streamlining communication efficiency between client and server.
+
+When connecting to frps versions older than v0.39.0 might encounter compatibility issues due to changes in the heartbeat mechanism. As a temporary workaround, setting the `heartbeatInterval` to 30 can help maintain stable connectivity with these older versions. We recommend updating to the latest frps version to leverage full functionality and improvements.
+
 ### Features
 
-* `https2http` and `https2https` plugin now supports `X-Forwared-For` header.
+* Show tcpmux proxies on the frps dashboard.
+* `http` proxy can modify the response header. For example, `responseHeaders.set.foo = "bar"` will add a new header `foo: bar` to the response.
 
 ### Fixes
 
-* `X-Forwared-For` header is now correctly set in the request to the backend server for proxy type http.
+* When an HTTP proxy request times out, it returns 504 instead of 404 now.

File diff suppressed because it is too large
+ 1 - 1
assets/frps/static/index-82-40HIG.js


+ 1 - 1
assets/frps/static/index.html

@@ -4,7 +4,7 @@
 <head>
     <meta charset="utf-8">
     <title>frps dashboard</title>
-  <script type="module" crossorigin src="./index-Q42Pu2_S.js"></script>
+  <script type="module" crossorigin src="./index-82-40HIG.js"></script>
   <link rel="stylesheet" crossorigin href="./index-rzPDshRD.css">
 </head>
 

+ 1 - 1
client/admin_api.go

@@ -253,7 +253,7 @@ func (svr *Service) apiPutConfig(w http.ResponseWriter, r *http.Request) {
 		return
 	}
 
-	if err := os.WriteFile(svr.configFilePath, body, 0o644); err != nil {
+	if err := os.WriteFile(svr.configFilePath, body, 0o600); err != nil {
 		res.Code = 500
 		res.Msg = fmt.Sprintf("write content to frpc config file error: %v", err)
 		log.Warnf("%s", res.Msg)

+ 17 - 17
client/connector.go

@@ -24,7 +24,7 @@ import (
 	"sync"
 	"time"
 
-	libdial "github.com/fatedier/golib/net/dial"
+	libnet "github.com/fatedier/golib/net"
 	fmux "github.com/hashicorp/yamux"
 	quic "github.com/quic-go/quic-go"
 	"github.com/samber/lo"
@@ -169,44 +169,44 @@ func (c *defaultConnectorImpl) realConnect() (net.Conn, error) {
 		}
 	}
 
-	proxyType, addr, auth, err := libdial.ParseProxyURL(c.cfg.Transport.ProxyURL)
+	proxyType, addr, auth, err := libnet.ParseProxyURL(c.cfg.Transport.ProxyURL)
 	if err != nil {
 		xl.Errorf("fail to parse proxy url")
 		return nil, err
 	}
-	dialOptions := []libdial.DialOption{}
+	dialOptions := []libnet.DialOption{}
 	protocol := c.cfg.Transport.Protocol
 	switch protocol {
 	case "websocket":
 		protocol = "tcp"
-		dialOptions = append(dialOptions, libdial.WithAfterHook(libdial.AfterHook{Hook: netpkg.DialHookWebsocket(protocol, "")}))
-		dialOptions = append(dialOptions, libdial.WithAfterHook(libdial.AfterHook{
+		dialOptions = append(dialOptions, libnet.WithAfterHook(libnet.AfterHook{Hook: netpkg.DialHookWebsocket(protocol, "")}))
+		dialOptions = append(dialOptions, libnet.WithAfterHook(libnet.AfterHook{
 			Hook: netpkg.DialHookCustomTLSHeadByte(tlsConfig != nil, lo.FromPtr(c.cfg.Transport.TLS.DisableCustomTLSFirstByte)),
 		}))
-		dialOptions = append(dialOptions, libdial.WithTLSConfig(tlsConfig))
+		dialOptions = append(dialOptions, libnet.WithTLSConfig(tlsConfig))
 	case "wss":
 		protocol = "tcp"
-		dialOptions = append(dialOptions, libdial.WithTLSConfigAndPriority(100, tlsConfig))
+		dialOptions = append(dialOptions, libnet.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: netpkg.DialHookWebsocket(protocol, tlsConfig.ServerName), Priority: 110}))
+		dialOptions = append(dialOptions, libnet.WithAfterHook(libnet.AfterHook{Hook: netpkg.DialHookWebsocket(protocol, tlsConfig.ServerName), Priority: 110}))
 	default:
-		dialOptions = append(dialOptions, libdial.WithAfterHook(libdial.AfterHook{
+		dialOptions = append(dialOptions, libnet.WithAfterHook(libnet.AfterHook{
 			Hook: netpkg.DialHookCustomTLSHeadByte(tlsConfig != nil, lo.FromPtr(c.cfg.Transport.TLS.DisableCustomTLSFirstByte)),
 		}))
-		dialOptions = append(dialOptions, libdial.WithTLSConfig(tlsConfig))
+		dialOptions = append(dialOptions, libnet.WithTLSConfig(tlsConfig))
 	}
 
 	if c.cfg.Transport.ConnectServerLocalIP != "" {
-		dialOptions = append(dialOptions, libdial.WithLocalAddr(c.cfg.Transport.ConnectServerLocalIP))
+		dialOptions = append(dialOptions, libnet.WithLocalAddr(c.cfg.Transport.ConnectServerLocalIP))
 	}
 	dialOptions = append(dialOptions,
-		libdial.WithProtocol(protocol),
-		libdial.WithTimeout(time.Duration(c.cfg.Transport.DialServerTimeout)*time.Second),
-		libdial.WithKeepAlive(time.Duration(c.cfg.Transport.DialServerKeepAlive)*time.Second),
-		libdial.WithProxy(proxyType, addr),
-		libdial.WithProxyAuth(auth),
+		libnet.WithProtocol(protocol),
+		libnet.WithTimeout(time.Duration(c.cfg.Transport.DialServerTimeout)*time.Second),
+		libnet.WithKeepAlive(time.Duration(c.cfg.Transport.DialServerKeepAlive)*time.Second),
+		libnet.WithProxy(proxyType, addr),
+		libnet.WithProxyAuth(auth),
 	)
-	conn, err := libdial.DialContext(
+	conn, err := libnet.DialContext(
 		c.ctx,
 		net.JoinHostPort(c.cfg.ServerAddr, strconv.Itoa(c.cfg.ServerPort)),
 		dialOptions...,

+ 3 - 9
client/control.go

@@ -20,8 +20,6 @@ import (
 	"sync/atomic"
 	"time"
 
-	"github.com/samber/lo"
-
 	"github.com/fatedier/frp/client/proxy"
 	"github.com/fatedier/frp/client/visitor"
 	"github.com/fatedier/frp/pkg/auth"
@@ -236,10 +234,8 @@ func (ctl *Control) registerMsgHandlers() {
 func (ctl *Control) heartbeatWorker() {
 	xl := ctl.xl
 
-	// TODO(fatedier): Change default value of HeartbeatInterval to -1 if tcpmux is enabled.
-	// Users can still enable heartbeat feature by setting HeartbeatInterval to a positive value.
 	if ctl.sessionCtx.Common.Transport.HeartbeatInterval > 0 {
-		// send heartbeat to server
+		// Send heartbeat to server.
 		sendHeartBeat := func() (bool, error) {
 			xl.Debugf("send heartbeat to server")
 			pingMsg := &msg.Ping{}
@@ -263,10 +259,8 @@ func (ctl *Control) heartbeatWorker() {
 		)
 	}
 
-	// Check heartbeat timeout only if TCPMux is not enabled and users don't disable heartbeat feature.
-	if ctl.sessionCtx.Common.Transport.HeartbeatInterval > 0 && ctl.sessionCtx.Common.Transport.HeartbeatTimeout > 0 &&
-		!lo.FromPtr(ctl.sessionCtx.Common.Transport.TCPMux) {
-
+	// Check heartbeat timeout.
+	if ctl.sessionCtx.Common.Transport.HeartbeatInterval > 0 && ctl.sessionCtx.Common.Transport.HeartbeatTimeout > 0 {
 		go wait.Until(func() {
 			if time.Since(ctl.lastPong.Load().(time.Time)) > time.Duration(ctl.sessionCtx.Common.Transport.HeartbeatTimeout)*time.Second {
 				xl.Warnf("heartbeat timeout")

+ 3 - 3
client/proxy/proxy.go

@@ -25,7 +25,7 @@ import (
 	"time"
 
 	libio "github.com/fatedier/golib/io"
-	libdial "github.com/fatedier/golib/net/dial"
+	libnet "github.com/fatedier/golib/net"
 	pp "github.com/pires/go-proxyproto"
 	"golang.org/x/time/rate"
 
@@ -197,9 +197,9 @@ func (pxy *BaseProxy) HandleTCPWorkConnection(workConn net.Conn, m *msg.StartWor
 		return
 	}
 
-	localConn, err := libdial.Dial(
+	localConn, err := libnet.Dial(
 		net.JoinHostPort(baseCfg.LocalIP, strconv.Itoa(baseCfg.LocalPort)),
-		libdial.WithTimeout(10*time.Second),
+		libnet.WithTimeout(10*time.Second),
 	)
 	if err != nil {
 		workConn.Close()

+ 20 - 7
client/service.go

@@ -380,18 +380,31 @@ func (svr *Service) stop() {
 	}
 }
 
-// TODO(fatedier): Use StatusExporter to provide query interfaces instead of directly using methods from the Service.
-func (svr *Service) GetProxyStatus(name string) (*proxy.WorkingStatus, error) {
+func (svr *Service) getProxyStatus(name string) (*proxy.WorkingStatus, bool) {
 	svr.ctlMu.RLock()
 	ctl := svr.ctl
 	svr.ctlMu.RUnlock()
 
 	if ctl == nil {
-		return nil, fmt.Errorf("control is not running")
+		return nil, false
 	}
-	ws, ok := ctl.pm.GetProxyStatus(name)
-	if !ok {
-		return nil, fmt.Errorf("proxy [%s] is not found", name)
+	return ctl.pm.GetProxyStatus(name)
+}
+
+func (svr *Service) StatusExporter() StatusExporter {
+	return &statusExporterImpl{
+		getProxyStatusFunc: svr.getProxyStatus,
 	}
-	return ws, nil
+}
+
+type StatusExporter interface {
+	GetProxyStatus(name string) (*proxy.WorkingStatus, bool)
+}
+
+type statusExporterImpl struct {
+	getProxyStatusFunc func(name string) (*proxy.WorkingStatus, bool)
+}
+
+func (s *statusExporterImpl) GetProxyStatus(name string) (*proxy.WorkingStatus, bool) {
+	return s.getProxyStatusFunc(name)
 }

+ 1 - 0
conf/frpc_full_example.toml

@@ -209,6 +209,7 @@ locations = ["/", "/pic"]
 # routeByHTTPUser = abc
 hostHeaderRewrite = "example.com"
 requestHeaders.set.x-from-where = "frp"
+responseHeaders.set.foo = "bar"
 healthCheck.type = "http"
 # frpc will send a GET http request '/status' to local http service
 # http service is alive when it return 2xx http response code

+ 3 - 3
go.mod

@@ -5,10 +5,10 @@ go 1.22
 require (
 	github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5
 	github.com/coreos/go-oidc/v3 v3.10.0
-	github.com/fatedier/golib v0.4.2
+	github.com/fatedier/golib v0.5.0
 	github.com/google/uuid v1.6.0
 	github.com/gorilla/mux v1.8.1
-	github.com/gorilla/websocket v1.5.1
+	github.com/gorilla/websocket v1.5.0
 	github.com/hashicorp/yamux v0.1.1
 	github.com/onsi/ginkgo/v2 v2.17.1
 	github.com/onsi/gomega v1.32.0
@@ -35,7 +35,7 @@ require (
 )
 
 require (
-	github.com/Azure/go-ntlmssp v0.0.0-20200615164410-66371956d46c // indirect
+	github.com/Azure/go-ntlmssp v0.0.0-20221128193559-754e69321358 // indirect
 	github.com/beorn7/perks v1.0.1 // indirect
 	github.com/cespare/xxhash/v2 v2.2.0 // indirect
 	github.com/davecgh/go-spew v1.1.1 // indirect

+ 6 - 6
go.sum

@@ -1,6 +1,6 @@
 cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
-github.com/Azure/go-ntlmssp v0.0.0-20200615164410-66371956d46c h1:/IBSNwUN8+eKzUzbJPqhK839ygXJ82sde8x3ogr6R28=
-github.com/Azure/go-ntlmssp v0.0.0-20200615164410-66371956d46c/go.mod h1:chxPXzSsl7ZWRAuOIE23GDNzjWuZquvFlgA8xmpunjU=
+github.com/Azure/go-ntlmssp v0.0.0-20221128193559-754e69321358 h1:mFRzDkZVAjdal+s7s0MwaRv9igoPqLRdzOLzw/8Xvq8=
+github.com/Azure/go-ntlmssp v0.0.0-20221128193559-754e69321358/go.mod h1:chxPXzSsl7ZWRAuOIE23GDNzjWuZquvFlgA8xmpunjU=
 github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
 github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5 h1:0CwZNZbxp69SHPdPJAN/hZIm0C4OItdklCFmMRWYpio=
 github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5/go.mod h1:wHh0iHkYZB8zMSxRWpUBQtwG5a7fFgvEO+odwuTv2gs=
@@ -24,8 +24,8 @@ github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSs
 github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
 github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98=
 github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
-github.com/fatedier/golib v0.4.2 h1:k+ZBdUFTTipnP1RHfEhGbzyShRdz/rZtFGnjpXG9D9c=
-github.com/fatedier/golib v0.4.2/go.mod h1:gpu+1vXxtJ072NYaNsn/YWgojDL8Ap2kFZQtbzT2qkg=
+github.com/fatedier/golib v0.5.0 h1:hNcH7hgfIFqVWbP+YojCCAj4eO94pPf4dEF8lmq2jWs=
+github.com/fatedier/golib v0.5.0/go.mod h1:W6kIYkIFxHsTzbgqg5piCxIiDo4LzwgTY6R5W8l9NFQ=
 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/go-jose/go-jose/v4 v4.0.1 h1:QVEPDE3OluqXBQZDcnNvQrInro2h0e4eqNbnZSWqS6U=
@@ -64,8 +64,8 @@ github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
 github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
 github.com/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY=
 github.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWSiMQ=
-github.com/gorilla/websocket v1.5.1 h1:gmztn0JnHVt9JZquRuzLw3g4wouNVzKL15iLr/zn/QY=
-github.com/gorilla/websocket v1.5.1/go.mod h1:x3kM2JMyaluk02fnUJpQuwD2dCS5NDG2ZHL0uE0tcaY=
+github.com/gorilla/websocket v1.5.0 h1:PPwGk2jz7EePpoHN/+ClbZu8SPxiqlu12wZP/3sWmnc=
+github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
 github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
 github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
 github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=

+ 42 - 35
package.sh

@@ -19,48 +19,55 @@ mkdir -p ./release/packages
 
 os_all='linux windows darwin freebsd android'
 arch_all='386 amd64 arm arm64 mips64 mips64le mips mipsle riscv64'
+extra_all='_ hf'
 
 cd ./release
 
 for os in $os_all; do
     for arch in $arch_all; do
-        frp_dir_name="frp_${frp_version}_${os}_${arch}"
-        frp_path="./packages/frp_${frp_version}_${os}_${arch}"
-
-        if [ "x${os}" = x"windows" ]; then
-            if [ ! -f "./frpc_${os}_${arch}.exe" ]; then
-                continue
-            fi
-            if [ ! -f "./frps_${os}_${arch}.exe" ]; then
-                continue
-            fi
-            mkdir ${frp_path}
-            mv ./frpc_${os}_${arch}.exe ${frp_path}/frpc.exe
-            mv ./frps_${os}_${arch}.exe ${frp_path}/frps.exe
-        else
-            if [ ! -f "./frpc_${os}_${arch}" ]; then
-                continue
+        for extra in $extra_all; do
+            suffix="${os}_${arch}"
+            if [ "x${extra}" != x"_" ]; then
+                suffix="${os}_${arch}_${extra}"
             fi
-            if [ ! -f "./frps_${os}_${arch}" ]; then
-                continue
-            fi
-            mkdir ${frp_path}
-            mv ./frpc_${os}_${arch} ${frp_path}/frpc
-            mv ./frps_${os}_${arch} ${frp_path}/frps
-        fi  
-        cp ../LICENSE ${frp_path}
-        cp -f ../conf/frpc.toml ${frp_path}
-        cp -f ../conf/frps.toml ${frp_path}
+            frp_dir_name="frp_${frp_version}_${suffix}"
+            frp_path="./packages/frp_${frp_version}_${suffix}"
+
+            if [ "x${os}" = x"windows" ]; then
+                if [ ! -f "./frpc_${os}_${arch}.exe" ]; then
+                    continue
+                fi
+                if [ ! -f "./frps_${os}_${arch}.exe" ]; then
+                    continue
+                fi
+                mkdir ${frp_path}
+                mv ./frpc_${os}_${arch}.exe ${frp_path}/frpc.exe
+                mv ./frps_${os}_${arch}.exe ${frp_path}/frps.exe
+            else
+                if [ ! -f "./frpc_${suffix}" ]; then
+                    continue
+                fi
+                if [ ! -f "./frps_${suffix}" ]; then
+                    continue
+                fi
+                mkdir ${frp_path}
+                mv ./frpc_${suffix} ${frp_path}/frpc
+                mv ./frps_${suffix} ${frp_path}/frps
+            fi  
+            cp ../LICENSE ${frp_path}
+            cp -f ../conf/frpc.toml ${frp_path}
+            cp -f ../conf/frps.toml ${frp_path}
 
-        # packages
-        cd ./packages
-        if [ "x${os}" = x"windows" ]; then
-            zip -rq ${frp_dir_name}.zip ${frp_dir_name}
-        else
-            tar -zcf ${frp_dir_name}.tar.gz ${frp_dir_name}
-        fi  
-        cd ..
-        rm -rf ${frp_path}
+            # packages
+            cd ./packages
+            if [ "x${os}" = x"windows" ]; then
+                zip -rq ${frp_dir_name}.zip ${frp_dir_name}
+            else
+                tar -zcf ${frp_dir_name}.tar.gz ${frp_dir_name}
+            fi  
+            cd ..
+            rm -rf ${frp_path}
+        done
     done
 done
 

+ 8 - 2
pkg/config/v1/client.go

@@ -136,8 +136,14 @@ func (c *ClientTransportConfig) Complete() {
 	c.PoolCount = util.EmptyOr(c.PoolCount, 1)
 	c.TCPMux = util.EmptyOr(c.TCPMux, lo.ToPtr(true))
 	c.TCPMuxKeepaliveInterval = util.EmptyOr(c.TCPMuxKeepaliveInterval, 60)
-	c.HeartbeatInterval = util.EmptyOr(c.HeartbeatInterval, 30)
-	c.HeartbeatTimeout = util.EmptyOr(c.HeartbeatTimeout, 90)
+	if lo.FromPtr(c.TCPMux) {
+		// If TCPMux is enabled, heartbeat of application layer is unnecessary because we can rely on heartbeat in tcpmux.
+		c.HeartbeatInterval = util.EmptyOr(c.HeartbeatInterval, -1)
+		c.HeartbeatTimeout = util.EmptyOr(c.HeartbeatTimeout, -1)
+	} else {
+		c.HeartbeatInterval = util.EmptyOr(c.HeartbeatInterval, 30)
+		c.HeartbeatTimeout = util.EmptyOr(c.HeartbeatTimeout, 90)
+	}
 	c.QUIC.Complete()
 	c.TLS.Complete()
 }

+ 3 - 0
pkg/config/v1/proxy.go

@@ -291,6 +291,7 @@ type HTTPProxyConfig struct {
 	HTTPPassword      string           `json:"httpPassword,omitempty"`
 	HostHeaderRewrite string           `json:"hostHeaderRewrite,omitempty"`
 	RequestHeaders    HeaderOperations `json:"requestHeaders,omitempty"`
+	ResponseHeaders   HeaderOperations `json:"responseHeaders,omitempty"`
 	RouteByHTTPUser   string           `json:"routeByHTTPUser,omitempty"`
 }
 
@@ -304,6 +305,7 @@ func (c *HTTPProxyConfig) MarshalToMsg(m *msg.NewProxy) {
 	m.HTTPUser = c.HTTPUser
 	m.HTTPPwd = c.HTTPPassword
 	m.Headers = c.RequestHeaders.Set
+	m.ResponseHeaders = c.ResponseHeaders.Set
 	m.RouteByHTTPUser = c.RouteByHTTPUser
 }
 
@@ -317,6 +319,7 @@ func (c *HTTPProxyConfig) UnmarshalFromMsg(m *msg.NewProxy) {
 	c.HTTPUser = m.HTTPUser
 	c.HTTPPassword = m.HTTPPwd
 	c.RequestHeaders.Set = m.Headers
+	c.ResponseHeaders.Set = m.ResponseHeaders
 	c.RouteByHTTPUser = m.RouteByHTTPUser
 }
 

+ 6 - 1
pkg/config/v1/server.go

@@ -179,7 +179,12 @@ func (c *ServerTransportConfig) Complete() {
 	c.TCPMuxKeepaliveInterval = util.EmptyOr(c.TCPMuxKeepaliveInterval, 60)
 	c.TCPKeepAlive = util.EmptyOr(c.TCPKeepAlive, 7200)
 	c.MaxPoolCount = util.EmptyOr(c.MaxPoolCount, 5)
-	c.HeartbeatTimeout = util.EmptyOr(c.HeartbeatTimeout, 90)
+	if lo.FromPtr(c.TCPMux) {
+		// If TCPMux is enabled, heartbeat of application layer is unnecessary because we can rely on heartbeat in tcpmux.
+		c.HeartbeatTimeout = util.EmptyOr(c.HeartbeatTimeout, -1)
+	} else {
+		c.HeartbeatTimeout = util.EmptyOr(c.HeartbeatTimeout, 90)
+	}
 	c.QUIC.Complete()
 	if c.TLS.TrustedCaFile != "" {
 		c.TLS.Force = true

+ 1 - 0
pkg/msg/msg.go

@@ -121,6 +121,7 @@ type NewProxy struct {
 	HTTPPwd           string            `json:"http_pwd,omitempty"`
 	HostHeaderRewrite string            `json:"host_header_rewrite,omitempty"`
 	Headers           map[string]string `json:"headers,omitempty"`
+	ResponseHeaders   map[string]string `json:"response_headers,omitempty"`
 	RouteByHTTPUser   string            `json:"route_by_http_user,omitempty"`
 
 	// stcp, sudp, xtcp

+ 7 - 1
pkg/plugin/client/http2https.go

@@ -19,11 +19,15 @@ package plugin
 import (
 	"crypto/tls"
 	"io"
+	stdlog "log"
 	"net"
 	"net/http"
 	"net/http/httputil"
 
+	"github.com/fatedier/golib/pool"
+
 	v1 "github.com/fatedier/frp/pkg/config/v1"
+	"github.com/fatedier/frp/pkg/util/log"
 	netpkg "github.com/fatedier/frp/pkg/util/net"
 )
 
@@ -67,7 +71,9 @@ func NewHTTP2HTTPSPlugin(options v1.ClientPluginOptions) (Plugin, error) {
 				req.Header.Set(k, v)
 			}
 		},
-		Transport: tr,
+		Transport:  tr,
+		BufferPool: pool.NewBuffer(32 * 1024),
+		ErrorLog:   stdlog.New(log.NewWriteLogger(log.WarnLevel, 2), "", 0),
 	}
 
 	p.s = &http.Server{

+ 2 - 1
pkg/plugin/client/http_proxy.go

@@ -54,7 +54,8 @@ func NewHTTPProxyPlugin(options v1.ClientPluginOptions) (Plugin, error) {
 	}
 
 	hp.s = &http.Server{
-		Handler: hp,
+		Handler:           hp,
+		ReadHeaderTimeout: 60 * time.Second,
 	}
 
 	go func() {

+ 9 - 1
pkg/plugin/client/https2http.go

@@ -20,12 +20,17 @@ import (
 	"crypto/tls"
 	"fmt"
 	"io"
+	stdlog "log"
 	"net"
 	"net/http"
 	"net/http/httputil"
+	"time"
+
+	"github.com/fatedier/golib/pool"
 
 	v1 "github.com/fatedier/frp/pkg/config/v1"
 	"github.com/fatedier/frp/pkg/transport"
+	"github.com/fatedier/frp/pkg/util/log"
 	netpkg "github.com/fatedier/frp/pkg/util/net"
 )
 
@@ -63,10 +68,13 @@ func NewHTTPS2HTTPPlugin(options v1.ClientPluginOptions) (Plugin, error) {
 				req.Header.Set(k, v)
 			}
 		},
+		BufferPool: pool.NewBuffer(32 * 1024),
+		ErrorLog:   stdlog.New(log.NewWriteLogger(log.WarnLevel, 2), "", 0),
 	}
 
 	p.s = &http.Server{
-		Handler: rp,
+		Handler:           rp,
+		ReadHeaderTimeout: 60 * time.Second,
 	}
 
 	var (

+ 10 - 2
pkg/plugin/client/https2https.go

@@ -20,12 +20,17 @@ import (
 	"crypto/tls"
 	"fmt"
 	"io"
+	stdlog "log"
 	"net"
 	"net/http"
 	"net/http/httputil"
+	"time"
+
+	"github.com/fatedier/golib/pool"
 
 	v1 "github.com/fatedier/frp/pkg/config/v1"
 	"github.com/fatedier/frp/pkg/transport"
+	"github.com/fatedier/frp/pkg/util/log"
 	netpkg "github.com/fatedier/frp/pkg/util/net"
 )
 
@@ -68,11 +73,14 @@ func NewHTTPS2HTTPSPlugin(options v1.ClientPluginOptions) (Plugin, error) {
 				req.Header.Set(k, v)
 			}
 		},
-		Transport: tr,
+		Transport:  tr,
+		BufferPool: pool.NewBuffer(32 * 1024),
+		ErrorLog:   stdlog.New(log.NewWriteLogger(log.WarnLevel, 2), "", 0),
 	}
 
 	p.s = &http.Server{
-		Handler: rp,
+		Handler:           rp,
+		ReadHeaderTimeout: 60 * time.Second,
 	}
 
 	var (

+ 2 - 1
pkg/plugin/client/static_file.go

@@ -60,7 +60,8 @@ func NewStaticFilePlugin(options v1.ClientPluginOptions) (Plugin, error) {
 	router.Use(netpkg.NewHTTPAuthMiddleware(opts.HTTPUser, opts.HTTPPassword).SetAuthFailDelay(200 * time.Millisecond).Middleware)
 	router.PathPrefix(prefix).Handler(netpkg.MakeHTTPGzipHandler(http.StripPrefix(prefix, http.FileServer(http.Dir(opts.LocalPath))))).Methods("GET")
 	sp.s = &http.Server{
-		Handler: router,
+		Handler:           router,
+		ReadHeaderTimeout: 60 * time.Second,
 	}
 	go func() {
 		_ = sp.s.Serve(listener)

+ 4 - 2
pkg/ssh/server.go

@@ -363,11 +363,13 @@ func (s *TunnelServer) waitProxyStatusReady(name string, timeout time.Duration)
 	timer := time.NewTimer(timeout)
 	defer timer.Stop()
 
+	statusExporter := s.vc.Service().StatusExporter()
+
 	for {
 		select {
 		case <-ticker.C:
-			ps, err := s.vc.Service().GetProxyStatus(name)
-			if err != nil {
+			ps, ok := statusExporter.GetProxyStatus(name)
+			if !ok {
 				continue
 			}
 			switch ps.Phase {

+ 30 - 0
pkg/util/log/log.go

@@ -15,11 +15,20 @@
 package log
 
 import (
+	"bytes"
 	"os"
 
 	"github.com/fatedier/golib/log"
 )
 
+var (
+	TraceLevel = log.TraceLevel
+	DebugLevel = log.DebugLevel
+	InfoLevel  = log.InfoLevel
+	WarnLevel  = log.WarnLevel
+	ErrorLevel = log.ErrorLevel
+)
+
 var Logger *log.Logger
 
 func init() {
@@ -77,3 +86,24 @@ func Debugf(format string, v ...interface{}) {
 func Tracef(format string, v ...interface{}) {
 	Logger.Tracef(format, v...)
 }
+
+func Logf(level log.Level, offset int, format string, v ...interface{}) {
+	Logger.Logf(level, offset, format, v...)
+}
+
+type WriteLogger struct {
+	level  log.Level
+	offset int
+}
+
+func NewWriteLogger(level log.Level, offset int) *WriteLogger {
+	return &WriteLogger{
+		level:  level,
+		offset: offset,
+	}
+}
+
+func (w *WriteLogger) Write(p []byte) (n int, err error) {
+	Logger.Log(w.level, w.offset, string(bytes.TrimRight(p, "\n")))
+	return len(p), nil
+}

+ 3 - 3
pkg/util/net/dial.go

@@ -5,11 +5,11 @@ import (
 	"net"
 	"net/url"
 
-	libdial "github.com/fatedier/golib/net/dial"
+	libnet "github.com/fatedier/golib/net"
 	"golang.org/x/net/websocket"
 )
 
-func DialHookCustomTLSHeadByte(enableTLS bool, disableCustomTLSHeadByte bool) libdial.AfterHookFunc {
+func DialHookCustomTLSHeadByte(enableTLS bool, disableCustomTLSHeadByte bool) libnet.AfterHookFunc {
 	return func(ctx context.Context, c net.Conn, addr string) (context.Context, net.Conn, error) {
 		if enableTLS && !disableCustomTLSHeadByte {
 			_, err := c.Write([]byte{byte(FRPTLSHeadByte)})
@@ -21,7 +21,7 @@ func DialHookCustomTLSHeadByte(enableTLS bool, disableCustomTLSHeadByte bool) li
 	}
 }
 
-func DialHookWebsocket(protocol string, host string) libdial.AfterHookFunc {
+func DialHookWebsocket(protocol string, host string) libnet.AfterHookFunc {
 	return func(ctx context.Context, c net.Conn, addr string) (context.Context, net.Conn, error) {
 		if protocol != "wss" {
 			protocol = "ws"

+ 4 - 2
pkg/util/net/websocket.go

@@ -4,6 +4,7 @@ import (
 	"errors"
 	"net"
 	"net/http"
+	"time"
 
 	"golang.org/x/net/websocket"
 )
@@ -39,8 +40,9 @@ func NewWebsocketListener(ln net.Listener) (wl *WebsocketListener) {
 	}))
 
 	wl.server = &http.Server{
-		Addr:    ln.Addr().String(),
-		Handler: muxer,
+		Addr:              ln.Addr().String(),
+		Handler:           muxer,
+		ReadHeaderTimeout: 60 * time.Second,
 	}
 
 	go func() {

+ 1 - 1
pkg/util/version/version.go

@@ -14,7 +14,7 @@
 
 package version
 
-var version = "0.57.0"
+var version = "0.58.0"
 
 func Full() string {
 	return version

+ 26 - 32
pkg/util/vhost/http.go

@@ -15,7 +15,6 @@
 package vhost
 
 import (
-	"bytes"
 	"context"
 	"encoding/base64"
 	"errors"
@@ -64,9 +63,9 @@ func NewHTTPReverseProxy(option HTTPReverseProxyOptions, vhostRouter *Routers) *
 			req := r.Out
 			req.URL.Scheme = "http"
 			reqRouteInfo := req.Context().Value(RouteInfoKey).(*RequestRouteInfo)
-			oldHost, _ := httppkg.CanonicalHost(reqRouteInfo.Host)
+			originalHost, _ := httppkg.CanonicalHost(reqRouteInfo.Host)
 
-			rc := rp.GetRouteConfig(oldHost, reqRouteInfo.URL, reqRouteInfo.HTTPUser)
+			rc := req.Context().Value(RouteConfigKey).(*RouteConfig)
 			if rc != nil {
 				if rc.RewriteHost != "" {
 					req.Host = rc.RewriteHost
@@ -78,7 +77,7 @@ func NewHTTPReverseProxy(option HTTPReverseProxyOptions, vhostRouter *Routers) *
 					endpoint, _ = rc.ChooseEndpointFn()
 					reqRouteInfo.Endpoint = endpoint
 					log.Tracef("choose endpoint name [%s] for http request host [%s] path [%s] httpuser [%s]",
-						endpoint, oldHost, reqRouteInfo.URL, reqRouteInfo.HTTPUser)
+						endpoint, originalHost, reqRouteInfo.URL, reqRouteInfo.HTTPUser)
 				}
 				// Set {domain}.{location}.{routeByHTTPUser}.{endpoint} as URL host here to let http transport reuse connections.
 				req.URL.Host = rc.Domain + "." +
@@ -93,6 +92,15 @@ func NewHTTPReverseProxy(option HTTPReverseProxyOptions, vhostRouter *Routers) *
 				req.URL.Host = req.Host
 			}
 		},
+		ModifyResponse: func(r *http.Response) error {
+			rc := r.Request.Context().Value(RouteConfigKey).(*RouteConfig)
+			if rc != nil {
+				for k, v := range rc.ResponseHeaders {
+					r.Header.Set(k, v)
+				}
+			}
+			return nil
+		},
 		// Create a connection to one proxy routed by route policy.
 		Transport: &http.Transport{
 			ResponseHeaderTimeout: rp.responseHeaderTimeout,
@@ -116,10 +124,16 @@ func NewHTTPReverseProxy(option HTTPReverseProxyOptions, vhostRouter *Routers) *
 				return nil, nil
 			},
 		},
-		BufferPool: newWrapPool(),
-		ErrorLog:   stdlog.New(newWrapLogger(), "", 0),
+		BufferPool: pool.NewBuffer(32 * 1024),
+		ErrorLog:   stdlog.New(log.NewWriteLogger(log.WarnLevel, 2), "", 0),
 		ErrorHandler: func(rw http.ResponseWriter, req *http.Request, err error) {
-			log.Warnf("do http proxy request [host: %s] error: %v", req.Host, err)
+			log.Logf(log.WarnLevel, 1, "do http proxy request [host: %s] error: %v", req.Host, err)
+			if err != nil {
+				if e, ok := err.(net.Error); ok && e.Timeout() {
+					rw.WriteHeader(http.StatusGatewayTimeout)
+					return
+				}
+			}
 			rw.WriteHeader(http.StatusNotFound)
 			_, _ = rw.Write(getNotFoundPageContent())
 		},
@@ -152,14 +166,6 @@ func (rp *HTTPReverseProxy) GetRouteConfig(domain, location, routeByHTTPUser str
 	return nil
 }
 
-func (rp *HTTPReverseProxy) GetHeaders(domain, location, routeByHTTPUser string) (headers map[string]string) {
-	vr, ok := rp.getVhost(domain, location, routeByHTTPUser)
-	if ok {
-		headers = vr.payload.(*RouteConfig).Headers
-	}
-	return
-}
-
 // CreateConnection create a new connection by route config
 func (rp *HTTPReverseProxy) CreateConnection(reqRouteInfo *RequestRouteInfo, byEndpoint bool) (net.Conn, error) {
 	host, _ := httppkg.CanonicalHost(reqRouteInfo.Host)
@@ -300,8 +306,13 @@ func (rp *HTTPReverseProxy) injectRequestInfoToCtx(req *http.Request) *http.Requ
 		RemoteAddr: req.RemoteAddr,
 		URLHost:    req.URL.Host,
 	}
+
+	originalHost, _ := httppkg.CanonicalHost(reqRouteInfo.Host)
+	rc := rp.GetRouteConfig(originalHost, reqRouteInfo.URL, reqRouteInfo.HTTPUser)
+
 	newctx := req.Context()
 	newctx = context.WithValue(newctx, RouteInfoKey, reqRouteInfo)
+	newctx = context.WithValue(newctx, RouteConfigKey, rc)
 	return req.Clone(newctx)
 }
 
@@ -322,20 +333,3 @@ func (rp *HTTPReverseProxy) ServeHTTP(rw http.ResponseWriter, req *http.Request)
 		rp.proxy.ServeHTTP(rw, newreq)
 	}
 }
-
-type wrapPool struct{}
-
-func newWrapPool() *wrapPool { return &wrapPool{} }
-
-func (p *wrapPool) Get() []byte { return pool.GetBuf(32 * 1024) }
-
-func (p *wrapPool) Put(buf []byte) { pool.PutBuf(buf) }
-
-type wrapLogger struct{}
-
-func newWrapLogger() *wrapLogger { return &wrapLogger{} }
-
-func (l *wrapLogger) Write(p []byte) (n int, err error) {
-	log.Warnf("%s", string(bytes.TrimRight(p, "\n")))
-	return len(p), nil
-}

+ 1 - 1
pkg/util/vhost/https_test.go

@@ -12,7 +12,7 @@ import (
 func TestGetHTTPSHostname(t *testing.T) {
 	require := require.New(t)
 
-	l, err := net.Listen("tcp", ":")
+	l, err := net.Listen("tcp", "127.0.0.1:")
 	require.NoError(err)
 	defer l.Close()
 

+ 3 - 1
pkg/util/vhost/vhost.go

@@ -29,7 +29,8 @@ import (
 type RouteInfo string
 
 const (
-	RouteInfoKey RouteInfo = "routeInfo"
+	RouteInfoKey   RouteInfo = "routeInfo"
+	RouteConfigKey RouteInfo = "routeConfig"
 )
 
 type RequestRouteInfo struct {
@@ -113,6 +114,7 @@ type RouteConfig struct {
 	Username        string
 	Password        string
 	Headers         map[string]string
+	ResponseHeaders map[string]string
 	RouteByHTTPUser string
 
 	CreateConnFn           CreateConnFunc

+ 11 - 13
server/control.go

@@ -297,20 +297,18 @@ func (ctl *Control) GetWorkConn() (workConn net.Conn, err error) {
 }
 
 func (ctl *Control) heartbeatWorker() {
-	xl := ctl.xl
-
-	// Don't need application heartbeat if TCPMux is enabled,
-	// yamux will do same thing.
-	// TODO(fatedier): let default HeartbeatTimeout to -1 if TCPMux is enabled. Users can still set it to positive value to enable it.
-	if !lo.FromPtr(ctl.serverCfg.Transport.TCPMux) && ctl.serverCfg.Transport.HeartbeatTimeout > 0 {
-		go wait.Until(func() {
-			if time.Since(ctl.lastPing.Load().(time.Time)) > time.Duration(ctl.serverCfg.Transport.HeartbeatTimeout)*time.Second {
-				xl.Warnf("heartbeat timeout")
-				ctl.conn.Close()
-				return
-			}
-		}, time.Second, ctl.doneCh)
+	if ctl.serverCfg.Transport.HeartbeatTimeout <= 0 {
+		return
 	}
+
+	xl := ctl.xl
+	go wait.Until(func() {
+		if time.Since(ctl.lastPing.Load().(time.Time)) > time.Duration(ctl.serverCfg.Transport.HeartbeatTimeout)*time.Second {
+			xl.Warnf("heartbeat timeout")
+			ctl.conn.Close()
+			return
+		}
+	}, time.Second, ctl.doneCh)
 }
 
 // block until Control closed

+ 2 - 3
server/dashboard_api.go

@@ -32,8 +32,6 @@ import (
 	"github.com/fatedier/frp/pkg/util/version"
 )
 
-// TODO(fatedier): add an API to clean status of all offline proxies.
-
 type GeneralResponse struct {
 	Code int
 	Msg  string
@@ -146,7 +144,8 @@ type TCPOutConf struct {
 type TCPMuxOutConf struct {
 	BaseOutConf
 	v1.DomainConfig
-	Multiplexer string `json:"multiplexer"`
+	Multiplexer     string `json:"multiplexer"`
+	RouteByHTTPUser string `json:"routeByHTTPUser"`
 }
 
 type UDPOutConf struct {

+ 1 - 0
server/proxy/http.go

@@ -58,6 +58,7 @@ func (pxy *HTTPProxy) Run() (remoteAddr string, err error) {
 		RewriteHost:     pxy.cfg.HostHeaderRewrite,
 		RouteByHTTPUser: pxy.cfg.RouteByHTTPUser,
 		Headers:         pxy.cfg.RequestHeaders.Set,
+		ResponseHeaders: pxy.cfg.ResponseHeaders.Set,
 		Username:        pxy.cfg.HTTPUser,
 		Password:        pxy.cfg.HTTPPassword,
 		CreateConnFn:    pxy.GetRealConn,

+ 5 - 4
server/service.go

@@ -286,12 +286,13 @@ func NewService(cfg *v1.ServerConfig) (*Service, error) {
 
 		address := net.JoinHostPort(cfg.ProxyBindAddr, strconv.Itoa(cfg.VhostHTTPPort))
 		server := &http.Server{
-			Addr:    address,
-			Handler: rp,
+			Addr:              address,
+			Handler:           rp,
+			ReadHeaderTimeout: 60 * time.Second,
 		}
 		var l net.Listener
 		if httpMuxOn {
-			l = svr.muxer.ListenHttp(1)
+			l = svr.muxer.ListenHTTP(1)
 		} else {
 			l, err = net.Listen("tcp", address)
 			if err != nil {
@@ -308,7 +309,7 @@ func NewService(cfg *v1.ServerConfig) (*Service, error) {
 	if cfg.VhostHTTPSPort > 0 {
 		var l net.Listener
 		if httpsMuxOn {
-			l = svr.muxer.ListenHttps(1)
+			l = svr.muxer.ListenHTTPS(1)
 		} else {
 			address := net.JoinHostPort(cfg.ProxyBindAddr, strconv.Itoa(cfg.VhostHTTPSPort))
 			l, err = net.Listen("tcp", address)

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

@@ -260,7 +260,7 @@ func (f *Framework) SetEnvs(envs []string) {
 
 func (f *Framework) WriteTempFile(name string, content string) string {
 	filePath := filepath.Join(f.TempDirectory, name)
-	err := os.WriteFile(filePath, []byte(content), 0o766)
+	err := os.WriteFile(filePath, []byte(content), 0o600)
 	ExpectNoError(err)
 	return filePath
 }

+ 3 - 3
test/e2e/framework/process.go

@@ -27,7 +27,7 @@ func (f *Framework) RunProcesses(serverTemplates []string, clientTemplates []str
 	currentServerProcesses := make([]*process.Process, 0, len(serverTemplates))
 	for i := range serverTemplates {
 		path := filepath.Join(f.TempDirectory, fmt.Sprintf("frp-e2e-server-%d", i))
-		err = os.WriteFile(path, []byte(outs[i]), 0o666)
+		err = os.WriteFile(path, []byte(outs[i]), 0o600)
 		ExpectNoError(err)
 
 		if TestContext.Debug {
@@ -48,7 +48,7 @@ func (f *Framework) RunProcesses(serverTemplates []string, clientTemplates []str
 	for i := range clientTemplates {
 		index := i + len(serverTemplates)
 		path := filepath.Join(f.TempDirectory, fmt.Sprintf("frp-e2e-client-%d", i))
-		err = os.WriteFile(path, []byte(outs[index]), 0o666)
+		err = os.WriteFile(path, []byte(outs[index]), 0o600)
 		ExpectNoError(err)
 
 		if TestContext.Debug {
@@ -94,7 +94,7 @@ func (f *Framework) RunFrpc(args ...string) (*process.Process, string, error) {
 func (f *Framework) GenerateConfigFile(content string) string {
 	f.configFileIndex++
 	path := filepath.Join(f.TempDirectory, fmt.Sprintf("frp-e2e-config-%d", f.configFileIndex))
-	err := os.WriteFile(path, []byte(content), 0o666)
+	err := os.WriteFile(path, []byte(content), 0o600)
 	ExpectNoError(err)
 	return path
 }

+ 3 - 3
test/e2e/pkg/request/request.go

@@ -12,7 +12,7 @@ import (
 	"strconv"
 	"time"
 
-	libdial "github.com/fatedier/golib/net/dial"
+	libnet "github.com/fatedier/golib/net"
 
 	httppkg "github.com/fatedier/frp/pkg/util/http"
 	"github.com/fatedier/frp/test/e2e/pkg/rpc"
@@ -160,11 +160,11 @@ func (r *Request) Do() (*Response, error) {
 		if r.protocol != "tcp" {
 			return nil, fmt.Errorf("only tcp protocol is allowed for proxy")
 		}
-		proxyType, proxyAddress, auth, err := libdial.ParseProxyURL(r.proxyURL)
+		proxyType, proxyAddress, auth, err := libnet.ParseProxyURL(r.proxyURL)
 		if err != nil {
 			return nil, fmt.Errorf("parse ProxyURL error: %v", err)
 		}
-		conn, err = libdial.Dial(addr, libdial.WithProxy(proxyType, proxyAddress), libdial.WithProxyAuth(auth))
+		conn, err = libnet.Dial(addr, libnet.WithProxy(proxyType, proxyAddress), libnet.WithProxyAuth(auth))
 		if err != nil {
 			return nil, err
 		}

+ 80 - 2
test/e2e/v1/basic/http.go

@@ -5,6 +5,7 @@ import (
 	"net/http"
 	"net/url"
 	"strconv"
+	"time"
 
 	"github.com/gorilla/websocket"
 	"github.com/onsi/ginkgo/v2"
@@ -266,7 +267,7 @@ var _ = ginkgo.Describe("[Feature: HTTP]", func() {
 			Ensure()
 	})
 
-	ginkgo.It("Modify headers", func() {
+	ginkgo.It("Modify request headers", func() {
 		vhostHTTPPort := f.AllocPort()
 		serverConf := getDefaultServerConf(vhostHTTPPort)
 
@@ -291,7 +292,6 @@ var _ = ginkgo.Describe("[Feature: HTTP]", func() {
 
 		f.RunProcesses([]string{serverConf}, []string{clientConf})
 
-		// not set auth header
 		framework.NewRequestExpect(f).Port(vhostHTTPPort).
 			RequestModify(func(r *request.Request) {
 				r.HTTP().HTTPHost("normal.example.com")
@@ -300,6 +300,40 @@ var _ = ginkgo.Describe("[Feature: HTTP]", func() {
 			Ensure()
 	})
 
+	ginkgo.It("Modify response headers", func() {
+		vhostHTTPPort := f.AllocPort()
+		serverConf := getDefaultServerConf(vhostHTTPPort)
+
+		localPort := f.AllocPort()
+		localServer := httpserver.New(
+			httpserver.WithBindPort(localPort),
+			httpserver.WithHandler(http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
+				w.WriteHeader(200)
+			})),
+		)
+		f.RunServer("", localServer)
+
+		clientConf := consts.DefaultClientConfig
+		clientConf += fmt.Sprintf(`
+			[[proxies]]
+			name = "test"
+			type = "http"
+			localPort = %d
+			customDomains = ["normal.example.com"]
+			responseHeaders.set.x-from-where = "frp"
+			`, localPort)
+
+		f.RunProcesses([]string{serverConf}, []string{clientConf})
+
+		framework.NewRequestExpect(f).Port(vhostHTTPPort).
+			RequestModify(func(r *request.Request) {
+				r.HTTP().HTTPHost("normal.example.com")
+			}).
+			Ensure(func(res *request.Response) bool {
+				return res.Header.Get("X-From-Where") == "frp"
+			})
+	})
+
 	ginkgo.It("Host Header Rewrite", func() {
 		vhostHTTPPort := f.AllocPort()
 		serverConf := getDefaultServerConf(vhostHTTPPort)
@@ -385,4 +419,48 @@ var _ = ginkgo.Describe("[Feature: HTTP]", func() {
 		framework.ExpectNoError(err)
 		framework.ExpectEqualValues(consts.TestString, string(msg))
 	})
+
+	ginkgo.It("vhostHTTPTimeout", func() {
+		vhostHTTPPort := f.AllocPort()
+		serverConf := getDefaultServerConf(vhostHTTPPort)
+		serverConf += `
+		vhostHTTPTimeout = 2
+		`
+
+		delayDuration := 0 * time.Second
+		localPort := f.AllocPort()
+		localServer := httpserver.New(
+			httpserver.WithBindPort(localPort),
+			httpserver.WithHandler(http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
+				time.Sleep(delayDuration)
+				_, _ = w.Write([]byte(req.Host))
+			})),
+		)
+		f.RunServer("", localServer)
+
+		clientConf := consts.DefaultClientConfig
+		clientConf += fmt.Sprintf(`
+			[[proxies]]
+			name = "test"
+			type = "http"
+			localPort = %d
+			customDomains = ["normal.example.com"]
+			`, localPort)
+
+		f.RunProcesses([]string{serverConf}, []string{clientConf})
+
+		framework.NewRequestExpect(f).Port(vhostHTTPPort).
+			RequestModify(func(r *request.Request) {
+				r.HTTP().HTTPHost("normal.example.com").HTTP().Timeout(time.Second)
+			}).
+			ExpectResp([]byte("normal.example.com")).
+			Ensure()
+
+		delayDuration = 3 * time.Second
+		framework.NewRequestExpect(f).Port(vhostHTTPPort).
+			RequestModify(func(r *request.Request) {
+				r.HTTP().HTTPHost("normal.example.com").HTTP().Timeout(5 * time.Second)
+			}).
+			Ensure(framework.ExpectResponseCode(504))
+	})
 })

+ 1 - 0
web/frps/components.d.ts

@@ -31,6 +31,7 @@ declare module 'vue' {
     ProxiesSTCP: typeof import('./src/components/ProxiesSTCP.vue')['default']
     ProxiesSUDP: typeof import('./src/components/ProxiesSUDP.vue')['default']
     ProxiesTCP: typeof import('./src/components/ProxiesTCP.vue')['default']
+    ProxiesTCPMux: typeof import('./src/components/ProxiesTCPMux.vue')['default']
     ProxiesUDP: typeof import('./src/components/ProxiesUDP.vue')['default']
     ProxyView: typeof import('./src/components/ProxyView.vue')['default']
     ProxyViewExpand: typeof import('./src/components/ProxyViewExpand.vue')['default']

+ 1 - 0
web/frps/src/App.vue

@@ -39,6 +39,7 @@
               <el-menu-item index="/proxies/udp">UDP</el-menu-item>
               <el-menu-item index="/proxies/http">HTTP</el-menu-item>
               <el-menu-item index="/proxies/https">HTTPS</el-menu-item>
+              <el-menu-item index="/proxies/tcpmux">TCPMUX</el-menu-item>
               <el-menu-item index="/proxies/stcp">STCP</el-menu-item>
               <el-menu-item index="/proxies/sudp">SUDP</el-menu-item>
             </el-sub-menu>

+ 38 - 0
web/frps/src/components/ProxiesTCPMux.vue

@@ -0,0 +1,38 @@
+<template>
+  <ProxyView :proxies="proxies" proxyType="tcpmux" @refresh="fetchData" />
+</template>
+
+<script setup lang="ts">
+import { ref } from 'vue'
+import { TCPMuxProxy } from '../utils/proxy.js'
+import ProxyView from './ProxyView.vue'
+
+let proxies = ref<TCPMuxProxy[]>([])
+
+const fetchData = () => {
+  let tcpmuxHTTPConnectPort: number
+  let subdomainHost: string
+  fetch('../api/serverinfo', { credentials: 'include' })
+    .then((res) => {
+      return res.json()
+    })
+    .then((json) => {
+      tcpmuxHTTPConnectPort = json.tcpmuxHTTPConnectPort
+      subdomainHost = json.subdomainHost
+
+      fetch('../api/proxy/tcpmux', { credentials: 'include' })
+        .then((res) => {
+          return res.json()
+        })
+        .then((json) => {
+          proxies.value = []
+          for (let proxyStats of json.proxies) {
+            proxies.value.push(new TCPMuxProxy(proxyStats, tcpmuxHTTPConnectPort, subdomainHost))
+          }
+        })
+    })
+}
+fetchData()
+</script>
+
+<style></style>

+ 34 - 36
web/frps/src/components/ProxyViewExpand.vue

@@ -1,9 +1,9 @@
 <template>
   <el-form
     label-position="left"
+    label-width="auto"
     inline
     class="proxy-table-expand"
-    v-if="proxyType === 'http' || proxyType === 'https'"
   >
     <el-form-item label="Name">
       <span>{{ row.name }}</span>
@@ -11,18 +11,6 @@
     <el-form-item label="Type">
       <span>{{ row.type }}</span>
     </el-form-item>
-    <el-form-item label="Domains">
-      <span>{{ row.customDomains }}</span>
-    </el-form-item>
-    <el-form-item label="SubDomain">
-      <span>{{ row.subdomain }}</span>
-    </el-form-item>
-    <el-form-item label="locations">
-      <span>{{ row.locations }}</span>
-    </el-form-item>
-    <el-form-item label="HostRewrite">
-      <span>{{ row.hostHeaderRewrite }}</span>
-    </el-form-item>
     <el-form-item label="Encryption">
       <span>{{ row.encryption }}</span>
     </el-form-item>
@@ -35,30 +23,40 @@
     <el-form-item label="Last Close">
       <span>{{ row.lastCloseTime }}</span>
     </el-form-item>
-  </el-form>
 
-  <el-form label-position="left" inline class="proxy-table-expand" v-else>
-    <el-form-item label="Name">
-      <span>{{ row.name }}</span>
-    </el-form-item>
-    <el-form-item label="Type">
-      <span>{{ row.type }}</span>
-    </el-form-item>
-    <el-form-item label="Addr">
-      <span>{{ row.addr }}</span>
-    </el-form-item>
-    <el-form-item label="Encryption">
-      <span>{{ row.encryption }}</span>
-    </el-form-item>
-    <el-form-item label="Compression">
-      <span>{{ row.compression }}</span>
-    </el-form-item>
-    <el-form-item label="Last Start">
-      <span>{{ row.lastStartTime }}</span>
-    </el-form-item>
-    <el-form-item label="Last Close">
-      <span>{{ row.lastCloseTime }}</span>
-    </el-form-item>
+    <div v-if="proxyType === 'http' || proxyType === 'https'">
+      <el-form-item label="Domains">
+        <span>{{ row.customDomains }}</span>
+      </el-form-item>
+      <el-form-item label="SubDomain">
+        <span>{{ row.subdomain }}</span>
+      </el-form-item>
+      <el-form-item label="locations">
+        <span>{{ row.locations }}</span>
+      </el-form-item>
+      <el-form-item label="HostRewrite">
+        <span>{{ row.hostHeaderRewrite }}</span>
+      </el-form-item>
+    </div>
+    <div v-else-if="proxyType === 'tcpmux'">
+      <el-form-item label="Multiplexer">
+        <span>{{ row.multiplexer }}</span>
+      </el-form-item>
+      <el-form-item label="RouteByHTTPUser">
+        <span>{{ row.routeByHTTPUser }}</span>
+      </el-form-item>
+      <el-form-item label="Domains">
+        <span>{{ row.customDomains }}</span>
+      </el-form-item>
+      <el-form-item label="SubDomain">
+        <span>{{ row.subdomain }}</span>
+      </el-form-item>
+    </div>
+    <div v-else>
+      <el-form-item label="Addr">
+        <span>{{ row.addr }}</span>
+      </el-form-item>
+    </div>
   </el-form>
 
   <div v-if="row.annotations && row.annotations.size > 0">

+ 2 - 2
web/frps/src/components/ServerOverview.vue

@@ -20,10 +20,10 @@
             <el-form-item label="QUIC Bind Port" v-if="data.quicBindPort != 0">
               <span>{{ data.quicBindPort }}</span>
             </el-form-item>
-            <el-form-item label="Http Port" v-if="data.vhostHTTPPort != 0">
+            <el-form-item label="HTTP Port" v-if="data.vhostHTTPPort != 0">
               <span>{{ data.vhostHTTPPort }}</span>
             </el-form-item>
-            <el-form-item label="Https Port" v-if="data.vhostHTTPSPort != 0">
+            <el-form-item label="HTTPS Port" v-if="data.vhostHTTPSPort != 0">
               <span>{{ data.vhostHTTPSPort }}</span>
             </el-form-item>
             <el-form-item

+ 6 - 0
web/frps/src/router/index.ts

@@ -4,6 +4,7 @@ import ProxiesTCP from '../components/ProxiesTCP.vue'
 import ProxiesUDP from '../components/ProxiesUDP.vue'
 import ProxiesHTTP from '../components/ProxiesHTTP.vue'
 import ProxiesHTTPS from '../components/ProxiesHTTPS.vue'
+import ProxiesTCPMux from '../components/ProxiesTCPMux.vue'
 import ProxiesSTCP from '../components/ProxiesSTCP.vue'
 import ProxiesSUDP from '../components/ProxiesSUDP.vue'
 
@@ -35,6 +36,11 @@ const router = createRouter({
       name: 'ProxiesHTTPS',
       component: ProxiesHTTPS,
     },
+    {
+      path: '/proxies/tcpmux',
+      name: 'ProxiesTCPMux',
+      component: ProxiesTCPMux,
+    },
     {
       path: '/proxies/stcp',
       name: 'ProxiesSTCP',

+ 23 - 0
web/frps/src/utils/proxy.ts

@@ -110,6 +110,28 @@ class HTTPSProxy extends BaseProxy {
   }
 }
 
+class TCPMuxProxy extends BaseProxy {
+  multiplexer: string
+  routeByHTTPUser: string
+
+  constructor(proxyStats: any, port: number, subdomainHost: string) {
+    super(proxyStats)
+    this.type = 'tcpmux'
+    this.port = port
+    this.multiplexer = ''
+    this.routeByHTTPUser = ''
+
+    if (proxyStats.conf) {
+      this.customDomains = proxyStats.conf.customDomains || this.customDomains
+      this.multiplexer = proxyStats.conf.multiplexer
+      this.routeByHTTPUser = proxyStats.conf.routeByHTTPUser
+      if (proxyStats.conf.subdomain) {
+        this.subdomain = `${proxyStats.conf.subdomain}.${subdomainHost}`
+      }
+    } 
+  }
+}
+
 class STCPProxy extends BaseProxy {
   constructor(proxyStats: any) {
     super(proxyStats)
@@ -128,6 +150,7 @@ export {
   BaseProxy,
   TCPProxy,
   UDPProxy,
+  TCPMuxProxy,
   HTTPProxy,
   HTTPSProxy,
   STCPProxy,

Some files were not shown because too many files changed in this diff