diff --git a/docs/configuration/inbound/hysteria2.md b/docs/configuration/inbound/hysteria2.md index 7c611e64..a061dec5 100644 --- a/docs/configuration/inbound/hysteria2.md +++ b/docs/configuration/inbound/hysteria2.md @@ -21,7 +21,15 @@ ], "ignore_client_bandwidth": false, "tls": {}, - "masquerade": "", + "masquerade": { + "type": "proxy", + "proxy": { + "url": "", + "rewriteHost": true + }, + "file": "/var/www", + "string": "Some-Stuffs" + }, "brutal_debug": false } ``` @@ -81,10 +89,13 @@ TLS configuration, see [TLS](/configuration/shared/tls/#inbound). HTTP3 server behavior when authentication fails. -| Scheme | Example | Description | -|--------------|-------------------------|--------------------| -| `file` | `file:///var/www` | As a file server | -| `http/https` | `http://127.0.0.1:8080` | As a reverse proxy | +| Key | Example | Description | +|--------------|--------------------------------|----------------------| +| `type` | `file \| proxy \| string` | masquerade modes | +| `file` | `/var/www` | As a file server | +| `proxy.url` | `http://127.0.0.1:8080` | As a reverse proxy | +| `proxy.rewriteHost` | `true \| false` | Rewrite the Host header to match the proxied website | +| `string` | `Some-Stuffs` | as a constant string server | A 404 page will be returned if empty. diff --git a/docs/configuration/inbound/hysteria2.zh.md b/docs/configuration/inbound/hysteria2.zh.md index c936aae8..40110398 100644 --- a/docs/configuration/inbound/hysteria2.zh.md +++ b/docs/configuration/inbound/hysteria2.zh.md @@ -21,7 +21,15 @@ ], "ignore_client_bandwidth": false, "tls": {}, - "masquerade": "", + "masquerade": { + "type": "proxy", + "proxy": { + "url": "", + "rewriteHost": true + }, + "file": "/var/www", + "string": "Some-Stuffs" + }, "brutal_debug": false } ``` @@ -83,6 +91,14 @@ HTTP3 服务器认证失败时的行为。 | `file` | `file:///var/www` | 作为文件服务器 | | `http/https` | `http://127.0.0.1:8080` | 作为反向代理 | +| Key | 示例 | 描述 | +|--------------|--------------------------------|----------------------| +| `type` | `file \| proxy \| string` | 模式 | +| `file` | `/var/www` | 作为文件服务器 | +| `proxy.url` | `http://127.0.0.1:8080` | 作为反向代理 | +| `proxy.rewriteHost` | `true \| false` | 重写 `Host` 头以匹配被代理的网站 | +| `string` | `Some-Stuffs` | 作为常量字符服务器 | + 如果为空,则返回 404 页。 #### brutal_debug diff --git a/option/hysteria2.go b/option/hysteria2.go index 5032c734..8b6a07fa 100644 --- a/option/hysteria2.go +++ b/option/hysteria2.go @@ -1,5 +1,11 @@ package option +import ( + "net/url" + + "github.com/sagernet/sing/common/json" +) + type Hysteria2InboundOptions struct { ListenOptions UpMbps int `json:"up_mbps,omitempty"` @@ -8,8 +14,8 @@ type Hysteria2InboundOptions struct { Users []Hysteria2User `json:"users,omitempty"` IgnoreClientBandwidth bool `json:"ignore_client_bandwidth,omitempty"` InboundTLSOptionsContainer - Masquerade string `json:"masquerade,omitempty"` - BrutalDebug bool `json:"brutal_debug,omitempty"` + Masquerade Hysteria2Masquerade `json:"masquerade,omitempty"` + BrutalDebug bool `json:"brutal_debug,omitempty"` } type Hysteria2Obfs struct { @@ -33,3 +39,49 @@ type Hysteria2OutboundOptions struct { OutboundTLSOptionsContainer BrutalDebug bool `json:"brutal_debug,omitempty"` } + +type Hysteria2Masquerade struct { + Type string `json:"type,omitempty"` + File string `json:"file,omitempty"` + Proxy Hysteria2MasqueradeProxy `json:"proxy,omitempty"` + String string `json:"string,omitempty"` +} + +type Hysteria2MasqueradeProxy struct { + URL string `json:"url,omitempty"` + RewriteHost bool `json:"rewriteHost,omitempty"` +} + +func (m *Hysteria2Masquerade) UnmarshalJSON(data []byte) error { + // Attempt to unmarshal data as a string + var str string + if err := json.Unmarshal(data, &str); err == nil { + masqueradeURL, err := url.Parse(str) + if err != nil || masqueradeURL.Scheme == "" { + m.String = str + m.Type = "string" + return nil + } + switch masqueradeURL.Scheme { + case "file": + m.File = masqueradeURL.Path + m.Type = "file" + case "http", "https": + m.Proxy.URL = str + m.Type = "proxy" + default: + } + return nil + } + // If not a string, attempt to unmarshal into the struct + type Alias Hysteria2Masquerade + aux := &struct { + *Alias + }{ + Alias: (*Alias)(m), + } + if err := json.Unmarshal(data, &aux); err != nil { + return err + } + return nil +} diff --git a/protocol/hysteria2/inbound.go b/protocol/hysteria2/inbound.go index 8081a06e..5a1e1e56 100644 --- a/protocol/hysteria2/inbound.go +++ b/protocol/hysteria2/inbound.go @@ -60,25 +60,37 @@ func NewInbound(ctx context.Context, router adapter.Router, logger log.ContextLo } } var masqueradeHandler http.Handler - if options.Masquerade != "" { - masqueradeURL, err := url.Parse(options.Masquerade) - if err != nil { - return nil, E.Cause(err, "parse masquerade URL") - } - switch masqueradeURL.Scheme { + if options.Masquerade != (option.Hysteria2Masquerade{}) { + switch options.Masquerade.Type { case "file": - masqueradeHandler = http.FileServer(http.Dir(masqueradeURL.Path)) - case "http", "https": + masqueradeHandler = http.FileServer(http.Dir(options.Masquerade.File)) + case "proxy": + masqueradeURL, err := url.Parse(options.Masquerade.Proxy.URL) + if err != nil { + return nil, E.Cause(err, "parse masquerade URL") + } masqueradeHandler = &httputil.ReverseProxy{ Rewrite: func(r *httputil.ProxyRequest) { r.SetURL(masqueradeURL) + // SetURL rewrites the Host header, + // but we don't want that if rewriteHost is false + if !options.Masquerade.Proxy.RewriteHost { + r.Out.Host = r.In.Host + } }, ErrorHandler: func(w http.ResponseWriter, r *http.Request, err error) { w.WriteHeader(http.StatusBadGateway) }, } + case "string": + if options.Masquerade.String != "" { + masqueradeHandler = http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusOK) // Use 200 OK by default + _, _ = w.Write([]byte(options.Masquerade.String)) + }) + } default: - return nil, E.New("unknown masquerade URL scheme: ", masqueradeURL.Scheme) + return nil, E.New("unknown masquerade type: ", options.Masquerade.Type) } } inbound := &Inbound{ diff --git a/test/hysteria2_test.go b/test/hysteria2_test.go index 10a5b086..cccd6cb4 100644 --- a/test/hysteria2_test.go +++ b/test/hysteria2_test.go @@ -1,6 +1,7 @@ package main import ( + "encoding/json" "net/netip" "testing" @@ -11,6 +12,35 @@ import ( "github.com/sagernet/sing/common/json/badoption" ) +func TestHysteria2InboundOptionsMasqueradeUnmarshalJSON(t *testing.T) { + t.Run("schema-file", func(t *testing.T) { + m := testMasqueradeUnmarshalJSON(t, []byte(`"file:///var/www"`)) + if m.Type != "file" || m.File != "/var/www" { + t.Errorf("Unexpected values: %+v", m) + } + }) + t.Run("schema-https", func(t *testing.T) { + m := testMasqueradeUnmarshalJSON(t, []byte(`"https://example.org:443"`)) + if m.Type != "proxy" || m.Proxy.URL != "https://example.org:443" || m.Proxy.RewriteHost != false { + t.Errorf("Unexpected values: %+v", m) + } + }) + t.Run("schema-string", func(t *testing.T) { + m := testMasqueradeUnmarshalJSON(t, []byte(`"Some-Stuffs"`)) + if m.Type != "string" || m.String != "Some-Stuffs" { + t.Errorf("Unexpected values: %+v", m) + } + }) +} + +func testMasqueradeUnmarshalJSON(t *testing.T, jsonData []byte) option.Hysteria2Masquerade { + var m option.Hysteria2Masquerade + if err := json.Unmarshal(jsonData, &m); err != nil { + t.Fatalf("Unmarshal failed: %v", err) + } + return m +} + func TestHysteria2Self(t *testing.T) { t.Run("self", func(t *testing.T) { testHysteria2Self(t, "")