mirror of
https://github.com/shtorm-7/sing-box-extended.git
synced 2026-06-06 19:24:56 +03:00
Add Mieru inbound, refactor sudoku. Fixes
This commit is contained in:
7
.gitignore
vendored
7
.gitignore
vendored
@@ -23,3 +23,10 @@ AGENTS.md
|
|||||||
/.claude/
|
/.claude/
|
||||||
dist
|
dist
|
||||||
logs
|
logs
|
||||||
|
/*.so
|
||||||
|
/*.log
|
||||||
|
/*.db-shm
|
||||||
|
/*.db-wal
|
||||||
|
/*.db.backup.*
|
||||||
|
/test_download.bin
|
||||||
|
/wget-log
|
||||||
@@ -31,6 +31,7 @@
|
|||||||
| `hysteria2` | [Hysteria2](./hysteria2/) | :material-close: |
|
| `hysteria2` | [Hysteria2](./hysteria2/) | :material-close: |
|
||||||
| `vless` | [VLESS](./vless/) | TCP |
|
| `vless` | [VLESS](./vless/) | TCP |
|
||||||
| `anytls` | [AnyTLS](./anytls/) | TCP |
|
| `anytls` | [AnyTLS](./anytls/) | TCP |
|
||||||
|
| `mieru` | [Mieru](./mieru/) | :material-close: |
|
||||||
| `tun` | [Tun](./tun/) | :material-close: |
|
| `tun` | [Tun](./tun/) | :material-close: |
|
||||||
| `redirect` | [Redirect](./redirect/) | :material-close: |
|
| `redirect` | [Redirect](./redirect/) | :material-close: |
|
||||||
| `tproxy` | [TProxy](./tproxy/) | :material-close: |
|
| `tproxy` | [TProxy](./tproxy/) | :material-close: |
|
||||||
|
|||||||
@@ -31,6 +31,7 @@
|
|||||||
| `hysteria2` | [Hysteria2](./hysteria2/) | :material-close: |
|
| `hysteria2` | [Hysteria2](./hysteria2/) | :material-close: |
|
||||||
| `vless` | [VLESS](./vless/) | TCP |
|
| `vless` | [VLESS](./vless/) | TCP |
|
||||||
| `anytls` | [AnyTLS](./anytls/) | TCP |
|
| `anytls` | [AnyTLS](./anytls/) | TCP |
|
||||||
|
| `mieru` | [Mieru](./mieru/) | :material-close: |
|
||||||
| `tun` | [Tun](./tun/) | :material-close: |
|
| `tun` | [Tun](./tun/) | :material-close: |
|
||||||
| `redirect` | [Redirect](./redirect/) | :material-close: |
|
| `redirect` | [Redirect](./redirect/) | :material-close: |
|
||||||
| `tproxy` | [TProxy](./tproxy/) | :material-close: |
|
| `tproxy` | [TProxy](./tproxy/) | :material-close: |
|
||||||
|
|||||||
49
docs/configuration/inbound/mieru.md
Normal file
49
docs/configuration/inbound/mieru.md
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
---
|
||||||
|
icon: material/new-box
|
||||||
|
---
|
||||||
|
|
||||||
|
### Structure
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"type": "mieru",
|
||||||
|
"tag": "mieru-in",
|
||||||
|
|
||||||
|
... // Listen Fields
|
||||||
|
|
||||||
|
"transport": "TCP",
|
||||||
|
"users": [
|
||||||
|
{
|
||||||
|
"name": "asdf",
|
||||||
|
"password": "hjkl"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"traffic_pattern": "GgQIARAK",
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Listen Fields
|
||||||
|
|
||||||
|
See [Listen Fields](/configuration/shared/listen/) for details.
|
||||||
|
|
||||||
|
### Fields
|
||||||
|
|
||||||
|
#### transport
|
||||||
|
|
||||||
|
==Required==
|
||||||
|
|
||||||
|
Transmission protocol. Allowed values are `TCP` and `UDP`.
|
||||||
|
|
||||||
|
#### users
|
||||||
|
|
||||||
|
==Required==
|
||||||
|
|
||||||
|
A list of mieru user name and password.
|
||||||
|
|
||||||
|
#### traffic_pattern
|
||||||
|
|
||||||
|
A base64 string to fine tune network behavior.
|
||||||
|
|
||||||
|
#### user_hint_is_mandatory
|
||||||
|
|
||||||
|
If proxy client doesn't sent user hint, proxy server will refuse the connection.
|
||||||
49
docs/configuration/inbound/mieru.zh.md
Normal file
49
docs/configuration/inbound/mieru.zh.md
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
---
|
||||||
|
icon: material/new-box
|
||||||
|
---
|
||||||
|
|
||||||
|
### 结构
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"type": "mieru",
|
||||||
|
"tag": "mieru-in",
|
||||||
|
|
||||||
|
... // 监听字段
|
||||||
|
|
||||||
|
"transport": "TCP",
|
||||||
|
"users": [
|
||||||
|
{
|
||||||
|
"name": "asdf",
|
||||||
|
"password": "hjkl"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"traffic_pattern": "GgQIARAK",
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 监听字段
|
||||||
|
|
||||||
|
参阅 [监听字段](/zh/configuration/shared/listen/)。
|
||||||
|
|
||||||
|
### 字段
|
||||||
|
|
||||||
|
#### transport
|
||||||
|
|
||||||
|
==必填==
|
||||||
|
|
||||||
|
通信协议。可设为 `TCP` 或 `UDP`。
|
||||||
|
|
||||||
|
#### users
|
||||||
|
|
||||||
|
==必填==
|
||||||
|
|
||||||
|
一组 mieru 用户名和密码。
|
||||||
|
|
||||||
|
#### traffic_pattern
|
||||||
|
|
||||||
|
一个 base64 字符串用于微调网络行为。
|
||||||
|
|
||||||
|
#### user_hint_is_mandatory
|
||||||
|
|
||||||
|
客户端若不发送用户提示,代理服务器将拒绝连接。
|
||||||
@@ -32,6 +32,7 @@
|
|||||||
| `hysteria2` | [Hysteria2](./hysteria2/) |
|
| `hysteria2` | [Hysteria2](./hysteria2/) |
|
||||||
| `mieru` | [Mieru](./mieru/) |
|
| `mieru` | [Mieru](./mieru/) |
|
||||||
| `anytls` | [AnyTLS](./anytls/) |
|
| `anytls` | [AnyTLS](./anytls/) |
|
||||||
|
| `mieru` | [Mieru](./mieru/) |
|
||||||
| `tor` | [Tor](./tor/) |
|
| `tor` | [Tor](./tor/) |
|
||||||
| `ssh` | [SSH](./ssh/) |
|
| `ssh` | [SSH](./ssh/) |
|
||||||
| `dns` | [DNS](./dns/) |
|
| `dns` | [DNS](./dns/) |
|
||||||
|
|||||||
@@ -32,6 +32,7 @@
|
|||||||
| `hysteria2` | [Hysteria2](./hysteria2/) |
|
| `hysteria2` | [Hysteria2](./hysteria2/) |
|
||||||
| `mieru` | [Mieru](./mieru/) |
|
| `mieru` | [Mieru](./mieru/) |
|
||||||
| `anytls` | [AnyTLS](./anytls/) |
|
| `anytls` | [AnyTLS](./anytls/) |
|
||||||
|
| `mieru` | [Mieru](./mieru/) |
|
||||||
| `tor` | [Tor](./tor/) |
|
| `tor` | [Tor](./tor/) |
|
||||||
| `ssh` | [SSH](./ssh/) |
|
| `ssh` | [SSH](./ssh/) |
|
||||||
| `dns` | [DNS](./dns/) |
|
| `dns` | [DNS](./dns/) |
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ icon: material/new-box
|
|||||||
"username": "asdf",
|
"username": "asdf",
|
||||||
"password": "hjkl",
|
"password": "hjkl",
|
||||||
"multiplexing": "MULTIPLEXING_LOW",
|
"multiplexing": "MULTIPLEXING_LOW",
|
||||||
|
"traffic_pattern": "GgQIARAK",
|
||||||
|
|
||||||
... // Dial Fields
|
... // Dial Fields
|
||||||
}
|
}
|
||||||
@@ -48,7 +49,7 @@ Must set at least one field between `server_port` and `server_ports`.
|
|||||||
|
|
||||||
==Required==
|
==Required==
|
||||||
|
|
||||||
Transmission protocol. The only allowed value is `TCP`.
|
Transmission protocol. Allowed values are `TCP` and `UDP`.
|
||||||
|
|
||||||
#### username
|
#### username
|
||||||
|
|
||||||
@@ -66,6 +67,10 @@ mieru password.
|
|||||||
|
|
||||||
Multiplexing level. Supported values are `MULTIPLEXING_OFF`, `MULTIPLEXING_LOW`, `MULTIPLEXING_MIDDLE`, `MULTIPLEXING_HIGH`. `MULTIPLEXING_OFF` disables multiplexing.
|
Multiplexing level. Supported values are `MULTIPLEXING_OFF`, `MULTIPLEXING_LOW`, `MULTIPLEXING_MIDDLE`, `MULTIPLEXING_HIGH`. `MULTIPLEXING_OFF` disables multiplexing.
|
||||||
|
|
||||||
|
#### traffic_pattern
|
||||||
|
|
||||||
|
A base64 string to fine tune network behavior.
|
||||||
|
|
||||||
### Dial Fields
|
### Dial Fields
|
||||||
|
|
||||||
See [Dial Fields](/configuration/shared/dial/) for details.
|
See [Dial Fields](/configuration/shared/dial/) for details.
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ icon: material/new-box
|
|||||||
"username": "asdf",
|
"username": "asdf",
|
||||||
"password": "hjkl",
|
"password": "hjkl",
|
||||||
"multiplexing": "MULTIPLEXING_LOW",
|
"multiplexing": "MULTIPLEXING_LOW",
|
||||||
|
"traffic_pattern": "GgQIARAK",
|
||||||
|
|
||||||
... // 拨号字段
|
... // 拨号字段
|
||||||
}
|
}
|
||||||
@@ -48,7 +49,7 @@ icon: material/new-box
|
|||||||
|
|
||||||
==必填==
|
==必填==
|
||||||
|
|
||||||
通信协议。仅可设为 `TCP`。
|
通信协议。可设为 `TCP` 或 `UDP`。
|
||||||
|
|
||||||
#### username
|
#### username
|
||||||
|
|
||||||
@@ -66,6 +67,10 @@ mieru 密码。
|
|||||||
|
|
||||||
多路复用设置。可以设为 `MULTIPLEXING_OFF`,`MULTIPLEXING_LOW`,`MULTIPLEXING_MIDDLE`,`MULTIPLEXING_HIGH`。其中 `MULTIPLEXING_OFF` 会关闭多路复用功能。
|
多路复用设置。可以设为 `MULTIPLEXING_OFF`,`MULTIPLEXING_LOW`,`MULTIPLEXING_MIDDLE`,`MULTIPLEXING_HIGH`。其中 `MULTIPLEXING_OFF` 会关闭多路复用功能。
|
||||||
|
|
||||||
|
#### traffic_pattern
|
||||||
|
|
||||||
|
一个 base64 字符串用于微调网络行为。
|
||||||
|
|
||||||
### 拨号字段
|
### 拨号字段
|
||||||
|
|
||||||
参阅 [拨号字段](/zh/configuration/shared/dial/)。
|
参阅 [拨号字段](/zh/configuration/shared/dial/)。
|
||||||
|
|||||||
@@ -27,14 +27,16 @@
|
|||||||
"tag": "mieru-out",
|
"tag": "mieru-out",
|
||||||
"server": "example.com",
|
"server": "example.com",
|
||||||
"server_port": 27017,
|
"server_port": 27017,
|
||||||
"server_ports": "27017-27019",
|
"server_ports": [
|
||||||
|
"27017-27019"
|
||||||
|
],
|
||||||
"transport": "TCP",
|
"transport": "TCP",
|
||||||
"username": "username",
|
"username": "username",
|
||||||
"password": "password",
|
"password": "password",
|
||||||
// valid: MULTIPLEXING_DEFAULT / MULTIPLEXING_OFF / MULTIPLEXING_LOW
|
// valid: MULTIPLEXING_DEFAULT / MULTIPLEXING_OFF / MULTIPLEXING_LOW
|
||||||
// MULTIPLEXING_MIDDLE / MULTIPLEXING_HIGH
|
// MULTIPLEXING_MIDDLE / MULTIPLEXING_HIGH
|
||||||
"multiplexing": "MULTIPLEXING_LOW"
|
"multiplexing": "MULTIPLEXING_LOW",
|
||||||
// Dial Fields
|
"traffic_pattern": "GgQIARAK"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"route": {
|
"route": {
|
||||||
|
|||||||
29
examples/mieru/server.json
Normal file
29
examples/mieru/server.json
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
{
|
||||||
|
"log": {
|
||||||
|
"level": "error"
|
||||||
|
},
|
||||||
|
"inbounds": [
|
||||||
|
{
|
||||||
|
"type": "mieru",
|
||||||
|
"tag": "mieru-in",
|
||||||
|
"listen_port": 27017,
|
||||||
|
"transport": "TCP",
|
||||||
|
"users": [
|
||||||
|
{
|
||||||
|
"name": "username",
|
||||||
|
"password": "password"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"traffic_pattern": "GgQIARAK"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"outbounds": [
|
||||||
|
{
|
||||||
|
"type": "direct",
|
||||||
|
"tag": "direct"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"route": {
|
||||||
|
"final": "direct"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -12,6 +12,20 @@
|
|||||||
"server": "your-server.com",
|
"server": "your-server.com",
|
||||||
"server_port": 443,
|
"server_port": 443,
|
||||||
"key": "your-secret-key"
|
"key": "your-secret-key"
|
||||||
|
// "aead_method": "chacha20-poly1305", // chacha20-poly1305 | aes-128-gcm | none
|
||||||
|
// "table_type": "prefer_ascii", // prefer_ascii | prefer_entropy | up_ascii_down_entropy | up_entropy_down_ascii
|
||||||
|
// "padding_min": 10, // 0-100
|
||||||
|
// "padding_max": 30, // 0-100, >= padding_min
|
||||||
|
// "enable_pure_downlink": true, // true | false
|
||||||
|
// "custom_table": "xpxvvpvv", // 8 chars: 2x, 2p, 4v
|
||||||
|
// "custom_tables": ["xpxvvpvv", "vxpvxvvp"],
|
||||||
|
// "http_mask": {
|
||||||
|
// "enabled": true, // true | false
|
||||||
|
// "mode": "stream", // legacy | stream | poll | auto | ws
|
||||||
|
// "host": "cdn.example.com", // optional, Host header / SNI override
|
||||||
|
// "path_root": "secret", // optional, URL path prefix (single segment)
|
||||||
|
// "multiplex": "auto" // off | auto | on
|
||||||
|
// }
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,17 +12,23 @@
|
|||||||
"server": "your-server.com",
|
"server": "your-server.com",
|
||||||
"server_port": 443,
|
"server_port": 443,
|
||||||
"key": "your-secret-key",
|
"key": "your-secret-key",
|
||||||
"tls": {
|
// "aead_method": "chacha20-poly1305", // chacha20-poly1305 | aes-128-gcm | none
|
||||||
"enabled": true,
|
// "table_type": "prefer_ascii", // prefer_ascii | prefer_entropy | up_ascii_down_entropy | up_entropy_down_ascii
|
||||||
"fragment": true,
|
// "padding_min": 10, // 0-100
|
||||||
"fragment_fallback_delay": "300ms"
|
// "padding_max": 30, // 0-100, >= padding_min
|
||||||
},
|
// "enable_pure_downlink": true, // true | false
|
||||||
|
// "custom_table": "xpxvvpvv", // 8 chars: 2x, 2p, 4v
|
||||||
|
// "custom_tables": ["xpxvvpvv", "vxpvxvvp"],
|
||||||
"http_mask": {
|
"http_mask": {
|
||||||
"enabled": true,
|
"enabled": true, // true | false
|
||||||
"mode": "stream",
|
"mode": "stream", // legacy | stream | poll | auto | ws
|
||||||
"host": "cdn.example.com",
|
"host": "cdn.example.com", // optional, Host header / SNI override
|
||||||
"path_root": "secret",
|
"path_root": "secret", // optional, URL path prefix (single segment)
|
||||||
"multiplex": "auto"
|
"multiplex": "auto", // off | auto | on
|
||||||
|
"tls": { // https://sing-box.sagernet.org/configuration/shared/tls/#outbound
|
||||||
|
"enabled": true,
|
||||||
|
"server_name": "cdn.example.com",
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -5,6 +5,18 @@
|
|||||||
"listen": "::",
|
"listen": "::",
|
||||||
"listen_port": 443,
|
"listen_port": 443,
|
||||||
"key": "your-secret-key"
|
"key": "your-secret-key"
|
||||||
|
// "aead_method": "chacha20-poly1305", // chacha20-poly1305 | aes-128-gcm | none
|
||||||
|
// "table_type": "prefer_ascii", // prefer_ascii | prefer_entropy | up_ascii_down_entropy | up_entropy_down_ascii
|
||||||
|
// "padding_min": 10, // 0-100
|
||||||
|
// "padding_max": 30, // 0-100, >= padding_min
|
||||||
|
// "enable_pure_downlink": true, // true | false
|
||||||
|
// "handshake_timeout": 5, // seconds
|
||||||
|
// "custom_table": "xpxvvpvv", // 8 chars: 2x, 2p, 4v
|
||||||
|
// "custom_tables": ["xpxvvpvv", "vxpvxvvp"],
|
||||||
|
// "disable_http_mask": false, // true | false
|
||||||
|
// "http_mask_mode": "legacy", // legacy | stream | poll | auto | ws
|
||||||
|
// "path_root": "secret", // optional, URL path prefix (single segment)
|
||||||
|
// "fallback": "127.0.0.1:8080" // optional, fallback address for rejected connections
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"outbounds": [
|
"outbounds": [
|
||||||
|
|||||||
2
go.mod
2
go.mod
@@ -11,7 +11,7 @@ require (
|
|||||||
github.com/coder/websocket v1.8.14
|
github.com/coder/websocket v1.8.14
|
||||||
github.com/cretz/bine v0.2.0
|
github.com/cretz/bine v0.2.0
|
||||||
github.com/database64128/tfo-go/v2 v2.3.2
|
github.com/database64128/tfo-go/v2 v2.3.2
|
||||||
github.com/enfein/mieru/v3 v3.17.1
|
github.com/enfein/mieru/v3 v3.33.0
|
||||||
github.com/go-chi/chi/v5 v5.2.5
|
github.com/go-chi/chi/v5 v5.2.5
|
||||||
github.com/go-chi/render v1.0.3
|
github.com/go-chi/render v1.0.3
|
||||||
github.com/go-playground/validator/v10 v10.30.1
|
github.com/go-playground/validator/v10 v10.30.1
|
||||||
|
|||||||
4
go.sum
4
go.sum
@@ -89,8 +89,8 @@ github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkp
|
|||||||
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
|
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
|
||||||
github.com/ebitengine/purego v0.10.0 h1:QIw4xfpWT6GWTzaW5XEKy3HXoqrJGx1ijYHzTF0/ISU=
|
github.com/ebitengine/purego v0.10.0 h1:QIw4xfpWT6GWTzaW5XEKy3HXoqrJGx1ijYHzTF0/ISU=
|
||||||
github.com/ebitengine/purego v0.10.0/go.mod h1:iIjxzd6CiRiOG0UyXP+V1+jWqUXVjPKLAI0mRfJZTmQ=
|
github.com/ebitengine/purego v0.10.0/go.mod h1:iIjxzd6CiRiOG0UyXP+V1+jWqUXVjPKLAI0mRfJZTmQ=
|
||||||
github.com/enfein/mieru/v3 v3.17.1 h1:pIKbspsKRYNyUrORVI33t1/yz2syaaUkIanskAbGBHY=
|
github.com/enfein/mieru/v3 v3.33.0 h1:hv2jK8nqYHwpSG86U2rpZR2I8Aff1/J3ifRmd9NBbFc=
|
||||||
github.com/enfein/mieru/v3 v3.17.1/go.mod h1:zJBUCsi5rxyvHM8fjFf+GLaEl4OEjjBXr1s5F6Qd3hM=
|
github.com/enfein/mieru/v3 v3.33.0/go.mod h1:zJBUCsi5rxyvHM8fjFf+GLaEl4OEjjBXr1s5F6Qd3hM=
|
||||||
github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg=
|
github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg=
|
||||||
github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
|
github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
|
||||||
github.com/florianl/go-nfqueue/v2 v2.0.2 h1:FL5lQTeetgpCvac1TRwSfgaXUn0YSO7WzGvWNIp3JPE=
|
github.com/florianl/go-nfqueue/v2 v2.0.2 h1:FL5lQTeetgpCvac1TRwSfgaXUn0YSO7WzGvWNIp3JPE=
|
||||||
|
|||||||
@@ -31,7 +31,6 @@ import (
|
|||||||
"github.com/sagernet/sing-box/protocol/limiter/rate"
|
"github.com/sagernet/sing-box/protocol/limiter/rate"
|
||||||
"github.com/sagernet/sing-box/protocol/limiter/traffic"
|
"github.com/sagernet/sing-box/protocol/limiter/traffic"
|
||||||
"github.com/sagernet/sing-box/protocol/mieru"
|
"github.com/sagernet/sing-box/protocol/mieru"
|
||||||
|
|
||||||
"github.com/sagernet/sing-box/protocol/mixed"
|
"github.com/sagernet/sing-box/protocol/mixed"
|
||||||
"github.com/sagernet/sing-box/protocol/naive"
|
"github.com/sagernet/sing-box/protocol/naive"
|
||||||
"github.com/sagernet/sing-box/protocol/parser"
|
"github.com/sagernet/sing-box/protocol/parser"
|
||||||
@@ -81,6 +80,7 @@ func InboundRegistry() *inbound.Registry {
|
|||||||
shadowtls.RegisterInbound(registry)
|
shadowtls.RegisterInbound(registry)
|
||||||
vless.RegisterInbound(registry)
|
vless.RegisterInbound(registry)
|
||||||
anytls.RegisterInbound(registry)
|
anytls.RegisterInbound(registry)
|
||||||
|
mieru.RegisterInbound(registry)
|
||||||
|
|
||||||
bond.RegisterInbound(registry)
|
bond.RegisterInbound(registry)
|
||||||
failover.RegisterInbound(registry)
|
failover.RegisterInbound(registry)
|
||||||
|
|||||||
@@ -10,4 +10,18 @@ type MieruOutboundOptions struct {
|
|||||||
UserName string `json:"username,omitempty"`
|
UserName string `json:"username,omitempty"`
|
||||||
Password string `json:"password,omitempty"`
|
Password string `json:"password,omitempty"`
|
||||||
Multiplexing string `json:"multiplexing,omitempty"`
|
Multiplexing string `json:"multiplexing,omitempty"`
|
||||||
|
TrafficPattern string `json:"traffic_pattern,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type MieruInboundOptions struct {
|
||||||
|
ListenOptions
|
||||||
|
Users []MieruUser `json:"users,omitempty"`
|
||||||
|
Transport string `json:"transport,omitempty"`
|
||||||
|
TrafficPattern string `json:"traffic_pattern,omitempty"`
|
||||||
|
UserHintIsMandatory bool `json:"user_hint_is_mandatory,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type MieruUser struct {
|
||||||
|
Name string `json:"name,omitempty"`
|
||||||
|
Password string `json:"password,omitempty"`
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,28 +1,26 @@
|
|||||||
package option
|
package option
|
||||||
|
|
||||||
import "github.com/sagernet/sing/common/json/badoption"
|
|
||||||
|
|
||||||
type SudokuOutboundOptions struct {
|
type SudokuOutboundOptions struct {
|
||||||
DialerOptions
|
DialerOptions
|
||||||
ServerOptions
|
ServerOptions
|
||||||
Key string `json:"key"`
|
Key string `json:"key"`
|
||||||
AEADMethod string `json:"aead_method,omitempty"`
|
AEADMethod string `json:"aead_method,omitempty"`
|
||||||
PaddingMin *int `json:"padding_min,omitempty"`
|
PaddingMin *int `json:"padding_min,omitempty"`
|
||||||
PaddingMax *int `json:"padding_max,omitempty"`
|
PaddingMax *int `json:"padding_max,omitempty"`
|
||||||
TableType string `json:"table_type,omitempty"`
|
TableType string `json:"table_type,omitempty"`
|
||||||
EnablePureDownlink *bool `json:"enable_pure_downlink,omitempty"`
|
EnablePureDownlink *bool `json:"enable_pure_downlink,omitempty"`
|
||||||
CustomTable string `json:"custom_table,omitempty"`
|
CustomTable string `json:"custom_table,omitempty"`
|
||||||
CustomTables []string `json:"custom_tables,omitempty"`
|
CustomTables []string `json:"custom_tables,omitempty"`
|
||||||
HTTPMask *SudokuHTTPMask `json:"http_mask,omitempty"`
|
HTTPMask *SudokuHTTPMask `json:"http_mask,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type SudokuHTTPMask struct {
|
type SudokuHTTPMask struct {
|
||||||
Enabled bool `json:"enabled,omitempty"`
|
Enabled bool `json:"enabled,omitempty"`
|
||||||
Mode string `json:"mode,omitempty"`
|
Mode string `json:"mode,omitempty"`
|
||||||
Host string `json:"host,omitempty"`
|
Host string `json:"host,omitempty"`
|
||||||
PathRoot string `json:"path_root,omitempty"`
|
PathRoot string `json:"path_root,omitempty"`
|
||||||
Multiplex string `json:"multiplex,omitempty"`
|
Multiplex string `json:"multiplex,omitempty"`
|
||||||
TLS *SudokuOutboundTLSOptions `json:"tls,omitempty"`
|
OutboundTLSOptionsContainer
|
||||||
}
|
}
|
||||||
|
|
||||||
type SudokuInboundOptions struct {
|
type SudokuInboundOptions struct {
|
||||||
@@ -41,14 +39,3 @@ type SudokuInboundOptions struct {
|
|||||||
PathRoot string `json:"path_root,omitempty"`
|
PathRoot string `json:"path_root,omitempty"`
|
||||||
Fallback string `json:"fallback,omitempty"`
|
Fallback string `json:"fallback,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type SudokuOutboundTLSOptions struct {
|
|
||||||
Enabled bool `json:"enabled,omitempty"`
|
|
||||||
Fragment bool `json:"fragment,omitempty"`
|
|
||||||
FragmentFallbackDelay badoption.Duration `json:"fragment_fallback_delay,omitempty"`
|
|
||||||
RecordFragment bool `json:"record_fragment,omitempty"`
|
|
||||||
KernelTx bool `json:"kernel_tx,omitempty"`
|
|
||||||
KernelRx bool `json:"kernel_rx,omitempty"`
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
340
protocol/mieru/inbound.go
Normal file
340
protocol/mieru/inbound.go
Normal file
@@ -0,0 +1,340 @@
|
|||||||
|
package mieru
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"net"
|
||||||
|
"net/netip"
|
||||||
|
"sync"
|
||||||
|
|
||||||
|
"github.com/sagernet/sing-box/adapter"
|
||||||
|
"github.com/sagernet/sing-box/adapter/inbound"
|
||||||
|
"github.com/sagernet/sing-box/common/listener"
|
||||||
|
"github.com/sagernet/sing-box/common/uot"
|
||||||
|
C "github.com/sagernet/sing-box/constant"
|
||||||
|
"github.com/sagernet/sing-box/log"
|
||||||
|
"github.com/sagernet/sing-box/option"
|
||||||
|
"github.com/sagernet/sing/common"
|
||||||
|
"github.com/sagernet/sing/common/buf"
|
||||||
|
E "github.com/sagernet/sing/common/exceptions"
|
||||||
|
M "github.com/sagernet/sing/common/metadata"
|
||||||
|
N "github.com/sagernet/sing/common/network"
|
||||||
|
|
||||||
|
mierucommon "github.com/enfein/mieru/v3/apis/common"
|
||||||
|
mieruconstant "github.com/enfein/mieru/v3/apis/constant"
|
||||||
|
mierumodel "github.com/enfein/mieru/v3/apis/model"
|
||||||
|
mieruserver "github.com/enfein/mieru/v3/apis/server"
|
||||||
|
mierutp "github.com/enfein/mieru/v3/apis/trafficpattern"
|
||||||
|
mierupb "github.com/enfein/mieru/v3/pkg/appctl/appctlpb"
|
||||||
|
"google.golang.org/protobuf/proto"
|
||||||
|
)
|
||||||
|
|
||||||
|
func RegisterInbound(registry *inbound.Registry) {
|
||||||
|
inbound.Register[option.MieruInboundOptions](registry, C.TypeMieru, NewInbound)
|
||||||
|
}
|
||||||
|
|
||||||
|
type Inbound struct {
|
||||||
|
inbound.Adapter
|
||||||
|
ctx context.Context
|
||||||
|
router adapter.ConnectionRouterEx
|
||||||
|
logger log.ContextLogger
|
||||||
|
listener *listener.Listener
|
||||||
|
server mieruserver.Server
|
||||||
|
userNames []string
|
||||||
|
|
||||||
|
mu sync.Mutex
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewInbound(ctx context.Context, router adapter.Router, logger log.ContextLogger, tag string, options option.MieruInboundOptions) (adapter.Inbound, error) {
|
||||||
|
config, userNames, err := buildMieruServerConfig(ctx, options)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to build mieru server config: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
s := mieruserver.NewServer()
|
||||||
|
if err := s.Store(config); err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to store mieru server config: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
inboundInstance := &Inbound{
|
||||||
|
Adapter: inbound.NewAdapter(C.TypeMieru, tag),
|
||||||
|
ctx: ctx,
|
||||||
|
router: uot.NewRouter(router, logger),
|
||||||
|
logger: logger,
|
||||||
|
server: s,
|
||||||
|
userNames: userNames,
|
||||||
|
}
|
||||||
|
inboundInstance.listener = listener.New(listener.Options{
|
||||||
|
Context: ctx,
|
||||||
|
Logger: logger,
|
||||||
|
Network: []string{N.NetworkTCP, N.NetworkUDP},
|
||||||
|
Listen: options.ListenOptions,
|
||||||
|
})
|
||||||
|
|
||||||
|
return inboundInstance, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *Inbound) Start(stage adapter.StartStage) error {
|
||||||
|
if stage != adapter.StartStateStart {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
h.mu.Lock()
|
||||||
|
defer h.mu.Unlock()
|
||||||
|
|
||||||
|
if err := h.server.Start(); err != nil {
|
||||||
|
return fmt.Errorf("failed to start mieru server: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
h.logger.Info("mieru server is started")
|
||||||
|
go h.acceptLoop()
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *Inbound) Close() error {
|
||||||
|
h.mu.Lock()
|
||||||
|
defer h.mu.Unlock()
|
||||||
|
|
||||||
|
if h.server.IsRunning() {
|
||||||
|
return h.server.Stop()
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *Inbound) acceptLoop() {
|
||||||
|
for {
|
||||||
|
conn, request, err := h.server.Accept()
|
||||||
|
if err != nil {
|
||||||
|
if !h.server.IsRunning() {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
h.logger.Debug("failed to accept mieru connection: ", err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
go h.handleConnection(conn, request)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *Inbound) handleConnection(conn net.Conn, request *mierumodel.Request) {
|
||||||
|
ctx := log.ContextWithNewID(h.ctx)
|
||||||
|
|
||||||
|
// Send fake SOCKS5 response back to proxy client.
|
||||||
|
resp := &mierumodel.Response{
|
||||||
|
Reply: mieruconstant.Socks5ReplySuccess,
|
||||||
|
BindAddr: mierumodel.AddrSpec{
|
||||||
|
IP: net.IPv4zero,
|
||||||
|
Port: 0,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
if err := resp.WriteToSocks5(conn); err != nil {
|
||||||
|
conn.Close()
|
||||||
|
h.logger.DebugContext(ctx, "failed to write mieru response: ", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Build metadata.
|
||||||
|
var metadata adapter.InboundContext
|
||||||
|
metadata.Inbound = h.Tag()
|
||||||
|
metadata.InboundType = h.Type()
|
||||||
|
//nolint:staticcheck
|
||||||
|
metadata.InboundDetour = h.listener.ListenOptions().Detour
|
||||||
|
metadata.UDPDisableDomainUnmapping = h.listener.ListenOptions().UDPDisableDomainUnmapping
|
||||||
|
|
||||||
|
// Parse source address.
|
||||||
|
if remoteAddr := conn.RemoteAddr(); remoteAddr != nil {
|
||||||
|
metadata.Source = M.SocksaddrFromNet(remoteAddr)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse destination from request.
|
||||||
|
if request.DstAddr.FQDN != "" {
|
||||||
|
metadata.Destination = M.Socksaddr{
|
||||||
|
Fqdn: request.DstAddr.FQDN,
|
||||||
|
Port: uint16(request.DstAddr.Port),
|
||||||
|
}
|
||||||
|
} else if request.DstAddr.IP != nil {
|
||||||
|
addr, _ := netip.AddrFromSlice(request.DstAddr.IP)
|
||||||
|
metadata.Destination = M.Socksaddr{
|
||||||
|
Addr: addr.Unmap(),
|
||||||
|
Port: uint16(request.DstAddr.Port),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get username from connection.
|
||||||
|
if userCtx, ok := conn.(mierucommon.UserContext); ok {
|
||||||
|
metadata.User = userCtx.UserName()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle request.
|
||||||
|
switch request.Command {
|
||||||
|
case mieruconstant.Socks5ConnectCmd:
|
||||||
|
h.logger.InfoContext(ctx, "inbound TCP connection from ", metadata.Source, " to ", metadata.Destination)
|
||||||
|
if metadata.User != "" {
|
||||||
|
h.logger.InfoContext(ctx, "[", metadata.User, "] inbound TCP connection")
|
||||||
|
}
|
||||||
|
h.router.RouteConnectionEx(ctx, conn, metadata, nil)
|
||||||
|
case mieruconstant.Socks5UDPAssociateCmd:
|
||||||
|
h.logger.InfoContext(ctx, "inbound UDP connection from ", metadata.Source, " to ", metadata.Destination)
|
||||||
|
if metadata.User != "" {
|
||||||
|
h.logger.InfoContext(ctx, "[", metadata.User, "] inbound UDP connection")
|
||||||
|
}
|
||||||
|
h.handleUDP(ctx, conn, metadata)
|
||||||
|
default:
|
||||||
|
conn.Close()
|
||||||
|
h.logger.WarnContext(ctx, "unsupported mieru command: ", request.Command)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *Inbound) handleUDP(ctx context.Context, conn net.Conn, metadata adapter.InboundContext) {
|
||||||
|
pc := mierucommon.NewPacketOverStreamTunnel(conn)
|
||||||
|
packetConn := &mieruPacketConn{
|
||||||
|
PacketConn: pc,
|
||||||
|
destination: metadata.Destination,
|
||||||
|
}
|
||||||
|
h.router.RoutePacketConnectionEx(ctx, packetConn, metadata, nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
// mieruPacketConn wraps mieru's PacketConn to implement N.PacketConn
|
||||||
|
type mieruPacketConn struct {
|
||||||
|
net.PacketConn
|
||||||
|
destination M.Socksaddr
|
||||||
|
}
|
||||||
|
|
||||||
|
var _ N.PacketConn = (*mieruPacketConn)(nil)
|
||||||
|
|
||||||
|
// ReadPacket parses the SOCKS5 UDP header and returns the destination address.
|
||||||
|
func (c *mieruPacketConn) ReadPacket(buffer *buf.Buffer) (destination M.Socksaddr, err error) {
|
||||||
|
n, _, err := c.PacketConn.ReadFrom(buffer.FreeBytes())
|
||||||
|
if err != nil {
|
||||||
|
return M.Socksaddr{}, err
|
||||||
|
}
|
||||||
|
buffer.Truncate(n)
|
||||||
|
if buffer.Len() < 3 {
|
||||||
|
return M.Socksaddr{}, io.ErrShortBuffer
|
||||||
|
}
|
||||||
|
|
||||||
|
// Skip RSV (2 bytes) and FRAG (1 byte).
|
||||||
|
buffer.Advance(3)
|
||||||
|
|
||||||
|
var addr mierumodel.AddrSpec
|
||||||
|
if err := addr.ReadFromSocks5(buffer); err != nil {
|
||||||
|
return M.Socksaddr{}, err
|
||||||
|
}
|
||||||
|
if addr.FQDN != "" {
|
||||||
|
destination = M.Socksaddr{
|
||||||
|
Fqdn: addr.FQDN,
|
||||||
|
Port: uint16(addr.Port),
|
||||||
|
}
|
||||||
|
} else if addr.IP != nil {
|
||||||
|
netAddr, _ := netip.AddrFromSlice(addr.IP)
|
||||||
|
destination = M.Socksaddr{
|
||||||
|
Addr: netAddr.Unmap(),
|
||||||
|
Port: uint16(addr.Port),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return destination, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// WritePacket writes the SOCKS5 UDP header and the payload.
|
||||||
|
func (c *mieruPacketConn) WritePacket(buffer *buf.Buffer, destination M.Socksaddr) error {
|
||||||
|
header := buf.NewSize(3 + M.MaxSocksaddrLength)
|
||||||
|
defer header.Release()
|
||||||
|
|
||||||
|
// RSV (2 bytes) + FRAG (1 byte)
|
||||||
|
common.Must(header.WriteZeroN(3))
|
||||||
|
|
||||||
|
var addr mierumodel.AddrSpec
|
||||||
|
if destination.IsFqdn() {
|
||||||
|
addr.FQDN = destination.Fqdn
|
||||||
|
} else {
|
||||||
|
addr.IP = destination.Addr.AsSlice()
|
||||||
|
}
|
||||||
|
addr.Port = int(destination.Port)
|
||||||
|
if err := addr.WriteToSocks5(header); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
packet := buf.NewSize(header.Len() + buffer.Len())
|
||||||
|
defer packet.Release()
|
||||||
|
common.Must1(packet.Write(header.Bytes()))
|
||||||
|
common.Must1(packet.Write(buffer.Bytes()))
|
||||||
|
_, err := c.PacketConn.WriteTo(packet.Bytes(), nil)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func buildMieruServerConfig(_ context.Context, options option.MieruInboundOptions) (*mieruserver.ServerConfig, []string, error) {
|
||||||
|
if err := validateMieruInboundOptions(options); err != nil {
|
||||||
|
return nil, nil, fmt.Errorf("failed to validate mieru options: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
var transportProtocol *mierupb.TransportProtocol
|
||||||
|
switch options.Transport {
|
||||||
|
case "TCP":
|
||||||
|
transportProtocol = mierupb.TransportProtocol_TCP.Enum()
|
||||||
|
case "UDP":
|
||||||
|
transportProtocol = mierupb.TransportProtocol_UDP.Enum()
|
||||||
|
}
|
||||||
|
|
||||||
|
if options.ListenOptions.ListenPort == 0 {
|
||||||
|
return nil, nil, E.New("listen_port must be set")
|
||||||
|
}
|
||||||
|
portBindings := []*mierupb.PortBinding{
|
||||||
|
{
|
||||||
|
Port: proto.Int32(int32(options.ListenOptions.ListenPort)),
|
||||||
|
Protocol: transportProtocol,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
var users []*mierupb.User
|
||||||
|
var userNames []string
|
||||||
|
for _, user := range options.Users {
|
||||||
|
users = append(users, &mierupb.User{
|
||||||
|
Name: proto.String(user.Name),
|
||||||
|
Password: proto.String(user.Password),
|
||||||
|
})
|
||||||
|
userNames = append(userNames, user.Name)
|
||||||
|
}
|
||||||
|
var trafficPattern *mierupb.TrafficPattern
|
||||||
|
trafficPattern, _ = mierutp.Decode(options.TrafficPattern)
|
||||||
|
var advancedSettings *mierupb.ServerAdvancedSettings
|
||||||
|
if options.UserHintIsMandatory {
|
||||||
|
advancedSettings = &mierupb.ServerAdvancedSettings{
|
||||||
|
UserHintIsMandatory: proto.Bool(true),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return &mieruserver.ServerConfig{
|
||||||
|
Config: &mierupb.ServerConfig{
|
||||||
|
PortBindings: portBindings,
|
||||||
|
Users: users,
|
||||||
|
TrafficPattern: trafficPattern,
|
||||||
|
AdvancedSettings: advancedSettings,
|
||||||
|
},
|
||||||
|
}, userNames, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func validateMieruInboundOptions(options option.MieruInboundOptions) error {
|
||||||
|
if options.Transport != "TCP" && options.Transport != "UDP" {
|
||||||
|
return E.New("transport must be TCP or UDP")
|
||||||
|
}
|
||||||
|
if len(options.Users) == 0 {
|
||||||
|
return E.New("users is empty")
|
||||||
|
}
|
||||||
|
for _, user := range options.Users {
|
||||||
|
if user.Name == "" {
|
||||||
|
return E.New("username is empty")
|
||||||
|
}
|
||||||
|
if user.Password == "" {
|
||||||
|
return E.New("password is empty")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if options.TrafficPattern != "" {
|
||||||
|
trafficPattern, err := mierutp.Decode(options.TrafficPattern)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to decode traffic pattern %q: %w", options.TrafficPattern, err)
|
||||||
|
}
|
||||||
|
if err := mierutp.Validate(trafficPattern); err != nil {
|
||||||
|
return fmt.Errorf("invalid traffic pattern %q: %w", options.TrafficPattern, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
@@ -20,6 +20,7 @@ import (
|
|||||||
mieruclient "github.com/enfein/mieru/v3/apis/client"
|
mieruclient "github.com/enfein/mieru/v3/apis/client"
|
||||||
mierucommon "github.com/enfein/mieru/v3/apis/common"
|
mierucommon "github.com/enfein/mieru/v3/apis/common"
|
||||||
mierumodel "github.com/enfein/mieru/v3/apis/model"
|
mierumodel "github.com/enfein/mieru/v3/apis/model"
|
||||||
|
mierutp "github.com/enfein/mieru/v3/apis/trafficpattern"
|
||||||
mierupb "github.com/enfein/mieru/v3/pkg/appctl/appctlpb"
|
mierupb "github.com/enfein/mieru/v3/pkg/appctl/appctlpb"
|
||||||
"google.golang.org/protobuf/proto"
|
"google.golang.org/protobuf/proto"
|
||||||
)
|
)
|
||||||
@@ -36,7 +37,7 @@ func RegisterOutbound(registry *outbound.Registry) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func NewOutbound(ctx context.Context, router adapter.Router, logger log.ContextLogger, tag string, options option.MieruOutboundOptions) (adapter.Outbound, error) {
|
func NewOutbound(ctx context.Context, router adapter.Router, logger log.ContextLogger, tag string, options option.MieruOutboundOptions) (adapter.Outbound, error) {
|
||||||
outboundDialer, err := dialer.New(ctx, options.DialerOptions, options.ServerIsDomain())
|
outboundDialer, err := dialer.New(ctx, options.DialerOptions, M.IsDomainName(options.Server))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -123,7 +124,15 @@ func (md mieruDialer) DialContext(ctx context.Context, network, address string)
|
|||||||
return md.dialer.DialContext(ctx, network, addr)
|
return md.dialer.DialContext(ctx, network, addr)
|
||||||
}
|
}
|
||||||
|
|
||||||
var _ mierucommon.Dialer = (*mieruDialer)(nil)
|
func (md mieruDialer) ListenPacket(ctx context.Context, network, laddr, raddr string) (net.PacketConn, error) {
|
||||||
|
addr := M.ParseSocksaddr(raddr)
|
||||||
|
return md.dialer.ListenPacket(ctx, addr)
|
||||||
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
_ mierucommon.Dialer = (*mieruDialer)(nil)
|
||||||
|
_ mierucommon.PacketDialer = (*mieruDialer)(nil)
|
||||||
|
)
|
||||||
|
|
||||||
// streamer converts a net.PacketConn to a net.Conn.
|
// streamer converts a net.PacketConn to a net.Conn.
|
||||||
type streamer struct {
|
type streamer struct {
|
||||||
@@ -161,7 +170,13 @@ func buildMieruClientConfig(options option.MieruOutboundOptions, dialer mieruDia
|
|||||||
return nil, fmt.Errorf("failed to validate mieru options: %w", err)
|
return nil, fmt.Errorf("failed to validate mieru options: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
transportProtocol := mierupb.TransportProtocol_TCP.Enum()
|
var transportProtocol *mierupb.TransportProtocol
|
||||||
|
switch options.Transport {
|
||||||
|
case "TCP":
|
||||||
|
transportProtocol = mierupb.TransportProtocol_TCP.Enum()
|
||||||
|
case "UDP":
|
||||||
|
transportProtocol = mierupb.TransportProtocol_UDP.Enum()
|
||||||
|
}
|
||||||
server := &mierupb.ServerEndpoint{}
|
server := &mierupb.ServerEndpoint{}
|
||||||
if options.ServerPort != 0 {
|
if options.ServerPort != 0 {
|
||||||
server.PortBindings = append(server.PortBindings, &mierupb.PortBinding{
|
server.PortBindings = append(server.PortBindings, &mierupb.PortBinding{
|
||||||
@@ -189,13 +204,21 @@ func buildMieruClientConfig(options option.MieruOutboundOptions, dialer mieruDia
|
|||||||
},
|
},
|
||||||
Servers: []*mierupb.ServerEndpoint{server},
|
Servers: []*mierupb.ServerEndpoint{server},
|
||||||
},
|
},
|
||||||
Dialer: dialer,
|
Dialer: dialer,
|
||||||
|
PacketDialer: dialer,
|
||||||
|
DNSConfig: &mierucommon.ClientDNSConfig{
|
||||||
|
BypassDialerDNS: true,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
if multiplexing, ok := mierupb.MultiplexingLevel_value[options.Multiplexing]; ok {
|
if multiplexing, ok := mierupb.MultiplexingLevel_value[options.Multiplexing]; ok {
|
||||||
config.Profile.Multiplexing = &mierupb.MultiplexingConfig{
|
config.Profile.Multiplexing = &mierupb.MultiplexingConfig{
|
||||||
Level: mierupb.MultiplexingLevel(multiplexing).Enum(),
|
Level: mierupb.MultiplexingLevel(multiplexing).Enum(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if options.TrafficPattern != "" {
|
||||||
|
trafficPattern, _ := mierutp.Decode(options.TrafficPattern)
|
||||||
|
config.Profile.TrafficPattern = trafficPattern
|
||||||
|
}
|
||||||
return config, nil
|
return config, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -221,8 +244,8 @@ func validateMieruOptions(options option.MieruOutboundOptions) error {
|
|||||||
return fmt.Errorf("begin port must be less than or equal to end port")
|
return fmt.Errorf("begin port must be less than or equal to end port")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if options.Transport != "TCP" {
|
if options.Transport != "TCP" && options.Transport != "UDP" {
|
||||||
return fmt.Errorf("transport must be TCP")
|
return fmt.Errorf("transport must be TCP or UDP")
|
||||||
}
|
}
|
||||||
if options.UserName == "" {
|
if options.UserName == "" {
|
||||||
return fmt.Errorf("username is empty")
|
return fmt.Errorf("username is empty")
|
||||||
@@ -235,6 +258,15 @@ func validateMieruOptions(options option.MieruOutboundOptions) error {
|
|||||||
return fmt.Errorf("invalid multiplexing level: %s", options.Multiplexing)
|
return fmt.Errorf("invalid multiplexing level: %s", options.Multiplexing)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if options.TrafficPattern != "" {
|
||||||
|
trafficPattern, err := mierutp.Decode(options.TrafficPattern)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to decode traffic pattern %q: %w", options.TrafficPattern, err)
|
||||||
|
}
|
||||||
|
if err := mierutp.Validate(trafficPattern); err != nil {
|
||||||
|
return fmt.Errorf("invalid traffic pattern %q: %w", options.TrafficPattern, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -67,27 +67,27 @@ func NewInbound(ctx context.Context, router adapter.Router, logger log.ContextLo
|
|||||||
protoConf.AEADMethod = options.AEADMethod
|
protoConf.AEADMethod = options.AEADMethod
|
||||||
}
|
}
|
||||||
|
|
||||||
in := &Inbound{
|
inbound := &Inbound{
|
||||||
Adapter: inbound.NewAdapter(C.TypeSudoku, tag),
|
Adapter: inbound.NewAdapter(C.TypeSudoku, tag),
|
||||||
router: router,
|
router: router,
|
||||||
logger: logger,
|
logger: logger,
|
||||||
protoConf: protoConf,
|
protoConf: protoConf,
|
||||||
fallback: strings.TrimSpace(options.Fallback),
|
fallback: strings.TrimSpace(options.Fallback),
|
||||||
}
|
}
|
||||||
if in.fallback != "" {
|
if inbound.fallback != "" {
|
||||||
in.tunnelSrv = sudoku.NewHTTPMaskTunnelServerWithFallback(&in.protoConf)
|
inbound.tunnelSrv = sudoku.NewHTTPMaskTunnelServerWithFallback(&inbound.protoConf)
|
||||||
} else {
|
} else {
|
||||||
in.tunnelSrv = sudoku.NewHTTPMaskTunnelServer(&in.protoConf)
|
inbound.tunnelSrv = sudoku.NewHTTPMaskTunnelServer(&inbound.protoConf)
|
||||||
}
|
}
|
||||||
|
|
||||||
in.listener = listener.New(listener.Options{
|
inbound.listener = listener.New(listener.Options{
|
||||||
Context: ctx,
|
Context: ctx,
|
||||||
Logger: logger,
|
Logger: logger,
|
||||||
Network: []string{N.NetworkTCP},
|
Network: []string{N.NetworkTCP},
|
||||||
Listen: options.ListenOptions,
|
Listen: options.ListenOptions,
|
||||||
ConnectionHandler: in,
|
ConnectionHandler: inbound,
|
||||||
})
|
})
|
||||||
return in, nil
|
return inbound, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (h *Inbound) Start(stage adapter.StartStage) error {
|
func (h *Inbound) Start(stage adapter.StartStage) error {
|
||||||
@@ -173,6 +173,7 @@ func (h *Inbound) routeTCP(ctx context.Context, conn net.Conn, target string, me
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (h *Inbound) handleUoT(ctx context.Context, conn net.Conn, metadata adapter.InboundContext, onClose N.CloseHandlerFunc) {
|
func (h *Inbound) handleUoT(ctx context.Context, conn net.Conn, metadata adapter.InboundContext, onClose N.CloseHandlerFunc) {
|
||||||
|
h.logger.InfoContext(ctx, "inbound packet connection")
|
||||||
packetConn := sudoku.NewUoTPacketConn(conn)
|
packetConn := sudoku.NewUoTPacketConn(conn)
|
||||||
h.router.RoutePacketConnectionEx(ctx, bufio.NewPacketConn(packetConn), metadata, onClose)
|
h.router.RoutePacketConnectionEx(ctx, bufio.NewPacketConn(packetConn), metadata, onClose)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,10 +2,8 @@ package sudoku
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"fmt"
|
|
||||||
"net"
|
"net"
|
||||||
"strings"
|
"strings"
|
||||||
"sync"
|
|
||||||
|
|
||||||
"github.com/sagernet/sing-box/adapter"
|
"github.com/sagernet/sing-box/adapter"
|
||||||
"github.com/sagernet/sing-box/adapter/outbound"
|
"github.com/sagernet/sing-box/adapter/outbound"
|
||||||
@@ -15,7 +13,6 @@ import (
|
|||||||
"github.com/sagernet/sing-box/log"
|
"github.com/sagernet/sing-box/log"
|
||||||
"github.com/sagernet/sing-box/option"
|
"github.com/sagernet/sing-box/option"
|
||||||
"github.com/sagernet/sing-box/transport/sudoku"
|
"github.com/sagernet/sing-box/transport/sudoku"
|
||||||
"github.com/sagernet/sing-box/transport/sudoku/obfs/httpmask"
|
|
||||||
"github.com/sagernet/sing/common"
|
"github.com/sagernet/sing/common"
|
||||||
"github.com/sagernet/sing/common/bufio"
|
"github.com/sagernet/sing/common/bufio"
|
||||||
E "github.com/sagernet/sing/common/exceptions"
|
E "github.com/sagernet/sing/common/exceptions"
|
||||||
@@ -31,16 +28,8 @@ func RegisterOutbound(registry *outbound.Registry) {
|
|||||||
type Outbound struct {
|
type Outbound struct {
|
||||||
outbound.Adapter
|
outbound.Adapter
|
||||||
logger logger.ContextLogger
|
logger logger.ContextLogger
|
||||||
dialer N.Dialer
|
client *sudoku.Client
|
||||||
tlsConfig tls.Config
|
tlsConfig tls.Config
|
||||||
baseConf sudoku.ProtocolConfig
|
|
||||||
|
|
||||||
muxMu sync.Mutex
|
|
||||||
muxClient *sudoku.MultiplexClient
|
|
||||||
|
|
||||||
httpMaskMu sync.Mutex
|
|
||||||
httpMaskClient *httpmask.TunnelClient
|
|
||||||
httpMaskKey string
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewOutbound(ctx context.Context, router adapter.Router, logger log.ContextLogger, tag string, options option.SudokuOutboundOptions) (adapter.Outbound, error) {
|
func NewOutbound(ctx context.Context, router adapter.Router, logger log.ContextLogger, tag string, options option.SudokuOutboundOptions) (adapter.Outbound, error) {
|
||||||
@@ -105,33 +94,31 @@ func NewOutbound(ctx context.Context, router adapter.Router, logger log.ContextL
|
|||||||
baseConf.Tables = tables
|
baseConf.Tables = tables
|
||||||
}
|
}
|
||||||
|
|
||||||
out := &Outbound{
|
var tlsConfig tls.Config
|
||||||
Adapter: outbound.NewAdapterWithDialerOptions(C.TypeSudoku, tag, []string{N.NetworkTCP, N.NetworkUDP}, options.DialerOptions),
|
|
||||||
logger: logger,
|
|
||||||
dialer: outboundDialer,
|
|
||||||
baseConf: baseConf,
|
|
||||||
}
|
|
||||||
if hm := options.HTTPMask; !disableHTTPMask && hm != nil && hm.TLS != nil && hm.TLS.Enabled {
|
if hm := options.HTTPMask; !disableHTTPMask && hm != nil && hm.TLS != nil && hm.TLS.Enabled {
|
||||||
tlsOptions := option.OutboundTLSOptions{
|
tlsConfig, err = tls.NewClientWithOptions(tls.ClientOptions{
|
||||||
Enabled: true,
|
|
||||||
ServerName: options.Server,
|
|
||||||
Fragment: hm.TLS.Fragment,
|
|
||||||
FragmentFallbackDelay: hm.TLS.FragmentFallbackDelay,
|
|
||||||
RecordFragment: hm.TLS.RecordFragment,
|
|
||||||
KernelTx: hm.TLS.KernelTx,
|
|
||||||
KernelRx: hm.TLS.KernelRx,
|
|
||||||
}
|
|
||||||
out.tlsConfig, err = tls.NewClientWithOptions(tls.ClientOptions{
|
|
||||||
Context: ctx,
|
Context: ctx,
|
||||||
Logger: logger,
|
Logger: logger,
|
||||||
ServerAddress: options.Server,
|
ServerAddress: options.Server,
|
||||||
Options: tlsOptions,
|
Options: *hm.TLS,
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return out, nil
|
|
||||||
|
client := sudoku.NewClient(sudoku.ClientOptions{
|
||||||
|
Dialer: outboundDialer,
|
||||||
|
TLSConfig: tlsConfig,
|
||||||
|
Config: baseConf,
|
||||||
|
})
|
||||||
|
|
||||||
|
return &Outbound{
|
||||||
|
Adapter: outbound.NewAdapterWithDialerOptions(C.TypeSudoku, tag, []string{N.NetworkTCP, N.NetworkUDP}, options.DialerOptions),
|
||||||
|
logger: logger,
|
||||||
|
client: client,
|
||||||
|
tlsConfig: tlsConfig,
|
||||||
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (h *Outbound) DialContext(ctx context.Context, network string, destination M.Socksaddr) (net.Conn, error) {
|
func (h *Outbound) DialContext(ctx context.Context, network string, destination M.Socksaddr) (net.Conn, error) {
|
||||||
@@ -144,35 +131,7 @@ func (h *Outbound) DialContext(ctx context.Context, network string, destination
|
|||||||
ctx, metadata := adapter.ExtendContext(ctx)
|
ctx, metadata := adapter.ExtendContext(ctx)
|
||||||
metadata.Outbound = h.Tag()
|
metadata.Outbound = h.Tag()
|
||||||
metadata.Destination = destination
|
metadata.Destination = destination
|
||||||
|
return h.client.DialContext(ctx, network, destination)
|
||||||
cfg := h.baseConf
|
|
||||||
cfg.TargetAddress = destination.String()
|
|
||||||
|
|
||||||
muxMode := normalizeHTTPMaskMultiplex(cfg.HTTPMaskMultiplex)
|
|
||||||
if muxMode == "on" && !cfg.DisableHTTPMask && httpTunnelModeEnabled(cfg.HTTPMaskMode) {
|
|
||||||
stream, err := h.dialMultiplex(ctx, cfg.TargetAddress)
|
|
||||||
if err == nil {
|
|
||||||
return stream, nil
|
|
||||||
}
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
c, err := h.dialAndHandshake(ctx, &cfg)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
addrBuf, err := sudoku.EncodeAddress(cfg.TargetAddress)
|
|
||||||
if err != nil {
|
|
||||||
c.Close()
|
|
||||||
return nil, E.Cause(err, "encode target address")
|
|
||||||
}
|
|
||||||
if err = sudoku.WriteKIPMessage(c, sudoku.KIPTypeOpenTCP, addrBuf); err != nil {
|
|
||||||
c.Close()
|
|
||||||
return nil, E.Cause(err, "send target address")
|
|
||||||
}
|
|
||||||
|
|
||||||
return c, nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (h *Outbound) ListenPacket(ctx context.Context, destination M.Socksaddr) (net.PacketConn, error) {
|
func (h *Outbound) ListenPacket(ctx context.Context, destination M.Socksaddr) (net.PacketConn, error) {
|
||||||
@@ -180,222 +139,18 @@ func (h *Outbound) ListenPacket(ctx context.Context, destination M.Socksaddr) (n
|
|||||||
ctx, metadata := adapter.ExtendContext(ctx)
|
ctx, metadata := adapter.ExtendContext(ctx)
|
||||||
metadata.Outbound = h.Tag()
|
metadata.Outbound = h.Tag()
|
||||||
metadata.Destination = destination
|
metadata.Destination = destination
|
||||||
|
conn, err := h.client.DialContext(ctx, N.NetworkUDP, destination)
|
||||||
cfg := h.baseConf
|
|
||||||
cfg.TargetAddress = destination.String()
|
|
||||||
|
|
||||||
c, err := h.dialAndHandshake(ctx, &cfg)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
return bufio.NewBindPacketConn(sudoku.NewUoTPacketConn(conn), destination), nil
|
||||||
if err = sudoku.WriteKIPMessage(c, sudoku.KIPTypeStartUoT, nil); err != nil {
|
|
||||||
c.Close()
|
|
||||||
return nil, E.Cause(err, "start uot")
|
|
||||||
}
|
|
||||||
|
|
||||||
return bufio.NewBindPacketConn(sudoku.NewUoTPacketConn(c), destination), nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (h *Outbound) Close() error {
|
func (h *Outbound) Close() error {
|
||||||
h.resetMuxClient()
|
h.client.Close()
|
||||||
h.resetHTTPMaskClient()
|
|
||||||
return common.Close(h.tlsConfig)
|
return common.Close(h.tlsConfig)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (h *Outbound) InterfaceUpdated() {
|
func (h *Outbound) InterfaceUpdated() {
|
||||||
h.resetMuxClient()
|
h.client.Close()
|
||||||
h.resetHTTPMaskClient()
|
|
||||||
}
|
|
||||||
|
|
||||||
func (h *Outbound) dialAndHandshake(ctx context.Context, cfg *sudoku.ProtocolConfig) (net.Conn, error) {
|
|
||||||
handshakeCfg := *cfg
|
|
||||||
if !handshakeCfg.DisableHTTPMask && httpTunnelModeEnabled(handshakeCfg.HTTPMaskMode) {
|
|
||||||
handshakeCfg.DisableHTTPMask = true
|
|
||||||
}
|
|
||||||
|
|
||||||
upgrade := func(raw net.Conn) (net.Conn, error) {
|
|
||||||
return sudoku.ClientHandshake(raw, &handshakeCfg)
|
|
||||||
}
|
|
||||||
|
|
||||||
var c net.Conn
|
|
||||||
var err error
|
|
||||||
var handshakeDone bool
|
|
||||||
|
|
||||||
if !cfg.DisableHTTPMask && httpTunnelModeEnabled(cfg.HTTPMaskMode) {
|
|
||||||
muxMode := normalizeHTTPMaskMultiplex(cfg.HTTPMaskMultiplex)
|
|
||||||
if muxMode == "auto" && strings.ToLower(strings.TrimSpace(cfg.HTTPMaskMode)) != "ws" {
|
|
||||||
if client, cerr := h.getOrCreateHTTPMaskClient(cfg); cerr == nil && client != nil {
|
|
||||||
c, err = client.DialTunnel(ctx, httpmask.TunnelDialOptions{
|
|
||||||
Mode: cfg.HTTPMaskMode,
|
|
||||||
TLSConfig: h.httpMaskTLSConfig(),
|
|
||||||
HostOverride: cfg.HTTPMaskHost,
|
|
||||||
PathRoot: cfg.HTTPMaskPathRoot,
|
|
||||||
AuthKey: sudoku.ClientAEADSeed(cfg.Key),
|
|
||||||
Upgrade: upgrade,
|
|
||||||
Multiplex: cfg.HTTPMaskMultiplex,
|
|
||||||
DialContext: h.dialRaw,
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
h.resetHTTPMaskClient()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if c == nil && err == nil {
|
|
||||||
c, err = sudoku.DialHTTPMaskTunnel(ctx, cfg.ServerAddress, cfg, h.dialRaw, upgrade)
|
|
||||||
}
|
|
||||||
if err == nil && c != nil {
|
|
||||||
handshakeDone = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if c == nil && err == nil {
|
|
||||||
c, err = h.dialer.DialContext(ctx, N.NetworkTCP, M.ParseSocksaddr(cfg.ServerAddress))
|
|
||||||
}
|
|
||||||
if err != nil {
|
|
||||||
return nil, E.Cause(err, "connect to ", cfg.ServerAddress)
|
|
||||||
}
|
|
||||||
|
|
||||||
if !handshakeDone {
|
|
||||||
c, err = sudoku.ClientHandshake(c, &handshakeCfg)
|
|
||||||
if err != nil {
|
|
||||||
common.Close(c)
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return c, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (h *Outbound) dialRaw(ctx context.Context, network, addr string) (net.Conn, error) {
|
|
||||||
return h.dialer.DialContext(ctx, network, M.ParseSocksaddr(addr))
|
|
||||||
}
|
|
||||||
|
|
||||||
func (h *Outbound) httpMaskTLSConfig() httpmask.TLSClientConfig {
|
|
||||||
if h.tlsConfig == nil {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
return tlsConfigAdapter{h.tlsConfig}
|
|
||||||
}
|
|
||||||
|
|
||||||
type tlsConfigAdapter struct {
|
|
||||||
config tls.Config
|
|
||||||
}
|
|
||||||
|
|
||||||
func (a tlsConfigAdapter) Client(conn net.Conn) (net.Conn, error) {
|
|
||||||
return a.config.Client(conn)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (h *Outbound) dialMultiplex(ctx context.Context, targetAddress string) (net.Conn, error) {
|
|
||||||
for attempt := 0; attempt < 2; attempt++ {
|
|
||||||
client, err := h.getOrCreateMuxClient(ctx)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
stream, err := client.Dial(ctx, targetAddress)
|
|
||||||
if err != nil {
|
|
||||||
h.resetMuxClient()
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
return stream, nil
|
|
||||||
}
|
|
||||||
return nil, fmt.Errorf("multiplex open stream failed")
|
|
||||||
}
|
|
||||||
|
|
||||||
func (h *Outbound) getOrCreateMuxClient(ctx context.Context) (*sudoku.MultiplexClient, error) {
|
|
||||||
h.muxMu.Lock()
|
|
||||||
defer h.muxMu.Unlock()
|
|
||||||
|
|
||||||
if h.muxClient != nil && !h.muxClient.IsClosed() {
|
|
||||||
return h.muxClient, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
baseCfg := h.baseConf
|
|
||||||
baseConn, err := h.dialAndHandshake(ctx, &baseCfg)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
client, err := sudoku.StartMultiplexClient(baseConn)
|
|
||||||
if err != nil {
|
|
||||||
baseConn.Close()
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
h.muxClient = client
|
|
||||||
return client, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (h *Outbound) resetMuxClient() {
|
|
||||||
h.muxMu.Lock()
|
|
||||||
defer h.muxMu.Unlock()
|
|
||||||
if h.muxClient != nil {
|
|
||||||
h.muxClient.Close()
|
|
||||||
h.muxClient = nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (h *Outbound) getOrCreateHTTPMaskClient(cfg *sudoku.ProtocolConfig) (*httpmask.TunnelClient, error) {
|
|
||||||
key := cfg.ServerAddress + "|" + fmt.Sprint(h.tlsConfig != nil) + "|" + strings.TrimSpace(cfg.HTTPMaskHost)
|
|
||||||
|
|
||||||
h.httpMaskMu.Lock()
|
|
||||||
if h.httpMaskClient != nil && h.httpMaskKey == key {
|
|
||||||
client := h.httpMaskClient
|
|
||||||
h.httpMaskMu.Unlock()
|
|
||||||
return client, nil
|
|
||||||
}
|
|
||||||
h.httpMaskMu.Unlock()
|
|
||||||
|
|
||||||
client, err := httpmask.NewTunnelClient(cfg.ServerAddress, httpmask.TunnelClientOptions{
|
|
||||||
TLSConfig: h.httpMaskTLSConfig(),
|
|
||||||
HostOverride: cfg.HTTPMaskHost,
|
|
||||||
DialContext: h.dialRaw,
|
|
||||||
MaxIdleConns: 32,
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
h.httpMaskMu.Lock()
|
|
||||||
defer h.httpMaskMu.Unlock()
|
|
||||||
if h.httpMaskClient != nil && h.httpMaskKey == key {
|
|
||||||
client.CloseIdleConnections()
|
|
||||||
return h.httpMaskClient, nil
|
|
||||||
}
|
|
||||||
if h.httpMaskClient != nil {
|
|
||||||
h.httpMaskClient.CloseIdleConnections()
|
|
||||||
}
|
|
||||||
h.httpMaskClient = client
|
|
||||||
h.httpMaskKey = key
|
|
||||||
return client, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (h *Outbound) resetHTTPMaskClient() {
|
|
||||||
h.httpMaskMu.Lock()
|
|
||||||
defer h.httpMaskMu.Unlock()
|
|
||||||
if h.httpMaskClient != nil {
|
|
||||||
h.httpMaskClient.CloseIdleConnections()
|
|
||||||
h.httpMaskClient = nil
|
|
||||||
h.httpMaskKey = ""
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func normalizeHTTPMaskMultiplex(mode string) string {
|
|
||||||
switch strings.ToLower(strings.TrimSpace(mode)) {
|
|
||||||
case "", "off":
|
|
||||||
return "off"
|
|
||||||
case "auto":
|
|
||||||
return "auto"
|
|
||||||
case "on":
|
|
||||||
return "on"
|
|
||||||
default:
|
|
||||||
return "off"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func httpTunnelModeEnabled(mode string) bool {
|
|
||||||
switch strings.ToLower(strings.TrimSpace(mode)) {
|
|
||||||
case "stream", "poll", "auto", "ws":
|
|
||||||
return true
|
|
||||||
default:
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -138,7 +138,7 @@ func (h *Inbound) Close() error {
|
|||||||
return common.Close(
|
return common.Close(
|
||||||
h.listener,
|
h.listener,
|
||||||
common.PtrOrNil(h.httpServer),
|
common.PtrOrNil(h.httpServer),
|
||||||
h.quicService,
|
common.PtrOrNil(h.quicService),
|
||||||
h.tlsConfig,
|
h.tlsConfig,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -279,7 +279,7 @@ func (m *ConnectionManager) connectionCopy(ctx context.Context, source net.Conn,
|
|||||||
if !direction {
|
if !direction {
|
||||||
if err == nil {
|
if err == nil {
|
||||||
m.logger.DebugContext(ctx, "connection upload finished")
|
m.logger.DebugContext(ctx, "connection upload finished")
|
||||||
} else if !E.IsClosedOrCanceled(err) && !strings.Contains(err.Error(), "NO_ERROR") {
|
} else if !E.IsClosedOrCanceled(err) && !strings.Contains(err.Error(), "NO_ERROR") && !strings.Contains(err.Error(), "CANCEL") && !strings.Contains(err.Error(), "body closed") {
|
||||||
m.logger.ErrorContext(ctx, "connection upload closed: ", err)
|
m.logger.ErrorContext(ctx, "connection upload closed: ", err)
|
||||||
} else {
|
} else {
|
||||||
m.logger.TraceContext(ctx, "connection upload closed")
|
m.logger.TraceContext(ctx, "connection upload closed")
|
||||||
|
|||||||
@@ -75,7 +75,7 @@ require (
|
|||||||
github.com/dunglas/httpsfv v1.1.0 // indirect
|
github.com/dunglas/httpsfv v1.1.0 // indirect
|
||||||
github.com/dustin/go-humanize v1.0.1 // indirect
|
github.com/dustin/go-humanize v1.0.1 // indirect
|
||||||
github.com/ebitengine/purego v0.10.0 // indirect
|
github.com/ebitengine/purego v0.10.0 // indirect
|
||||||
github.com/enfein/mieru/v3 v3.17.1 // indirect
|
github.com/enfein/mieru/v3 v3.33.0 // indirect
|
||||||
github.com/felixge/httpsnoop v1.0.4 // indirect
|
github.com/felixge/httpsnoop v1.0.4 // indirect
|
||||||
github.com/florianl/go-nfqueue/v2 v2.0.2 // indirect
|
github.com/florianl/go-nfqueue/v2 v2.0.2 // indirect
|
||||||
github.com/fsnotify/fsnotify v1.9.0 // indirect
|
github.com/fsnotify/fsnotify v1.9.0 // indirect
|
||||||
|
|||||||
@@ -88,8 +88,8 @@ github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkp
|
|||||||
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
|
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
|
||||||
github.com/ebitengine/purego v0.10.0 h1:QIw4xfpWT6GWTzaW5XEKy3HXoqrJGx1ijYHzTF0/ISU=
|
github.com/ebitengine/purego v0.10.0 h1:QIw4xfpWT6GWTzaW5XEKy3HXoqrJGx1ijYHzTF0/ISU=
|
||||||
github.com/ebitengine/purego v0.10.0/go.mod h1:iIjxzd6CiRiOG0UyXP+V1+jWqUXVjPKLAI0mRfJZTmQ=
|
github.com/ebitengine/purego v0.10.0/go.mod h1:iIjxzd6CiRiOG0UyXP+V1+jWqUXVjPKLAI0mRfJZTmQ=
|
||||||
github.com/enfein/mieru/v3 v3.17.1 h1:pIKbspsKRYNyUrORVI33t1/yz2syaaUkIanskAbGBHY=
|
github.com/enfein/mieru/v3 v3.33.0 h1:hv2jK8nqYHwpSG86U2rpZR2I8Aff1/J3ifRmd9NBbFc=
|
||||||
github.com/enfein/mieru/v3 v3.17.1/go.mod h1:zJBUCsi5rxyvHM8fjFf+GLaEl4OEjjBXr1s5F6Qd3hM=
|
github.com/enfein/mieru/v3 v3.33.0/go.mod h1:zJBUCsi5rxyvHM8fjFf+GLaEl4OEjjBXr1s5F6Qd3hM=
|
||||||
github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg=
|
github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg=
|
||||||
github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
|
github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
|
||||||
github.com/florianl/go-nfqueue/v2 v2.0.2 h1:FL5lQTeetgpCvac1TRwSfgaXUn0YSO7WzGvWNIp3JPE=
|
github.com/florianl/go-nfqueue/v2 v2.0.2 h1:FL5lQTeetgpCvac1TRwSfgaXUn0YSO7WzGvWNIp3JPE=
|
||||||
|
|||||||
293
transport/sudoku/client.go
Normal file
293
transport/sudoku/client.go
Normal file
@@ -0,0 +1,293 @@
|
|||||||
|
package sudoku
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"net"
|
||||||
|
"strings"
|
||||||
|
"sync"
|
||||||
|
|
||||||
|
"github.com/sagernet/sing-box/common/tls"
|
||||||
|
"github.com/sagernet/sing-box/transport/sudoku/obfs/httpmask"
|
||||||
|
"github.com/sagernet/sing/common"
|
||||||
|
E "github.com/sagernet/sing/common/exceptions"
|
||||||
|
M "github.com/sagernet/sing/common/metadata"
|
||||||
|
N "github.com/sagernet/sing/common/network"
|
||||||
|
)
|
||||||
|
|
||||||
|
type ClientOptions struct {
|
||||||
|
Dialer N.Dialer
|
||||||
|
TLSConfig tls.Config
|
||||||
|
Config ProtocolConfig
|
||||||
|
}
|
||||||
|
|
||||||
|
type Client struct {
|
||||||
|
dialer N.Dialer
|
||||||
|
tlsConfig tls.Config
|
||||||
|
baseConf ProtocolConfig
|
||||||
|
|
||||||
|
muxMu sync.Mutex
|
||||||
|
muxClient *MultiplexClient
|
||||||
|
|
||||||
|
httpMaskMu sync.Mutex
|
||||||
|
httpMaskClient *httpmask.TunnelClient
|
||||||
|
httpMaskKey string
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewClient(options ClientOptions) *Client {
|
||||||
|
return &Client{
|
||||||
|
dialer: options.Dialer,
|
||||||
|
tlsConfig: options.TLSConfig,
|
||||||
|
baseConf: options.Config,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Client) DialContext(ctx context.Context, network string, destination M.Socksaddr) (net.Conn, error) {
|
||||||
|
cfg := c.baseConf
|
||||||
|
cfg.TargetAddress = destination.String()
|
||||||
|
|
||||||
|
muxMode := normalizeHTTPMaskMultiplex(cfg.HTTPMaskMultiplex)
|
||||||
|
if muxMode == "on" && !cfg.DisableHTTPMask && httpTunnelModeEnabled(cfg.HTTPMaskMode) {
|
||||||
|
stream, err := c.dialMultiplex(ctx, cfg.TargetAddress)
|
||||||
|
if err == nil {
|
||||||
|
return stream, nil
|
||||||
|
}
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
conn, err := c.dialAndHandshake(ctx, &cfg)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
switch N.NetworkName(network) {
|
||||||
|
case N.NetworkUDP:
|
||||||
|
if err = WriteKIPMessage(conn, KIPTypeStartUoT, nil); err != nil {
|
||||||
|
conn.Close()
|
||||||
|
return nil, E.Cause(err, "start uot")
|
||||||
|
}
|
||||||
|
return conn, nil
|
||||||
|
default:
|
||||||
|
addrBuf, err := EncodeAddress(cfg.TargetAddress)
|
||||||
|
if err != nil {
|
||||||
|
conn.Close()
|
||||||
|
return nil, E.Cause(err, "encode target address")
|
||||||
|
}
|
||||||
|
if err = WriteKIPMessage(conn, KIPTypeOpenTCP, addrBuf); err != nil {
|
||||||
|
conn.Close()
|
||||||
|
return nil, E.Cause(err, "send target address")
|
||||||
|
}
|
||||||
|
return conn, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Client) Close() {
|
||||||
|
c.resetMuxClient()
|
||||||
|
c.resetHTTPMaskClient()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Client) dialAndHandshake(ctx context.Context, cfg *ProtocolConfig) (net.Conn, error) {
|
||||||
|
handshakeCfg := *cfg
|
||||||
|
if !handshakeCfg.DisableHTTPMask && httpTunnelModeEnabled(handshakeCfg.HTTPMaskMode) {
|
||||||
|
handshakeCfg.DisableHTTPMask = true
|
||||||
|
}
|
||||||
|
|
||||||
|
upgrade := func(raw net.Conn) (net.Conn, error) {
|
||||||
|
return ClientHandshake(raw, &handshakeCfg)
|
||||||
|
}
|
||||||
|
|
||||||
|
var conn net.Conn
|
||||||
|
var err error
|
||||||
|
var handshakeDone bool
|
||||||
|
|
||||||
|
if !cfg.DisableHTTPMask && httpTunnelModeEnabled(cfg.HTTPMaskMode) {
|
||||||
|
muxMode := normalizeHTTPMaskMultiplex(cfg.HTTPMaskMultiplex)
|
||||||
|
if muxMode == "auto" && strings.ToLower(strings.TrimSpace(cfg.HTTPMaskMode)) != "ws" {
|
||||||
|
if client, cerr := c.getOrCreateHTTPMaskClient(cfg); cerr == nil && client != nil {
|
||||||
|
conn, err = client.DialTunnel(ctx, httpmask.TunnelDialOptions{
|
||||||
|
Mode: cfg.HTTPMaskMode,
|
||||||
|
TLSConfig: c.httpMaskTLSConfig(),
|
||||||
|
HostOverride: cfg.HTTPMaskHost,
|
||||||
|
PathRoot: cfg.HTTPMaskPathRoot,
|
||||||
|
AuthKey: ClientAEADSeed(cfg.Key),
|
||||||
|
Upgrade: upgrade,
|
||||||
|
Multiplex: cfg.HTTPMaskMultiplex,
|
||||||
|
DialContext: c.dialRaw,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
c.resetHTTPMaskClient()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if conn == nil && err == nil {
|
||||||
|
conn, err = c.dialHTTPMaskTunnel(ctx, cfg, upgrade)
|
||||||
|
}
|
||||||
|
if err == nil && conn != nil {
|
||||||
|
handshakeDone = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if conn == nil && err == nil {
|
||||||
|
conn, err = c.dialer.DialContext(ctx, N.NetworkTCP, M.ParseSocksaddr(cfg.ServerAddress))
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
return nil, E.Cause(err, "connect to ", cfg.ServerAddress)
|
||||||
|
}
|
||||||
|
|
||||||
|
if !handshakeDone {
|
||||||
|
conn, err = ClientHandshake(conn, &handshakeCfg)
|
||||||
|
if err != nil {
|
||||||
|
common.Close(conn)
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return conn, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Client) dialRaw(ctx context.Context, network, addr string) (net.Conn, error) {
|
||||||
|
return c.dialer.DialContext(ctx, network, M.ParseSocksaddr(addr))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Client) dialHTTPMaskTunnel(ctx context.Context, cfg *ProtocolConfig, upgrade func(net.Conn) (net.Conn, error)) (net.Conn, error) {
|
||||||
|
var earlyHandshake *httpmask.ClientEarlyHandshake
|
||||||
|
var err error
|
||||||
|
if upgrade != nil {
|
||||||
|
earlyHandshake, err = newClientHTTPMaskEarlyHandshake(cfg)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return httpmask.DialTunnel(ctx, cfg.ServerAddress, httpmask.TunnelDialOptions{
|
||||||
|
Mode: cfg.HTTPMaskMode,
|
||||||
|
TLSConfig: c.httpMaskTLSConfig(),
|
||||||
|
HostOverride: cfg.HTTPMaskHost,
|
||||||
|
PathRoot: cfg.HTTPMaskPathRoot,
|
||||||
|
AuthKey: ClientAEADSeed(cfg.Key),
|
||||||
|
EarlyHandshake: earlyHandshake,
|
||||||
|
Upgrade: upgrade,
|
||||||
|
Multiplex: cfg.HTTPMaskMultiplex,
|
||||||
|
DialContext: c.dialRaw,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Client) httpMaskTLSConfig() tls.Config {
|
||||||
|
if c.tlsConfig == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return c.tlsConfig
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Client) dialMultiplex(ctx context.Context, targetAddress string) (net.Conn, error) {
|
||||||
|
for attempt := 0; attempt < 2; attempt++ {
|
||||||
|
client, err := c.getOrCreateMuxClient(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
stream, err := client.Dial(ctx, targetAddress)
|
||||||
|
if err != nil {
|
||||||
|
c.resetMuxClient()
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
return stream, nil
|
||||||
|
}
|
||||||
|
return nil, fmt.Errorf("multiplex open stream failed")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Client) getOrCreateMuxClient(ctx context.Context) (*MultiplexClient, error) {
|
||||||
|
c.muxMu.Lock()
|
||||||
|
defer c.muxMu.Unlock()
|
||||||
|
|
||||||
|
if c.muxClient != nil && !c.muxClient.IsClosed() {
|
||||||
|
return c.muxClient, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
baseCfg := c.baseConf
|
||||||
|
baseConn, err := c.dialAndHandshake(ctx, &baseCfg)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
client, err := StartMultiplexClient(baseConn)
|
||||||
|
if err != nil {
|
||||||
|
baseConn.Close()
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
c.muxClient = client
|
||||||
|
return client, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Client) resetMuxClient() {
|
||||||
|
c.muxMu.Lock()
|
||||||
|
defer c.muxMu.Unlock()
|
||||||
|
if c.muxClient != nil {
|
||||||
|
c.muxClient.Close()
|
||||||
|
c.muxClient = nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Client) getOrCreateHTTPMaskClient(cfg *ProtocolConfig) (*httpmask.TunnelClient, error) {
|
||||||
|
key := cfg.ServerAddress + "|" + fmt.Sprint(c.tlsConfig != nil) + "|" + strings.TrimSpace(cfg.HTTPMaskHost)
|
||||||
|
|
||||||
|
c.httpMaskMu.Lock()
|
||||||
|
if c.httpMaskClient != nil && c.httpMaskKey == key {
|
||||||
|
client := c.httpMaskClient
|
||||||
|
c.httpMaskMu.Unlock()
|
||||||
|
return client, nil
|
||||||
|
}
|
||||||
|
c.httpMaskMu.Unlock()
|
||||||
|
|
||||||
|
client, err := httpmask.NewTunnelClient(cfg.ServerAddress, httpmask.TunnelClientOptions{
|
||||||
|
TLSConfig: c.httpMaskTLSConfig(),
|
||||||
|
HostOverride: cfg.HTTPMaskHost,
|
||||||
|
DialContext: c.dialRaw,
|
||||||
|
MaxIdleConns: 32,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
c.httpMaskMu.Lock()
|
||||||
|
defer c.httpMaskMu.Unlock()
|
||||||
|
if c.httpMaskClient != nil && c.httpMaskKey == key {
|
||||||
|
client.CloseIdleConnections()
|
||||||
|
return c.httpMaskClient, nil
|
||||||
|
}
|
||||||
|
if c.httpMaskClient != nil {
|
||||||
|
c.httpMaskClient.CloseIdleConnections()
|
||||||
|
}
|
||||||
|
c.httpMaskClient = client
|
||||||
|
c.httpMaskKey = key
|
||||||
|
return client, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Client) resetHTTPMaskClient() {
|
||||||
|
c.httpMaskMu.Lock()
|
||||||
|
defer c.httpMaskMu.Unlock()
|
||||||
|
if c.httpMaskClient != nil {
|
||||||
|
c.httpMaskClient.CloseIdleConnections()
|
||||||
|
c.httpMaskClient = nil
|
||||||
|
c.httpMaskKey = ""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func normalizeHTTPMaskMultiplex(mode string) string {
|
||||||
|
switch strings.ToLower(strings.TrimSpace(mode)) {
|
||||||
|
case "", "off":
|
||||||
|
return "off"
|
||||||
|
case "auto":
|
||||||
|
return "auto"
|
||||||
|
case "on":
|
||||||
|
return "on"
|
||||||
|
default:
|
||||||
|
return "off"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func httpTunnelModeEnabled(mode string) bool {
|
||||||
|
switch strings.ToLower(strings.TrimSpace(mode)) {
|
||||||
|
case "stream", "poll", "auto", "ws":
|
||||||
|
return true
|
||||||
|
default:
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -22,11 +22,9 @@ import (
|
|||||||
|
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/http/httputil"
|
"net/http/httputil"
|
||||||
)
|
|
||||||
|
|
||||||
type TLSClientConfig interface {
|
"github.com/sagernet/sing-box/common/tls"
|
||||||
Client(conn net.Conn) (net.Conn, error)
|
)
|
||||||
}
|
|
||||||
|
|
||||||
type TunnelMode string
|
type TunnelMode string
|
||||||
|
|
||||||
@@ -66,7 +64,7 @@ const (
|
|||||||
|
|
||||||
type TunnelDialOptions struct {
|
type TunnelDialOptions struct {
|
||||||
Mode string
|
Mode string
|
||||||
TLSConfig TLSClientConfig
|
TLSConfig tls.Config
|
||||||
HostOverride string
|
HostOverride string
|
||||||
// PathRoot is an optional first-level path prefix for all HTTP tunnel endpoints.
|
// PathRoot is an optional first-level path prefix for all HTTP tunnel endpoints.
|
||||||
// Example: "aabbcc" => "/aabbcc/session", "/aabbcc/api/v1/upload", ...
|
// Example: "aabbcc" => "/aabbcc/session", "/aabbcc/api/v1/upload", ...
|
||||||
@@ -91,7 +89,7 @@ type TunnelDialOptions struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type TunnelClientOptions struct {
|
type TunnelClientOptions struct {
|
||||||
TLSConfig TLSClientConfig
|
TLSConfig tls.Config
|
||||||
HostOverride string
|
HostOverride string
|
||||||
DialContext func(ctx context.Context, network, addr string) (net.Conn, error)
|
DialContext func(ctx context.Context, network, addr string) (net.Conn, error)
|
||||||
MaxIdleConns int
|
MaxIdleConns int
|
||||||
@@ -244,7 +242,7 @@ type httpClientTarget struct {
|
|||||||
headerHost string
|
headerHost string
|
||||||
}
|
}
|
||||||
|
|
||||||
func buildHTTPTransport(serverAddress string, tlsEnabled bool, tlsConfig TLSClientConfig, hostOverride string, dialContext func(ctx context.Context, network, addr string) (net.Conn, error), maxIdleConns int) (*http.Transport, httpClientTarget, error) {
|
func buildHTTPTransport(serverAddress string, tlsEnabled bool, tlsConfig tls.Config, hostOverride string, dialContext func(ctx context.Context, network, addr string) (net.Conn, error), maxIdleConns int) (*http.Transport, httpClientTarget, error) {
|
||||||
if dialContext == nil {
|
if dialContext == nil {
|
||||||
panic("httpmask: DialContext is nil")
|
panic("httpmask: DialContext is nil")
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -114,6 +114,9 @@ func (s *QUICService) Start(ctx context.Context, udpConn net.PacketConn, tlsConf
|
|||||||
return ctx
|
return ctx
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
if err := qtls.ConfigureHTTP3(tlsConfig); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
quicListener, err := qtls.ListenEarly(udpConn, tlsConfig, &quic.Config{
|
quicListener, err := qtls.ListenEarly(udpConn, tlsConfig, &quic.Config{
|
||||||
MaxIdleTimeout: DefaultSessionTimeout * 2,
|
MaxIdleTimeout: DefaultSessionTimeout * 2,
|
||||||
MaxIncomingStreams: 1 << 60,
|
MaxIncomingStreams: 1 << 60,
|
||||||
|
|||||||
Reference in New Issue
Block a user