Skip to content

Commit

Permalink
[hysteria2] Masquerade inbound option style refactor. Mainly for supp…
Browse files Browse the repository at this point in the history
…orting `rewriteHost`
  • Loading branch information
doggy committed Nov 23, 2024
1 parent cd515c0 commit d5162dc
Show file tree
Hide file tree
Showing 5 changed files with 138 additions and 17 deletions.
21 changes: 16 additions & 5 deletions docs/configuration/inbound/hysteria2.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
```
Expand Down Expand Up @@ -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.

Expand Down
18 changes: 17 additions & 1 deletion docs/configuration/inbound/hysteria2.zh.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
```
Expand Down Expand Up @@ -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
Expand Down
56 changes: 54 additions & 2 deletions option/hysteria2.go
Original file line number Diff line number Diff line change
@@ -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"`
Expand All @@ -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 {
Expand All @@ -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
}
30 changes: 21 additions & 9 deletions protocol/hysteria2/inbound.go
Original file line number Diff line number Diff line change
Expand Up @@ -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{
Expand Down
30 changes: 30 additions & 0 deletions test/hysteria2_test.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package main

import (
"encoding/json"
"net/netip"
"testing"

Expand All @@ -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, "")
Expand Down

0 comments on commit d5162dc

Please sign in to comment.