Add snell protocol
This commit is contained in:
@@ -11,6 +11,7 @@ const (
|
||||
TypeHTTP = "http"
|
||||
TypeMixed = "mixed"
|
||||
TypeShadowsocks = "shadowsocks"
|
||||
TypeSnell = "snell"
|
||||
TypeVMess = "vmess"
|
||||
TypeTrojan = "trojan"
|
||||
TypeNaive = "naive"
|
||||
@@ -67,6 +68,8 @@ func ProxyDisplayName(proxyType string) string {
|
||||
return "Mixed"
|
||||
case TypeShadowsocks:
|
||||
return "Shadowsocks"
|
||||
case TypeSnell:
|
||||
return "Snell"
|
||||
case TypeVMess:
|
||||
return "VMess"
|
||||
case TypeTrojan:
|
||||
|
||||
@@ -31,6 +31,7 @@
|
||||
| `hysteria2` | [Hysteria2](./hysteria2/) | :material-close: |
|
||||
| `vless` | [VLESS](./vless/) | TCP |
|
||||
| `anytls` | [AnyTLS](./anytls/) | TCP |
|
||||
| `snell` | [Snell](./snell/) | TCP |
|
||||
| `tun` | [Tun](./tun/) | :material-close: |
|
||||
| `redirect` | [Redirect](./redirect/) | :material-close: |
|
||||
| `tproxy` | [TProxy](./tproxy/) | :material-close: |
|
||||
|
||||
@@ -31,6 +31,7 @@
|
||||
| `hysteria2` | [Hysteria2](./hysteria2/) | :material-close: |
|
||||
| `vless` | [VLESS](./vless/) | TCP |
|
||||
| `anytls` | [AnyTLS](./anytls/) | TCP |
|
||||
| `snell` | [Snell](./snell/) | TCP |
|
||||
| `tun` | [Tun](./tun/) | :material-close: |
|
||||
| `redirect` | [Redirect](./redirect/) | :material-close: |
|
||||
| `tproxy` | [TProxy](./tproxy/) | :material-close: |
|
||||
|
||||
@@ -0,0 +1,96 @@
|
||||
---
|
||||
icon: material/new-box
|
||||
---
|
||||
|
||||
!!! question "Since sing-box 1.14.0"
|
||||
|
||||
### Structure
|
||||
|
||||
```json
|
||||
{
|
||||
"type": "snell",
|
||||
"tag": "snell-in",
|
||||
|
||||
... // Listen Fields
|
||||
|
||||
"version": 5,
|
||||
"psk": "password",
|
||||
"users": [
|
||||
{
|
||||
"name": "sekai",
|
||||
"userkey": "user-password"
|
||||
}
|
||||
],
|
||||
"obfs_mode": ""
|
||||
}
|
||||
```
|
||||
|
||||
### Version 6 Structure
|
||||
|
||||
```json
|
||||
{
|
||||
"type": "snell",
|
||||
"tag": "snell-in",
|
||||
|
||||
... // Listen Fields
|
||||
|
||||
"version": 6,
|
||||
"psk": "password",
|
||||
"users": [
|
||||
{
|
||||
"name": "sekai",
|
||||
"userkey": "user-password"
|
||||
}
|
||||
],
|
||||
"mode": ""
|
||||
}
|
||||
```
|
||||
|
||||
### Listen Fields
|
||||
|
||||
See [Listen Fields](/configuration/shared/listen/) for details.
|
||||
|
||||
### Fields
|
||||
|
||||
#### version
|
||||
|
||||
==Required==
|
||||
|
||||
The Snell protocol version, one of `5` `6`.
|
||||
|
||||
Version `5` supports HTTP obfuscation (`obfs_mode`); version `6` replaces it
|
||||
with traffic shaping (`mode`) and requires a `psk` of 12 to 255 bytes.
|
||||
|
||||
!!! note
|
||||
|
||||
Since we intentionally do not support the QUIC proxy mode of Snell v5, the v5 wire protocol
|
||||
is effectively identical to v4, so no separate v4 server or v5 client is provided.
|
||||
|
||||
#### psk
|
||||
|
||||
==Required==
|
||||
|
||||
The pre-shared key.
|
||||
|
||||
#### users
|
||||
|
||||
Snell users.
|
||||
|
||||
When set, the server runs in multi-user mode: each entry has a `name` (optional, used in
|
||||
logs) and a `userkey` (the user's key). The top-level `psk` remains the server key.
|
||||
|
||||
#### obfs_mode
|
||||
|
||||
==Version 5 only==
|
||||
|
||||
HTTP obfuscation mode, one of `none` `http`.
|
||||
|
||||
`none` is used by default.
|
||||
|
||||
#### mode
|
||||
|
||||
==Version 6 only==
|
||||
|
||||
Traffic shaping mode, one of `default` `unshaped` `unsafe-raw`.
|
||||
|
||||
`default` is used by default.
|
||||
@@ -0,0 +1,96 @@
|
||||
---
|
||||
icon: material/new-box
|
||||
---
|
||||
|
||||
!!! question "自 sing-box 1.14.0 起"
|
||||
|
||||
### 结构
|
||||
|
||||
```json
|
||||
{
|
||||
"type": "snell",
|
||||
"tag": "snell-in",
|
||||
|
||||
... // 监听字段
|
||||
|
||||
"version": 5,
|
||||
"psk": "password",
|
||||
"users": [
|
||||
{
|
||||
"name": "sekai",
|
||||
"userkey": "user-password"
|
||||
}
|
||||
],
|
||||
"obfs_mode": ""
|
||||
}
|
||||
```
|
||||
|
||||
### 版本 6 结构
|
||||
|
||||
```json
|
||||
{
|
||||
"type": "snell",
|
||||
"tag": "snell-in",
|
||||
|
||||
... // 监听字段
|
||||
|
||||
"version": 6,
|
||||
"psk": "password",
|
||||
"users": [
|
||||
{
|
||||
"name": "sekai",
|
||||
"userkey": "user-password"
|
||||
}
|
||||
],
|
||||
"mode": ""
|
||||
}
|
||||
```
|
||||
|
||||
### 监听字段
|
||||
|
||||
参阅 [监听字段](/zh/configuration/shared/listen/)。
|
||||
|
||||
### 字段
|
||||
|
||||
#### version
|
||||
|
||||
==必填==
|
||||
|
||||
Snell 协议版本,`5` `6` 之一。
|
||||
|
||||
版本 `5` 支持 HTTP 混淆(`obfs_mode`);版本 `6` 以流量整形(`mode`)取而代之,并要求
|
||||
`psk` 长度为 12 到 255 字节。
|
||||
|
||||
!!! note
|
||||
|
||||
由于我们有意不支持 Snell v5 的 QUIC 代理模式,v5 的线路协议实际上与 v4 没有区别,
|
||||
因此不提供独立的 v4 服务器和 v5 客户端。
|
||||
|
||||
#### psk
|
||||
|
||||
==必填==
|
||||
|
||||
预共享密钥。
|
||||
|
||||
#### users
|
||||
|
||||
Snell 用户。
|
||||
|
||||
设置后,服务器运行于多用户模式:每一项包含 `name`(可选,用于日志)和 `userkey`
|
||||
(用户密钥)。顶层的 `psk` 仍作为服务器密钥。
|
||||
|
||||
#### obfs_mode
|
||||
|
||||
==仅版本 5==
|
||||
|
||||
HTTP 混淆模式,`none` `http` 之一。
|
||||
|
||||
默认为 `none`。
|
||||
|
||||
#### mode
|
||||
|
||||
==仅版本 6==
|
||||
|
||||
流量整形模式,`default` `unshaped` `unsafe-raw` 之一。
|
||||
|
||||
默认为 `default`。
|
||||
@@ -31,6 +31,7 @@
|
||||
| `tuic` | [TUIC](./tuic/) |
|
||||
| `hysteria2` | [Hysteria2](./hysteria2/) |
|
||||
| `anytls` | [AnyTLS](./anytls/) |
|
||||
| `snell` | [Snell](./snell/) |
|
||||
| `tor` | [Tor](./tor/) |
|
||||
| `ssh` | [SSH](./ssh/) |
|
||||
| `dns` | [DNS](./dns/) |
|
||||
|
||||
@@ -31,6 +31,7 @@
|
||||
| `tuic` | [TUIC](./tuic/) |
|
||||
| `hysteria2` | [Hysteria2](./hysteria2/) |
|
||||
| `anytls` | [AnyTLS](./anytls/) |
|
||||
| `snell` | [Snell](./snell/) |
|
||||
| `tor` | [Tor](./tor/) |
|
||||
| `ssh` | [SSH](./ssh/) |
|
||||
| `dns` | [DNS](./dns/) |
|
||||
|
||||
@@ -0,0 +1,125 @@
|
||||
---
|
||||
icon: material/new-box
|
||||
---
|
||||
|
||||
!!! question "Since sing-box 1.14.0"
|
||||
|
||||
### Structure
|
||||
|
||||
```json
|
||||
{
|
||||
"type": "snell",
|
||||
"tag": "snell-out",
|
||||
|
||||
"server": "127.0.0.1",
|
||||
"server_port": 1080,
|
||||
"version": 4,
|
||||
"psk": "password",
|
||||
"userkey": "",
|
||||
"reuse": false,
|
||||
"network": "tcp",
|
||||
"obfs_mode": "",
|
||||
"obfs_host": "",
|
||||
|
||||
... // Dial Fields
|
||||
}
|
||||
```
|
||||
|
||||
### Version 6 Structure
|
||||
|
||||
```json
|
||||
{
|
||||
"type": "snell",
|
||||
"tag": "snell-out",
|
||||
|
||||
"server": "127.0.0.1",
|
||||
"server_port": 1080,
|
||||
"version": 6,
|
||||
"psk": "password",
|
||||
"userkey": "",
|
||||
"reuse": false,
|
||||
"network": "tcp",
|
||||
"mode": "",
|
||||
|
||||
... // Dial Fields
|
||||
}
|
||||
```
|
||||
|
||||
### Fields
|
||||
|
||||
#### server
|
||||
|
||||
==Required==
|
||||
|
||||
The server address.
|
||||
|
||||
#### server_port
|
||||
|
||||
==Required==
|
||||
|
||||
The server port.
|
||||
|
||||
#### version
|
||||
|
||||
==Required==
|
||||
|
||||
The Snell protocol version, one of `4` `6`.
|
||||
|
||||
Version `4` supports HTTP obfuscation (`obfs_mode` / `obfs_host`); version `6`
|
||||
replaces it with traffic shaping (`mode`) and requires a `psk` of 12 to 255
|
||||
bytes.
|
||||
|
||||
!!! note
|
||||
|
||||
Since we intentionally do not support the QUIC proxy mode of Snell v5, the v5 wire protocol
|
||||
is effectively identical to v4, so no separate v4 server or v5 client is provided.
|
||||
|
||||
#### psk
|
||||
|
||||
==Required==
|
||||
|
||||
The pre-shared key.
|
||||
|
||||
#### userkey
|
||||
|
||||
The user key, used to authenticate against a multi-user server.
|
||||
|
||||
#### reuse
|
||||
|
||||
Enable connection reuse (the Snell v2 `CONNECT` command).
|
||||
|
||||
#### network
|
||||
|
||||
Enabled network
|
||||
|
||||
One of `tcp` `udp`.
|
||||
|
||||
Both is enabled by default.
|
||||
|
||||
#### obfs_mode
|
||||
|
||||
==Version 4 only==
|
||||
|
||||
HTTP obfuscation mode, one of `none` `http`.
|
||||
|
||||
`none` is used by default.
|
||||
|
||||
#### obfs_host
|
||||
|
||||
==Version 4 only==
|
||||
|
||||
The HTTP `Host` header sent when `obfs_mode` is `http`.
|
||||
|
||||
`bing.com` is used by default.
|
||||
|
||||
#### mode
|
||||
|
||||
==Version 6 only==
|
||||
|
||||
Traffic shaping mode, one of `default` `unshaped` `unsafe-raw`.
|
||||
|
||||
`default` is used by default.
|
||||
|
||||
### Dial Fields
|
||||
|
||||
See [Dial Fields](/configuration/shared/dial/) for details.
|
||||
@@ -0,0 +1,124 @@
|
||||
---
|
||||
icon: material/new-box
|
||||
---
|
||||
|
||||
!!! question "自 sing-box 1.14.0 起"
|
||||
|
||||
### 结构
|
||||
|
||||
```json
|
||||
{
|
||||
"type": "snell",
|
||||
"tag": "snell-out",
|
||||
|
||||
"server": "127.0.0.1",
|
||||
"server_port": 1080,
|
||||
"version": 4,
|
||||
"psk": "password",
|
||||
"userkey": "",
|
||||
"reuse": false,
|
||||
"network": "tcp",
|
||||
"obfs_mode": "",
|
||||
"obfs_host": "",
|
||||
|
||||
... // 拨号字段
|
||||
}
|
||||
```
|
||||
|
||||
### 版本 6 结构
|
||||
|
||||
```json
|
||||
{
|
||||
"type": "snell",
|
||||
"tag": "snell-out",
|
||||
|
||||
"server": "127.0.0.1",
|
||||
"server_port": 1080,
|
||||
"version": 6,
|
||||
"psk": "password",
|
||||
"userkey": "",
|
||||
"reuse": false,
|
||||
"network": "tcp",
|
||||
"mode": "",
|
||||
|
||||
... // 拨号字段
|
||||
}
|
||||
```
|
||||
|
||||
### 字段
|
||||
|
||||
#### server
|
||||
|
||||
==必填==
|
||||
|
||||
服务器地址。
|
||||
|
||||
#### server_port
|
||||
|
||||
==必填==
|
||||
|
||||
服务器端口。
|
||||
|
||||
#### version
|
||||
|
||||
==必填==
|
||||
|
||||
Snell 协议版本,`4` `6` 之一。
|
||||
|
||||
版本 `4` 支持 HTTP 混淆(`obfs_mode` / `obfs_host`);版本 `6` 以流量整形(`mode`)
|
||||
取而代之,并要求 `psk` 长度为 12 到 255 字节。
|
||||
|
||||
!!! note
|
||||
|
||||
由于我们有意不支持 Snell v5 的 QUIC 代理模式,v5 的线路协议实际上与 v4 没有区别,
|
||||
因此不提供独立的 v4 服务器和 v5 客户端。
|
||||
|
||||
#### psk
|
||||
|
||||
==必填==
|
||||
|
||||
预共享密钥。
|
||||
|
||||
#### userkey
|
||||
|
||||
用户密钥,用于向多用户服务器进行认证。
|
||||
|
||||
#### reuse
|
||||
|
||||
启用连接复用(Snell v2 `CONNECT` 命令)。
|
||||
|
||||
#### network
|
||||
|
||||
启用的网络协议。
|
||||
|
||||
`tcp` 或 `udp`。
|
||||
|
||||
默认所有。
|
||||
|
||||
#### obfs_mode
|
||||
|
||||
==仅版本 4==
|
||||
|
||||
HTTP 混淆模式,`none` `http` 之一。
|
||||
|
||||
默认为 `none`。
|
||||
|
||||
#### obfs_host
|
||||
|
||||
==仅版本 4==
|
||||
|
||||
`obfs_mode` 为 `http` 时发送的 HTTP `Host` 头。
|
||||
|
||||
默认为 `bing.com`。
|
||||
|
||||
#### mode
|
||||
|
||||
==仅版本 6==
|
||||
|
||||
流量整形模式,`default` `unshaped` `unsafe-raw` 之一。
|
||||
|
||||
默认为 `default`。
|
||||
|
||||
### 拨号字段
|
||||
|
||||
参阅 [拨号字段](/zh/configuration/shared/dial/)。
|
||||
@@ -41,13 +41,14 @@ require (
|
||||
github.com/sagernet/gomobile v0.1.12
|
||||
github.com/sagernet/gvisor v0.0.0-20250811.0-sing-box-mod.1
|
||||
github.com/sagernet/quic-go v0.59.0-sing-box-mod.4
|
||||
github.com/sagernet/sing v0.8.12-0.20260702074557-63e2fd680d8a
|
||||
github.com/sagernet/sing v0.8.12-0.20260702081104-2ded2af32d3d
|
||||
github.com/sagernet/sing-cloudflared v0.1.2
|
||||
github.com/sagernet/sing-mux v0.3.5
|
||||
github.com/sagernet/sing-quic v0.6.4-0.20260709030527-cff0b763485d
|
||||
github.com/sagernet/sing-shadowsocks v0.2.8
|
||||
github.com/sagernet/sing-shadowsocks2 v0.2.1
|
||||
github.com/sagernet/sing-shadowtls v0.2.1
|
||||
github.com/sagernet/sing-snell v0.0.0-20260705044717-4e9e73be7814
|
||||
github.com/sagernet/sing-tun v0.8.12-0.20260629021427-b3c6babbd353
|
||||
github.com/sagernet/sing-usbip v0.0.0-20260616101517-efb91521eddb
|
||||
github.com/sagernet/sing-vmess v0.2.8-0.20250909125414-3aed155119a1
|
||||
|
||||
@@ -258,8 +258,8 @@ github.com/sagernet/nftables v0.3.0-mod.2 h1:ck2KMU02OxL1eDFgGaWYglMDpoOZ7OHzxje
|
||||
github.com/sagernet/nftables v0.3.0-mod.2/go.mod h1:8kslHG4VvYNihcco+i6uxIX7qbT8A56T0y5q7U44ZaQ=
|
||||
github.com/sagernet/quic-go v0.59.0-sing-box-mod.4 h1:6qvrUW79S+CrPwWz6cMePXohgjHoKxLo3c+MDhNwc3o=
|
||||
github.com/sagernet/quic-go v0.59.0-sing-box-mod.4/go.mod h1:OqILvS182CyOol5zNNo6bguvOGgXzV459+chpRaUC+4=
|
||||
github.com/sagernet/sing v0.8.12-0.20260702074557-63e2fd680d8a h1:I8FFJ8IOS+vUSNIq7L1pzBv0fMI507FEpe2CzmT0Jz0=
|
||||
github.com/sagernet/sing v0.8.12-0.20260702074557-63e2fd680d8a/go.mod h1:olXxWQNqRW/l2Q6JI3b2Qmz8iQnIFlOeeH8bx6JhgUA=
|
||||
github.com/sagernet/sing v0.8.12-0.20260702081104-2ded2af32d3d h1:BhsQU0Iug1tU4xR52cjm8Sc+LBo+KwdyLTRn3ie9moo=
|
||||
github.com/sagernet/sing v0.8.12-0.20260702081104-2ded2af32d3d/go.mod h1:olXxWQNqRW/l2Q6JI3b2Qmz8iQnIFlOeeH8bx6JhgUA=
|
||||
github.com/sagernet/sing-cloudflared v0.1.2 h1:rEz98+q2nvbNlQXaDXJQlRKngK81FHK+rm7pNCSLXB4=
|
||||
github.com/sagernet/sing-cloudflared v0.1.2/go.mod h1:bH2NKX+NpDTY1Zkxfboxw6MXB/ZywaNLmrDJYgKMJ2Y=
|
||||
github.com/sagernet/sing-mux v0.3.5 h1:RHnhVEc+SFqkrK4xMygYjDwwLhzp2Bj3lztSukONfhI=
|
||||
@@ -272,6 +272,8 @@ github.com/sagernet/sing-shadowsocks2 v0.2.1 h1:dWV9OXCeFPuYGHb6IRqlSptVnSzOelnq
|
||||
github.com/sagernet/sing-shadowsocks2 v0.2.1/go.mod h1:RnXS0lExcDAovvDeniJ4IKa2IuChrdipolPYWBv9hWQ=
|
||||
github.com/sagernet/sing-shadowtls v0.2.1 h1:ZiHZdnEnP+YS73NMsxiZmIFCwNd0M4k7PkGCKNXhbaM=
|
||||
github.com/sagernet/sing-shadowtls v0.2.1/go.mod h1:sWqKnGlMipCHaGsw1sTTlimyUpgzP4WP3pjhCsYt9oA=
|
||||
github.com/sagernet/sing-snell v0.0.0-20260705044717-4e9e73be7814 h1:xfnkRpjVRVeJhVvDZA8PzTLlKGTb1o2kdI4uv1YymXo=
|
||||
github.com/sagernet/sing-snell v0.0.0-20260705044717-4e9e73be7814/go.mod h1:PcwzX/Xvqky0EP3kGt8OCjYb3R1pydenPHNQZcPZmXY=
|
||||
github.com/sagernet/sing-tun v0.8.12-0.20260629021427-b3c6babbd353 h1:HA0TGrBQSFfvcoVXL1DxzF+i8pmaGOGb33jdReB7L4s=
|
||||
github.com/sagernet/sing-tun v0.8.12-0.20260629021427-b3c6babbd353/go.mod h1:QvarqUtHfj1ULaRR+6kZOS/OoCE+pYGq67A5tyIy+dQ=
|
||||
github.com/sagernet/sing-usbip v0.0.0-20260616101517-efb91521eddb h1:KEMbfexD4DvrQGYWwx6r+AwH9Veh8z6cnBZmtCS2G+0=
|
||||
|
||||
@@ -29,6 +29,7 @@ import (
|
||||
"github.com/sagernet/sing-box/protocol/redirect"
|
||||
"github.com/sagernet/sing-box/protocol/shadowsocks"
|
||||
"github.com/sagernet/sing-box/protocol/shadowtls"
|
||||
"github.com/sagernet/sing-box/protocol/snell"
|
||||
"github.com/sagernet/sing-box/protocol/socks"
|
||||
"github.com/sagernet/sing-box/protocol/ssh"
|
||||
"github.com/sagernet/sing-box/protocol/tor"
|
||||
@@ -60,6 +61,7 @@ func InboundRegistry() *inbound.Registry {
|
||||
mixed.RegisterInbound(registry)
|
||||
|
||||
shadowsocks.RegisterInbound(registry)
|
||||
snell.RegisterInbound(registry)
|
||||
vmess.RegisterInbound(registry)
|
||||
trojan.RegisterInbound(registry)
|
||||
naive.RegisterInbound(registry)
|
||||
@@ -87,6 +89,7 @@ func OutboundRegistry() *outbound.Registry {
|
||||
socks.RegisterOutbound(registry)
|
||||
http.RegisterOutbound(registry)
|
||||
shadowsocks.RegisterOutbound(registry)
|
||||
snell.RegisterOutbound(registry)
|
||||
vmess.RegisterOutbound(registry)
|
||||
trojan.RegisterOutbound(registry)
|
||||
registerNaiveOutbound(registry)
|
||||
|
||||
@@ -159,6 +159,7 @@ nav:
|
||||
- TUIC: configuration/inbound/tuic.md
|
||||
- Hysteria2: configuration/inbound/hysteria2.md
|
||||
- AnyTLS: configuration/inbound/anytls.md
|
||||
- Snell: configuration/inbound/snell.md
|
||||
- Tun: configuration/inbound/tun.md
|
||||
- Redirect: configuration/inbound/redirect.md
|
||||
- TProxy: configuration/inbound/tproxy.md
|
||||
@@ -180,6 +181,7 @@ nav:
|
||||
- TUIC: configuration/outbound/tuic.md
|
||||
- Hysteria2: configuration/outbound/hysteria2.md
|
||||
- AnyTLS: configuration/outbound/anytls.md
|
||||
- Snell: configuration/outbound/snell.md
|
||||
- Tor: configuration/outbound/tor.md
|
||||
- SSH: configuration/outbound/ssh.md
|
||||
- DNS: configuration/outbound/dns.md
|
||||
|
||||
+118
@@ -0,0 +1,118 @@
|
||||
package option
|
||||
|
||||
import (
|
||||
E "github.com/sagernet/sing/common/exceptions"
|
||||
"github.com/sagernet/sing/common/json"
|
||||
"github.com/sagernet/sing/common/json/badjson"
|
||||
)
|
||||
|
||||
type _SnellInboundOptions struct {
|
||||
ListenOptions
|
||||
Version int `json:"version"`
|
||||
PSK string `json:"psk"`
|
||||
Users []SnellUser `json:"users,omitempty"`
|
||||
ObfsOptions SnellObfsServerOptions `json:"-"`
|
||||
V6Options SnellV6Options `json:"-"`
|
||||
}
|
||||
|
||||
type SnellInboundOptions _SnellInboundOptions
|
||||
|
||||
func (o *SnellInboundOptions) UnmarshalJSON(content []byte) error {
|
||||
err := json.Unmarshal(content, (*_SnellInboundOptions)(o))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
var versionOptions any
|
||||
switch o.Version {
|
||||
case 5:
|
||||
versionOptions = &o.ObfsOptions
|
||||
case 6:
|
||||
versionOptions = &o.V6Options
|
||||
case 0:
|
||||
return E.New("snell: missing version")
|
||||
default:
|
||||
return E.New("snell: unsupported version: ", o.Version)
|
||||
}
|
||||
return badjson.UnmarshallExcluded(content, (*_SnellInboundOptions)(o), versionOptions)
|
||||
}
|
||||
|
||||
func (o SnellInboundOptions) MarshalJSON() ([]byte, error) {
|
||||
var versionOptions any
|
||||
switch o.Version {
|
||||
case 5:
|
||||
versionOptions = o.ObfsOptions
|
||||
case 6:
|
||||
versionOptions = o.V6Options
|
||||
case 0:
|
||||
return nil, E.New("snell: missing version")
|
||||
default:
|
||||
return nil, E.New("snell: unsupported version: ", o.Version)
|
||||
}
|
||||
return badjson.MarshallObjects((_SnellInboundOptions)(o), versionOptions)
|
||||
}
|
||||
|
||||
type _SnellOutboundOptions struct {
|
||||
DialerOptions
|
||||
ServerOptions
|
||||
Version int `json:"version"`
|
||||
PSK string `json:"psk"`
|
||||
UserKey string `json:"userkey,omitempty"`
|
||||
Reuse bool `json:"reuse,omitempty"`
|
||||
Network NetworkList `json:"network,omitempty"`
|
||||
ObfsOptions SnellObfsClientOptions `json:"-"`
|
||||
V6Options SnellV6Options `json:"-"`
|
||||
}
|
||||
|
||||
type SnellOutboundOptions _SnellOutboundOptions
|
||||
|
||||
func (o *SnellOutboundOptions) UnmarshalJSON(content []byte) error {
|
||||
err := json.Unmarshal(content, (*_SnellOutboundOptions)(o))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
var versionOptions any
|
||||
switch o.Version {
|
||||
case 4:
|
||||
versionOptions = &o.ObfsOptions
|
||||
case 6:
|
||||
versionOptions = &o.V6Options
|
||||
case 0:
|
||||
return E.New("snell: missing version")
|
||||
default:
|
||||
return E.New("snell: unsupported version: ", o.Version)
|
||||
}
|
||||
return badjson.UnmarshallExcluded(content, (*_SnellOutboundOptions)(o), versionOptions)
|
||||
}
|
||||
|
||||
func (o SnellOutboundOptions) MarshalJSON() ([]byte, error) {
|
||||
var versionOptions any
|
||||
switch o.Version {
|
||||
case 4:
|
||||
versionOptions = o.ObfsOptions
|
||||
case 6:
|
||||
versionOptions = o.V6Options
|
||||
case 0:
|
||||
return nil, E.New("snell: missing version")
|
||||
default:
|
||||
return nil, E.New("snell: unsupported version: ", o.Version)
|
||||
}
|
||||
return badjson.MarshallObjects((_SnellOutboundOptions)(o), versionOptions)
|
||||
}
|
||||
|
||||
type SnellObfsServerOptions struct {
|
||||
ObfsMode string `json:"obfs_mode,omitempty"`
|
||||
}
|
||||
|
||||
type SnellUser struct {
|
||||
Name string `json:"name,omitempty"`
|
||||
UserKey string `json:"userkey"`
|
||||
}
|
||||
|
||||
type SnellObfsClientOptions struct {
|
||||
ObfsMode string `json:"obfs_mode,omitempty"`
|
||||
ObfsHost string `json:"obfs_host,omitempty"`
|
||||
}
|
||||
|
||||
type SnellV6Options struct {
|
||||
Mode string `json:"mode,omitempty"`
|
||||
}
|
||||
@@ -0,0 +1,209 @@
|
||||
package snell
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net"
|
||||
"os"
|
||||
|
||||
"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"
|
||||
snellprotocol "github.com/sagernet/sing-snell"
|
||||
"github.com/sagernet/sing-snell/snellv5"
|
||||
"github.com/sagernet/sing-snell/snellv6"
|
||||
"github.com/sagernet/sing/common/auth"
|
||||
E "github.com/sagernet/sing/common/exceptions"
|
||||
F "github.com/sagernet/sing/common/format"
|
||||
"github.com/sagernet/sing/common/logger"
|
||||
M "github.com/sagernet/sing/common/metadata"
|
||||
N "github.com/sagernet/sing/common/network"
|
||||
)
|
||||
|
||||
func RegisterInbound(registry *inbound.Registry) {
|
||||
inbound.Register[option.SnellInboundOptions](registry, C.TypeSnell, NewInbound)
|
||||
}
|
||||
|
||||
var _ adapter.TCPInjectableInbound = (*Inbound)(nil)
|
||||
|
||||
type Inbound struct {
|
||||
inbound.Adapter
|
||||
router adapter.ConnectionRouterEx
|
||||
logger logger.ContextLogger
|
||||
listener *listener.Listener
|
||||
service snellprotocol.Service
|
||||
users []option.SnellUser
|
||||
}
|
||||
|
||||
func NewInbound(ctx context.Context, router adapter.Router, logger log.ContextLogger, tag string, options option.SnellInboundOptions) (adapter.Inbound, error) {
|
||||
inbound := &Inbound{
|
||||
Adapter: inbound.NewAdapter(C.TypeSnell, tag),
|
||||
router: uot.NewRouter(router, logger),
|
||||
logger: logger,
|
||||
users: options.Users,
|
||||
}
|
||||
var userList []int
|
||||
var keyList [][]byte
|
||||
if len(options.Users) > 0 {
|
||||
userList = make([]int, len(options.Users))
|
||||
keyList = make([][]byte, len(options.Users))
|
||||
for index, user := range options.Users {
|
||||
userList[index] = index
|
||||
keyList[index] = []byte(user.UserKey)
|
||||
}
|
||||
}
|
||||
var err error
|
||||
switch options.Version {
|
||||
case 5:
|
||||
var obfsMode snellprotocol.ObfsMode
|
||||
obfsMode, err = snellprotocol.ParseObfsMode(options.ObfsOptions.ObfsMode)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
serviceOptions := snellv5.ServiceOptions{
|
||||
PSK: []byte(options.PSK),
|
||||
ObfsMode: obfsMode,
|
||||
Handler: inbound,
|
||||
}
|
||||
if len(options.Users) > 0 {
|
||||
var service *snellv5.MultiService[int]
|
||||
service, err = snellv5.NewMultiService[int](serviceOptions)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
err = service.UpdateUsers(userList, keyList)
|
||||
inbound.service = service
|
||||
} else {
|
||||
inbound.service, err = snellv5.NewService(serviceOptions)
|
||||
}
|
||||
case 6:
|
||||
var mode snellv6.Mode
|
||||
mode, err = snellv6.ParseMode(options.V6Options.Mode)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
serviceOptions := snellv6.ServerOptions{
|
||||
PSK: []byte(options.PSK),
|
||||
Mode: mode,
|
||||
Handler: inbound,
|
||||
}
|
||||
if len(options.Users) > 0 {
|
||||
var service *snellv6.MultiService[int]
|
||||
service, err = snellv6.NewMultiService[int](serviceOptions)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
err = service.UpdateUsers(userList, keyList)
|
||||
inbound.service = service
|
||||
} else {
|
||||
inbound.service, err = snellv6.NewService(serviceOptions)
|
||||
}
|
||||
case 0:
|
||||
return nil, E.New("snell: missing version")
|
||||
default:
|
||||
return nil, E.New("snell: unsupported version: ", options.Version)
|
||||
}
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
inbound.listener = listener.New(listener.Options{
|
||||
Context: ctx,
|
||||
Logger: logger,
|
||||
Network: []string{N.NetworkTCP},
|
||||
Listen: options.ListenOptions,
|
||||
ConnectionHandler: inbound,
|
||||
})
|
||||
return inbound, nil
|
||||
}
|
||||
|
||||
func (h *Inbound) Start(stage adapter.StartStage) error {
|
||||
if stage != adapter.StartStateStart {
|
||||
return nil
|
||||
}
|
||||
return h.listener.Start()
|
||||
}
|
||||
|
||||
func (h *Inbound) Close() error {
|
||||
return h.listener.Close()
|
||||
}
|
||||
|
||||
func (h *Inbound) NewConnection(ctx context.Context, conn net.Conn, metadata adapter.InboundContext, onClose N.CloseHandlerFunc) {
|
||||
err := h.service.NewConnection(adapter.WithContext(ctx, &metadata), conn, metadata.Source, onClose)
|
||||
if err != nil {
|
||||
N.CloseOnHandshakeFailure(conn, onClose, err)
|
||||
if E.IsClosedOrCanceled(err) {
|
||||
h.logger.DebugContext(ctx, "connection closed: ", err)
|
||||
} else {
|
||||
h.logger.ErrorContext(ctx, E.Cause(err, "process connection from ", metadata.Source))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (h *Inbound) NewConnectionEx(ctx context.Context, conn net.Conn, source M.Socksaddr, destination M.Socksaddr, onClose N.CloseHandlerFunc) {
|
||||
_, metadata := adapter.ExtendContext(ctx)
|
||||
if source.IsValid() {
|
||||
metadata.Source = source
|
||||
}
|
||||
if destination.IsValid() {
|
||||
metadata.Destination = destination
|
||||
}
|
||||
h.newConnection(ctx, conn, *metadata, onClose)
|
||||
}
|
||||
|
||||
func (h *Inbound) NewPacketConnectionEx(ctx context.Context, conn N.PacketConn, source M.Socksaddr, destination M.Socksaddr, onClose N.CloseHandlerFunc) {
|
||||
_, metadata := adapter.ExtendContext(ctx)
|
||||
if source.IsValid() {
|
||||
metadata.Source = source
|
||||
}
|
||||
if destination.IsValid() {
|
||||
metadata.Destination = destination
|
||||
}
|
||||
h.newPacketConnection(ctx, conn, *metadata, onClose)
|
||||
}
|
||||
|
||||
func (h *Inbound) newConnection(ctx context.Context, conn net.Conn, metadata adapter.InboundContext, onClose N.CloseHandlerFunc) {
|
||||
metadata.Inbound = h.Tag()
|
||||
metadata.InboundType = h.Type()
|
||||
if len(h.users) > 0 {
|
||||
userIndex, loaded := auth.UserFromContext[int](ctx)
|
||||
if !loaded {
|
||||
N.CloseOnHandshakeFailure(conn, onClose, os.ErrInvalid)
|
||||
return
|
||||
}
|
||||
user := h.users[userIndex].Name
|
||||
if user == "" {
|
||||
user = F.ToString(userIndex)
|
||||
} else {
|
||||
metadata.User = user
|
||||
}
|
||||
h.logger.InfoContext(ctx, "[", user, "] inbound connection to ", metadata.Destination)
|
||||
} else {
|
||||
h.logger.InfoContext(ctx, "inbound connection to ", metadata.Destination)
|
||||
}
|
||||
h.router.RouteConnectionEx(ctx, conn, metadata, onClose)
|
||||
}
|
||||
|
||||
func (h *Inbound) newPacketConnection(ctx context.Context, conn N.PacketConn, metadata adapter.InboundContext, onClose N.CloseHandlerFunc) {
|
||||
metadata.Inbound = h.Tag()
|
||||
metadata.InboundType = h.Type()
|
||||
if len(h.users) > 0 {
|
||||
userIndex, loaded := auth.UserFromContext[int](ctx)
|
||||
if !loaded {
|
||||
N.CloseOnHandshakeFailure(conn, onClose, os.ErrInvalid)
|
||||
return
|
||||
}
|
||||
user := h.users[userIndex].Name
|
||||
if user == "" {
|
||||
user = F.ToString(userIndex)
|
||||
} else {
|
||||
metadata.User = user
|
||||
}
|
||||
h.logger.InfoContext(ctx, "[", user, "] inbound packet connection from ", metadata.Source)
|
||||
} else {
|
||||
h.logger.InfoContext(ctx, "inbound packet connection from ", metadata.Source)
|
||||
}
|
||||
h.router.RoutePacketConnectionEx(ctx, conn, metadata, onClose)
|
||||
}
|
||||
@@ -0,0 +1,141 @@
|
||||
package snell
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net"
|
||||
|
||||
"github.com/sagernet/sing-box/adapter"
|
||||
"github.com/sagernet/sing-box/adapter/outbound"
|
||||
"github.com/sagernet/sing-box/common/dialer"
|
||||
C "github.com/sagernet/sing-box/constant"
|
||||
"github.com/sagernet/sing-box/log"
|
||||
"github.com/sagernet/sing-box/option"
|
||||
snellprotocol "github.com/sagernet/sing-snell"
|
||||
"github.com/sagernet/sing-snell/snellv4"
|
||||
"github.com/sagernet/sing-snell/snellv6"
|
||||
"github.com/sagernet/sing/common/bufio"
|
||||
E "github.com/sagernet/sing/common/exceptions"
|
||||
"github.com/sagernet/sing/common/logger"
|
||||
M "github.com/sagernet/sing/common/metadata"
|
||||
N "github.com/sagernet/sing/common/network"
|
||||
)
|
||||
|
||||
func RegisterOutbound(registry *outbound.Registry) {
|
||||
outbound.Register[option.SnellOutboundOptions](registry, C.TypeSnell, NewOutbound)
|
||||
}
|
||||
|
||||
type Outbound struct {
|
||||
outbound.Adapter
|
||||
logger logger.ContextLogger
|
||||
dialer N.Dialer
|
||||
client snellClient
|
||||
serverAddr M.Socksaddr
|
||||
}
|
||||
|
||||
type snellClient interface {
|
||||
snellprotocol.Method
|
||||
DialContext(ctx context.Context, destination M.Socksaddr) (net.Conn, error)
|
||||
Close() error
|
||||
}
|
||||
|
||||
func NewOutbound(ctx context.Context, router adapter.Router, logger log.ContextLogger, tag string, options option.SnellOutboundOptions) (adapter.Outbound, error) {
|
||||
outboundDialer, err := dialer.New(ctx, options.DialerOptions, options.ServerIsDomain())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
serverAddr := options.ServerOptions.Build()
|
||||
var client snellClient
|
||||
switch options.Version {
|
||||
case 4:
|
||||
var obfsMode snellprotocol.ObfsMode
|
||||
obfsMode, err = snellprotocol.ParseObfsMode(options.ObfsOptions.ObfsMode)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
client, err = snellv4.NewClient(snellv4.ClientOptions{
|
||||
PSK: []byte(options.PSK),
|
||||
UserKey: []byte(options.UserKey),
|
||||
Reuse: options.Reuse,
|
||||
ObfsMode: obfsMode,
|
||||
ObfsHost: options.ObfsOptions.ObfsHost,
|
||||
Dialer: outboundDialer,
|
||||
Server: serverAddr,
|
||||
})
|
||||
case 6:
|
||||
var mode snellv6.Mode
|
||||
mode, err = snellv6.ParseMode(options.V6Options.Mode)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
client, err = snellv6.NewClient(snellv6.ClientOptions{
|
||||
PSK: []byte(options.PSK),
|
||||
UserKey: []byte(options.UserKey),
|
||||
Mode: mode,
|
||||
Reuse: options.Reuse,
|
||||
Dialer: outboundDialer,
|
||||
Server: serverAddr,
|
||||
})
|
||||
case 0:
|
||||
return nil, E.New("snell: missing version")
|
||||
default:
|
||||
return nil, E.New("snell: unsupported version: ", options.Version)
|
||||
}
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
outbound := &Outbound{
|
||||
Adapter: outbound.NewAdapterWithDialerOptions(C.TypeSnell, tag, options.Network.Build(), options.DialerOptions),
|
||||
logger: logger,
|
||||
dialer: outboundDialer,
|
||||
client: client,
|
||||
serverAddr: serverAddr,
|
||||
}
|
||||
return outbound, nil
|
||||
}
|
||||
|
||||
func (h *Outbound) DialContext(ctx context.Context, network string, destination M.Socksaddr) (net.Conn, error) {
|
||||
ctx, metadata := adapter.ExtendContext(ctx)
|
||||
metadata.Outbound = h.Tag()
|
||||
metadata.Destination = destination
|
||||
networkName := N.NetworkName(network)
|
||||
switch networkName {
|
||||
case N.NetworkTCP:
|
||||
h.logger.InfoContext(ctx, "outbound connection to ", destination)
|
||||
return h.client.DialContext(ctx, destination)
|
||||
case N.NetworkUDP:
|
||||
h.logger.InfoContext(ctx, "outbound packet connection to ", destination)
|
||||
conn, err := h.dialer.DialContext(ctx, N.NetworkTCP, h.serverAddr)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
packetConn, err := h.client.DialPacketConn(conn)
|
||||
if err != nil {
|
||||
conn.Close()
|
||||
return nil, err
|
||||
}
|
||||
return bufio.NewBindPacketConn(packetConn, destination), nil
|
||||
default:
|
||||
return nil, E.Extend(N.ErrUnknownNetwork, network)
|
||||
}
|
||||
}
|
||||
|
||||
func (h *Outbound) ListenPacket(ctx context.Context, destination M.Socksaddr) (net.PacketConn, error) {
|
||||
ctx, metadata := adapter.ExtendContext(ctx)
|
||||
metadata.Outbound = h.Tag()
|
||||
metadata.Destination = destination
|
||||
h.logger.InfoContext(ctx, "outbound packet connection to ", destination)
|
||||
conn, err := h.dialer.DialContext(ctx, N.NetworkTCP, h.serverAddr)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
packetConn, err := h.client.DialPacketConn(conn)
|
||||
if err != nil {
|
||||
conn.Close()
|
||||
return nil, err
|
||||
}
|
||||
return packetConn, nil
|
||||
}
|
||||
|
||||
func (h *Outbound) Close() error {
|
||||
return h.client.Close()
|
||||
}
|
||||
Reference in New Issue
Block a user