Repository: fatedier/frp (language: go)

Issue:
https多域名开启后，对于通配符域名SSL证书无法支持，会自动重定向到其中一个网站；同一时间只能一个子域名能正常访问
### Bug Description

通配符域名无法支持
当给每个子域名切换成各自的单域名证书时
网站访问正常

### frpc Version

0.58.1

### frps Version

0.58.1

### System Architecture

ubuntu

### Configurations

**FRPS配置：**
bindPort = 7000
vhostHTTPPort = 80
vhostHTTPSPort=443

**FRPC配置：**
**域名1：**
[[proxies]]
name = "plugin_https2http_dify"
type = "https"
customDomains = ["dify.bistudio.com.cn"]
transport.proxyProtocolVersion = "v2"  # 获取真实IP
[proxies.plugin]
type = "https2http"
localAddr = "127.0.0.1:2111"
crtPath = "/home/adminpc/deploy/nginx/conf.d/ssl/all.bistudio.com.cn/all.bistudio.com.cn.crt"
keyPath = "/home/adminpc/deploy/nginx/conf.d/ssl/all.bistudio.com.cn/all.bistudio.com.cn.key"
#hostHeaderRewrite = "127.0.0.1"
requestHeaders.set.x-from-where = "frp"

**域名2：**
[[proxies]]
name = "plugin_https2http_lobe-chat"
type = "https"
customDomains = ["lobe-chat.bistudio.com.cn"]
transport.proxyProtocolVersion = "v2"  # 获取真实IP
[proxies.plugin]
type = "https2http"
localAddr = "127.0.0.1:2104"
crtPath = "/home/adminpc/deploy/nginx/conf.d/ssl/all.bistudio.com.cn/all.bistudio.com.cn.crt"
keyPath = "/home/adminpc/deploy/nginx/conf.d/ssl/all.bistudio.com.cn/all.bistudio.com.cn.key"
requestHeaders.set.x-from-where = "frp"

### Logs

log观察一直都是正常的，没有报错
----刚观察到frpc访问会报：
2024-06-28 14:46:10.467 [W] [httputil/reverseproxy.go:661] http: proxy error: context canceled
2024-06-28 14:46:10.467 [W] [httputil/reverseproxy.go:661] http: proxy error: context canceled

### Steps to reproduce

1. 
2. 
3. 
...


### Affected area

- [ ] Docs
- [ ] Installation
- [ ] Performance and Scalability
- [ ] Security
- [ ] User Experience
- [ ] Test and Release
- [ ] Developer Infrastructure
- [ ] Client Plugin
- [ ] Server Plugin
- [ ] Extensions
- [ ] Others


Discussion / hints:
https://github.com/fatedier/frp/issues/4278#issuecomment-2166020723
> [#4278 (comment)](https://github.com/fatedier/frp/issues/4278#issuecomment-2166020723)

感谢提示，我中间没有nginx，全部用的frp进行的https网站访问多子域名复用443端口
看了文章，目前暂未找到在frp中禁用TLS Session Ticket的方法
之所以我的需求是直接用frps透传，是因为AI大模型文字输出上，各种实测后Frp是文字输出性能最高的！没有之一！

1、如果我最外层用nginx和泛域名证书转发内部frp一层后，文字输出速度呈断崖式下降！
2、但如果直接用frp的https2http插件透传出来，最外层不经过nginx， 对话AI模型可以马上输出结果，性能很高！
可惜只支持单域名，不支持泛域名！

作为一个使用了frp达6年的用户，从frp的0.2X版本一直用到现在，一直在生产环境使用，非常稳定从未失望过！
麻烦作者帮看看有没有办法，谢谢了！
-------------------
PS：目前最外层用nginx加了一层代理后，千问2模型的token响应很慢，网址：https://www.bistudio.com.cn   ,我真不知道为什么外面套一层nginx为什么会比原生frp的https2http慢这么多！也许学艺不精
----------------2024年6月29日凌晨更新
根据对nginx的研究，在server中加入如下配置后，可以保障AI模型文字的流式输出性能：

> proxy_cache off;  # 关闭缓存
> proxy_buffering off;  # 关闭代理缓冲
> chunked_transfer_encoding on;  # 开启分块传输编码
> tcp_nopush on;  # 开启TCP NOPUSH选项，禁止Nagle算法
> tcp_nodelay on;  # 开启TCP NODELAY选项，禁止延迟ACK算法
> keepalive_timeout 300;  # 设定keep-alive超时时间为65秒

frp还是正常可以作为后端http穿透

问题已解决，软件还是有各自的边界更好，谢谢
我也和你一样遇到同样的问题

客户端：
```
serverAddr = "1.2.3.4"
serverPort = 443
transport.protocol = "tcp"
transport.tls.disableCustomTLSFirstByte = false
auth.token = "123456"

[[proxies]]
name = "docker-nginx"
type = "https"
customDomains = ["w-1.test.com", "w-2.test.com", "w-3.test.com"]
[proxies.plugin]
type = "https2http"
localAddr = "nginx:80"
crtPath = "/etc/frp/ssl/xxx.com.cer"
keyPath = "/etc/frp/ssl/xxx.com.key"

[[proxies]]
name = "host-web"
type = "https"
customDomains = ["w-4.test.com", "w-5.test.com"]
[proxies.plugin]
type = "https2http"
localAddr = "docker.localhost:88"
crtPath = "/etc/frp/ssl/xxx.com.cer"
keyPath = "/etc/frp/ssl/xxx.com.key"
```
前面 w-1、w-2、w-3 正常，那么后面 w-4、w-5 也无法正常访问，会加载前的 1-3 的默认证书和网站内容， 
重启之后，后面的可以了，那么前的就不行，每次重启都随机的，就无法全部正确加载域名所对应的web

也不是上面你说的  nginx，我贴出来的只有两组 https2http，其实我是有4组，只有一组是 nginx 的，其他的都不是 nginx 的web 服务。同样会出现混乱的加载的情况。配置 localAddr 是不同内的 IP 上的 web。`估计是逻辑上的问题` 出现域名无法准确的对应上内网的 localAddr 的 web服务

@fatedier 

> 我也和你一样遇到同样的问题
> 
> 客户端：
> 
> ```
> serverAddr = "1.2.3.4"
> serverPort = 443
> transport.protocol = "tcp"
> transport.tls.disableCustomTLSFirstByte = false
> auth.token = "123456"
> 
> [[proxies]]
> name = "docker-nginx"
> type = "https"
> customDomains = ["w-1.test.com", "w-2.test.com", "w-3.test.com"]
> [proxies.plugin]
> type = "https2http"
> localAddr = "nginx:80"
> crtPath = "/etc/frp/ssl/xxx.com.cer"
> keyPath = "/etc/frp/ssl/xxx.com.key"
> 
> [[proxies]]
> name = "host-web"
> type = "https"
> customDomains = ["w-4.test.com", "w-5.test.com"]
> [proxies.plugin]
> type = "https2http"
> localAddr = "docker.localhost:88"
> crtPath = "/etc/frp/ssl/xxx.com.cer"
> keyPath = "/etc/frp/ssl/xxx.com.key"
> ```
> 
> 前面 w-1、w-2、w-3 正常，那么后面 w-4、w-5 也无法正常访问，会加载前的 1-3 的默认证书和网站内容， 重启之后，后面的可以了，那么前的就不行，每次重启都随机的，就无法全部正确加载域名所对应的web
> 
> 也不是上面你说的 nginx，我贴出来的只有两组 https2http，其实我是有4组，只有一组是 nginx 的，其他的都不是 nginx 的web 服务。同样会出现混乱的加载的情况。配置 localAddr 是不同内的 IP 上的 web。`估计是逻辑上的问题` 出现域名无法准确的对应上内网的 localAddr 的 web服务
> 
> @fatedier

最外层挂nginx。  FRP只做内网穿透，不做https服务，这样挂多少个域名都不会混乱；至少我是这样解决的
@osbi-code 你 nginx 的配置可以贴一下？nginx 上是否启用了 http2？

我目前观察下来，在 go https server 里关闭 TLS Session Ticket 也不会起到作用。只有禁用 http2 才可以，不太确定你的 nginx 配置是因为禁用了 http2 才解决问题，还是因为 TLS Session Ticket 的配置解决了问题。

Fix patch:
diff --git a/Release.md b/Release.md
index 2dcfc56604e..6e142deff52 100644
--- a/Release.md
+++ b/Release.md
@@ -1,3 +1,8 @@
 ### Features
 
 * Added a new plugin "http2http" which allows forwarding HTTP requests to another HTTP server, supporting options like local address binding, host header rewrite, and custom request headers.
+* Added `enableHTTP2` option to control whether to enable HTTP/2 in plugin https2http and https2https, default is true.
+
+### Changes
+
+* Plugin https2http & https2https: return 421 `Misdirected Request` if host not match sni.
diff --git a/pkg/config/v1/plugin.go b/pkg/config/v1/plugin.go
index 333b020a0f4..7ae4a4d4233 100644
--- a/pkg/config/v1/plugin.go
+++ b/pkg/config/v1/plugin.go
@@ -20,9 +20,15 @@ import (
 	"errors"
 	"fmt"
 	"reflect"
+
+	"github.com/samber/lo"
+
+	"github.com/fatedier/frp/pkg/util/util"
 )
 
-type ClientPluginOptions interface{}
+type ClientPluginOptions interface {
+	Complete()
+}
 
 type TypedClientPluginOptions struct {
 	Type string `json:"type"`
@@ -73,10 +79,10 @@ const (
 	PluginHTTPProxy        = "http_proxy"
 	PluginHTTPS2HTTP       = "https2http"
 	PluginHTTPS2HTTPS      = "https2https"
+	PluginHTTP2HTTP        = "http2http"
 	PluginSocks5           = "socks5"
 	PluginStaticFile       = "static_file"
 	PluginUnixDomainSocket = "unix_domain_socket"
-	PluginHTTP2HTTP        = "http2http"
 )
 
 var clientPluginOptionsTypeMap = map[string]reflect.Type{
@@ -84,10 +90,10 @@ var clientPluginOptionsTypeMap = map[string]reflect.Type{
 	PluginHTTPProxy:        reflect.TypeOf(HTTPProxyPluginOptions{}),
 	PluginHTTPS2HTTP:       reflect.TypeOf(HTTPS2HTTPPluginOptions{}),
 	PluginHTTPS2HTTPS:      reflect.TypeOf(HTTPS2HTTPSPluginOptions{}),
+	PluginHTTP2HTTP:        reflect.TypeOf(HTTP2HTTPPluginOptions{}),
 	PluginSocks5:           reflect.TypeOf(Socks5PluginOptions{}),
 	PluginStaticFile:       reflect.TypeOf(StaticFilePluginOptions{}),
 	PluginUnixDomainSocket: reflect.TypeOf(UnixDomainSocketPluginOptions{}),
-	PluginHTTP2HTTP:        reflect.TypeOf(HTTP2HTTPPluginOptions{}),
 }
 
 type HTTP2HTTPSPluginOptions struct {
@@ -97,36 +103,61 @@ type HTTP2HTTPSPluginOptions struct {
 	RequestHeaders    HeaderOperations `json:"requestHeaders,omitempty"`
 }
 
+func (o *HTTP2HTTPSPluginOptions) Complete() {}
+
 type HTTPProxyPluginOptions struct {
 	Type         string `json:"type,omitempty"`
 	HTTPUser     string `json:"httpUser,omitempty"`
 	HTTPPassword string `json:"httpPassword,omitempty"`
 }
 
+func (o *HTTPProxyPluginOptions) Complete() {}
+
 type HTTPS2HTTPPluginOptions struct {
 	Type              string           `json:"type,omitempty"`
 	LocalAddr         string           `json:"localAddr,omitempty"`
 	HostHeaderRewrite string           `json:"hostHeaderRewrite,omitempty"`
 	RequestHeaders    HeaderOperations `json:"requestHeaders,omitempty"`
+	EnableHTTP2       *bool            `json:"enableHTTP2,omitempty"`
 	CrtPath           string           `json:"crtPath,omitempty"`
 	KeyPath           string           `json:"keyPath,omitempty"`
 }
 
+func (o *HTTPS2HTTPPluginOptions) Complete() {
+	o.EnableHTTP2 = util.EmptyOr(o.EnableHTTP2, lo.ToPtr(true))
+}
+
 type HTTPS2HTTPSPluginOptions struct {
 	Type              string           `json:"type,omitempty"`
 	LocalAddr         string           `json:"localAddr,omitempty"`
 	HostHeaderRewrite string           `json:"hostHeaderRewrite,omitempty"`
 	RequestHeaders    HeaderOperations `json:"requestHeaders,omitempty"`
+	EnableHTTP2       *bool            `json:"enableHTTP2,omitempty"`
 	CrtPath           string           `json:"crtPath,omitempty"`
 	KeyPath           string           `json:"keyPath,omitempty"`
 }
 
+func (o *HTTPS2HTTPSPluginOptions) Complete() {
+	o.EnableHTTP2 = util.EmptyOr(o.EnableHTTP2, lo.ToPtr(true))
+}
+
+type HTTP2HTTPPluginOptions struct {
+	Type              string           `json:"type,omitempty"`
+	LocalAddr         string           `json:"localAddr,omitempty"`
+	HostHeaderRewrite string           `json:"hostHeaderRewrite,omitempty"`
+	RequestHeaders    HeaderOperations `json:"requestHeaders,omitempty"`
+}
+
+func (o *HTTP2HTTPPluginOptions) Complete() {}
+
 type Socks5PluginOptions struct {
 	Type     string `json:"type,omitempty"`
 	Username string `json:"username,omitempty"`
 	Password string `json:"password,omitempty"`
 }
 
+func (o *Socks5PluginOptions) Complete() {}
+
 type StaticFilePluginOptions struct {
 	Type         string `json:"type,omitempty"`
 	LocalPath    string `json:"localPath,omitempty"`
@@ -135,15 +166,11 @@ type StaticFilePluginOptions struct {
 	HTTPPassword string `json:"httpPassword,omitempty"`
 }
 
+func (o *StaticFilePluginOptions) Complete() {}
+
 type UnixDomainSocketPluginOptions struct {
 	Type     string `json:"type,omitempty"`
 	UnixPath string `json:"unixPath,omitempty"`
 }
 
-// Added HTTP2HTTPPluginOptions struct
-type HTTP2HTTPPluginOptions struct {
-	Type              string           `json:"type,omitempty"`
-	LocalAddr         string           `json:"localAddr,omitempty"`
-	HostHeaderRewrite string           `json:"hostHeaderRewrite,omitempty"`
-	RequestHeaders    HeaderOperations `json:"requestHeaders,omitempty"`
-}
+func (o *UnixDomainSocketPluginOptions) Complete() {}
diff --git a/pkg/config/v1/proxy.go b/pkg/config/v1/proxy.go
index 45c489f689c..d53d05e3cbd 100644
--- a/pkg/config/v1/proxy.go
+++ b/pkg/config/v1/proxy.go
@@ -127,6 +127,10 @@ func (c *ProxyBaseConfig) Complete(namePrefix string) {
 	c.Name = lo.Ternary(namePrefix == "", "", namePrefix+".") + c.Name
 	c.LocalIP = util.EmptyOr(c.LocalIP, "127.0.0.1")
 	c.Transport.BandwidthLimitMode = util.EmptyOr(c.Transport.BandwidthLimitMode, types.BandwidthLimitModeClient)
+
+	if c.Plugin.ClientPluginOptions != nil {
+		c.Plugin.ClientPluginOptions.Complete()
+	}
 }
 
 func (c *ProxyBaseConfig) MarshalToMsg(m *msg.NewProxy) {
diff --git a/pkg/plugin/client/https2http.go b/pkg/plugin/client/https2http.go
index 6d686361e99..3bb12c2252b 100644
--- a/pkg/plugin/client/https2http.go
+++ b/pkg/plugin/client/https2http.go
@@ -27,9 +27,11 @@ import (
 	"time"
 
 	"github.com/fatedier/golib/pool"
+	"github.com/samber/lo"
 
 	v1 "github.com/fatedier/frp/pkg/config/v1"
 	"github.com/fatedier/frp/pkg/transport"
+	httppkg "github.com/fatedier/frp/pkg/util/http"
 	"github.com/fatedier/frp/pkg/util/log"
 	netpkg "github.com/fatedier/frp/pkg/util/net"
 )
@@ -71,6 +73,17 @@ func NewHTTPS2HTTPPlugin(options v1.ClientPluginOptions) (Plugin, error) {
 		BufferPool: pool.NewBuffer(32 * 1024),
 		ErrorLog:   stdlog.New(log.NewWriteLogger(log.WarnLevel, 2), "", 0),
 	}
+	handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+		if r.TLS != nil {
+			tlsServerName, _ := httppkg.CanonicalHost(r.TLS.ServerName)
+			host, _ := httppkg.CanonicalHost(r.Host)
+			if tlsServerName != "" && tlsServerName != host {
+				w.WriteHeader(http.StatusMisdirectedRequest)
+				return
+			}
+		}
+		rp.ServeHTTP(w, r)
+	})
 
 	var (
 		tlsConfig *tls.Config
@@ -87,10 +100,13 @@ func NewHTTPS2HTTPPlugin(options v1.ClientPluginOptions) (Plugin, error) {
 	}
 
 	p.s = &http.Server{
-		Handler:           rp,
+		Handler:           handler,
 		ReadHeaderTimeout: 60 * time.Second,
 		TLSConfig:         tlsConfig,
 	}
+	if !lo.FromPtr(opts.EnableHTTP2) {
+		p.s.TLSNextProto = make(map[string]func(*http.Server, *tls.Conn, http.Handler))
+	}
 
 	go func() {
 		_ = p.s.ServeTLS(listener, "", "")
diff --git a/pkg/plugin/client/https2https.go b/pkg/plugin/client/https2https.go
index 5ddd4dd692e..c315c8e3244 100644
--- a/pkg/plugin/client/https2https.go
+++ b/pkg/plugin/client/https2https.go
@@ -27,9 +27,11 @@ import (
 	"time"
 
 	"github.com/fatedier/golib/pool"
+	"github.com/samber/lo"
 
 	v1 "github.com/fatedier/frp/pkg/config/v1"
 	"github.com/fatedier/frp/pkg/transport"
+	httppkg "github.com/fatedier/frp/pkg/util/http"
 	"github.com/fatedier/frp/pkg/util/log"
 	netpkg "github.com/fatedier/frp/pkg/util/net"
 )
@@ -77,6 +79,17 @@ func NewHTTPS2HTTPSPlugin(options v1.ClientPluginOptions) (Plugin, error) {
 		BufferPool: pool.NewBuffer(32 * 1024),
 		ErrorLog:   stdlog.New(log.NewWriteLogger(log.WarnLevel, 2), "", 0),
 	}
+	handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+		if r.TLS != nil {
+			tlsServerName, _ := httppkg.CanonicalHost(r.TLS.ServerName)
+			host, _ := httppkg.CanonicalHost(r.Host)
+			if tlsServerName != "" && tlsServerName != host {
+				w.WriteHeader(http.StatusMisdirectedRequest)
+				return
+			}
+		}
+		rp.ServeHTTP(w, r)
+	})
 
 	var (
 		tlsConfig *tls.Config
@@ -93,10 +106,13 @@ func NewHTTPS2HTTPSPlugin(options v1.ClientPluginOptions) (Plugin, error) {
 	}
 
 	p.s = &http.Server{
-		Handler:           rp,
+		Handler:           handler,
 		ReadHeaderTimeout: 60 * time.Second,
 		TLSConfig:         tlsConfig,
 	}
+	if !lo.FromPtr(opts.EnableHTTP2) {
+		p.s.TLSNextProto = make(map[string]func(*http.Server, *tls.Conn, http.Handler))
+	}
 
 	go func() {
 		_ = p.s.ServeTLS(listener, "", "")
diff --git a/pkg/util/version/version.go b/pkg/util/version/version.go
index 11d140eb772..561a52e702b 100644
--- a/pkg/util/version/version.go
+++ b/pkg/util/version/version.go
@@ -14,7 +14,7 @@
 
 package version
 
-var version = "0.58.1"
+var version = "0.59.0"
 
 func Full() string {
 	return version


Test patch: