Add snell protocol

This commit is contained in:
世界
2026-08-30 17:41:42 +08:00
parent d1d19fcca3
commit 14fd94e9d4
16 changed files with 924 additions and 0 deletions
+3
View File
@@ -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:
+1
View File
@@ -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: |
+1
View File
@@ -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: |
+96
View File
@@ -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.
+96
View File
@@ -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`
+1
View File
@@ -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/) |
+1
View File
@@ -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/) |
+125
View File
@@ -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.
+124
View File
@@ -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/)。
+1
View File
@@ -49,6 +49,7 @@ require (
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-20260727093646-7cb813e07b73
github.com/sagernet/sing-tun v0.9.0-beta.4
github.com/sagernet/sing-usbip v0.0.0-20260616101517-efb91521eddb
github.com/sagernet/sing-vmess v0.2.8-0.20250909125414-3aed155119a1
+2
View File
@@ -284,6 +284,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-20260727093646-7cb813e07b73 h1:Iyhoka9XutVbIhkhjn6s2Ez2AEuBG5i4KbkQKqRMsug=
github.com/sagernet/sing-snell v0.0.0-20260727093646-7cb813e07b73/go.mod h1:et8Lws4f5QbOrY65DmjevHGup3mijJkhswkto6cwciM=
github.com/sagernet/sing-tun v0.9.0-beta.4 h1:gIIZU4HevhtTQubZiOdMD0/RnECD6csl2kG6666KYmQ=
github.com/sagernet/sing-tun v0.9.0-beta.4/go.mod h1:3EgPst7agntRO7D6GOsiZ1l9FoqdLeuWmKT5TnWkmf0=
github.com/sagernet/sing-usbip v0.0.0-20260616101517-efb91521eddb h1:KEMbfexD4DvrQGYWwx6r+AwH9Veh8z6cnBZmtCS2G+0=
+3
View File
@@ -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)
+2
View File
@@ -160,6 +160,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
@@ -181,6 +182,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
View File
@@ -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"`
}
+209
View File
@@ -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)
}
+141
View File
@@ -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()
}