为sing-box添加ssr协议
Easul Lv6

相关思路

sing-box 是通过读取配置文件后,将相应参数传递到代码中然后与服务端建立连接。
故可尝试

  • 创建好outbound
  • 相关参数配置指定好
  • 协议名设置好
  • 然后看哪个地方调用outbound,设置好调用

那么就应该可以实现了。

相关工具

  • sing-boxdev-next 分支下的 commit ID8c1b03df4ffa70e49e4b168218d254b232f5be21 的代码。
  • 含有 ssr 协议的 sing-box 代码使用的是 v1.5.5 版本。
  • clash 库使用了 BackupTime 备份的 v1.17.0 版本
  • chatgpt
  • deepin系统

前置准备工作

BASH
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
# 创建操作目录
mkdir -p ~/Desktop/sing-box-with-ssr
cd ~/Desktop/sing-box-with-ssr

# 克隆 sing-box 的仓库
git clone https://github.com/sagernet/sing-box
# 切换当前分支到 8c1b03df4ffa70e49e4b168218d254b232f5be21 的版本
# 如果切换到该版本时,安装相关依赖出问题,用最新的版本应该问题也不大。
# sing-box 的接口规范很标准,基本不用担心什么
cd sing-box
git checkout -b temp-dev 8c1b03df4ffa70e49e4b168218d254b232f5be21
# 安装相关依赖
# 如果安装依赖的时候比较慢,可以设置一下命令行的代理
# 或者使用如下命令来设置国内的代理网址
# export GOPROXY=https://goproxy.cn
go mod tidy

# 下载 clash 核心依赖
cd ..
wget https://github.com/BackupTime/clash/archive/refs/tags/v1.17.0.tar.gz
tar -zxvf v1.17.0.tar.gz
mv clash-1.17.0 clash
rm -rf v1.17.0.tar.gz
# 安装相关依赖
cd clash
go mod tidy
# 如果中间下载了很多没用的依赖想要清除掉,可以使用如下的命令
go clean -modcache

# 下载 sing-box v1.5.5 的版本
cd ..
wget https://github.com/SagerNet/sing-box/archive/refs/tags/v1.5.5.tar.gz
tar -zxvf v1.5.5.tar.gz
rm -rf v1.5.5.tar.gz

将必要代码放到sing-box

go.mod

BASH
1
2
3
4
5
6
7
8
9
10
11
12
require	github.com/Dreamacro/clash v1.17.0

replace github.com/Dreamacro/clash => ../clash

# 构建时,可能会提示如下错误,
#
# error while importing github.com/Dreamacro/clash/transport/socks5: missing go.sum entry # for module providing package github.com/Dreamacro/protobytes (imported by github.com/# Dreamacro/clash/transport/socks5); to add:
# go get github.com/Dreamacro/clash/transport/socks5@v1.17.0
#
# 故需要在 go.mod 中添加如下 require 再构建
#
require github.com/Dreamacro/protobytes v0.0.0-20230617041236-6500a9f4f158 // indirect

golang代码

v1.5.5 中如下文件夹中的所有文件及目录复制到 sing-boxtransport 目录下。

1
2
transport/clashssr/obfs
transport/clashssr/protocol

然后在 sing-boxprotocol 目录下创建 shadowsocksr/outbound.go,填写如下内容

GOLANG
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
package shadowsocksr

// 相关 v1.5.5 需要的包也倒了过来
import (
"context"
"errors"
"fmt"
"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"
"github.com/sagernet/sing-box/transport/clashssr/obfs"
"github.com/sagernet/sing-box/transport/clashssr/protocol"
"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"

"github.com/Dreamacro/clash/transport/shadowsocks/core"
"github.com/Dreamacro/clash/transport/shadowsocks/shadowstream"
"github.com/Dreamacro/clash/transport/socks5"
)

// 这里也参考了 shadowsocks 下的 outbound
func RegisterOutbound(registry *outbound.Registry) {
outbound.Register[option.ShadowsocksROutboundOptions](registry, C.TypeShadowsocksR, NewOutbound)
}

// Outbound 的结构体参考了 v1.5.5 的 ShadowsocksR 的结构体
type Outbound struct {
outbound.Adapter
logger logger.ContextLogger
dialer N.Dialer
serverAddr M.Socksaddr
cipher core.Cipher
obfs obfs.Obfs
protocol protocol.Protocol
}

// 这里将 v1.5.5 同函数的代码复制了过来。
// 同时对已经变更的方法名做了下替换
// Outbound 对象及其他不同的部分参考了 shadowsocks 下的 outbound
func NewOutbound(ctx context.Context, router adapter.Router, logger log.ContextLogger, tag string, options option.ShadowsocksROutboundOptions) (adapter.Outbound, error) {
outboundDialer, err := dialer.New(ctx, options.DialerOptions, options.ServerIsDomain())
if err != nil {
return nil, err
}
outbound := &Outbound{
logger: logger,
Adapter: outbound.NewAdapterWithDialerOptions(C.TypeShadowsocksR, tag, options.Network.Build(), options.DialerOptions),
dialer: outboundDialer,
serverAddr: options.ServerOptions.Build(),
}
var cipher string
switch options.Method {
case "none":
cipher = "dummy"
default:
cipher = options.Method
}
outbound.cipher, err = core.PickCipher(cipher, nil, options.Password)
if err != nil {
return nil, err
}
var (
ivSize int
key []byte
)
if cipher == "dummy" {
ivSize = 0
key = core.Kdf(options.Password, 16)
} else {
streamCipher, ok := outbound.cipher.(*core.StreamCipher)
if !ok {
return nil, fmt.Errorf("%s is not none or a supported stream cipher in ssr", cipher)
}
ivSize = streamCipher.IVSize()
key = streamCipher.Key
}
obfs, obfsOverhead, err := obfs.PickObfs(options.Obfs, &obfs.Base{
Host: options.Server,
Port: int(options.ServerPort),
Key: key,
IVSize: ivSize,
Param: options.ObfsParam,
})
if err != nil {
return nil, E.Cause(err, "initialize obfs")
}
protocol, err := protocol.PickProtocol(options.Protocol, &protocol.Base{
Key: key,
Overhead: obfsOverhead,
Param: options.ProtocolParam,
})
if err != nil {
return nil, E.Cause(err, "initialize protocol")
}
outbound.obfs = obfs
outbound.protocol = protocol

return outbound, nil
}

// 这里将 v1.5.5 同函数的代码复制了过来。
// 同时对已经变更的方法名做了下替换
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
switch network {
case N.NetworkTCP:
h.logger.InfoContext(ctx, "outbound connection to ", destination)
conn, err := h.dialer.DialContext(ctx, network, h.serverAddr)
if err != nil {
return nil, err
}
conn = h.cipher.StreamConn(h.obfs.StreamConn(conn))
writeIv, err := conn.(*shadowstream.Conn).ObtainWriteIV()
if err != nil {
conn.Close()
return nil, err
}
conn = h.protocol.StreamConn(conn, writeIv)
err = M.SocksaddrSerializer.WriteAddrPort(conn, destination)
if err != nil {
conn.Close()
return nil, E.Cause(err, "write request")
}
return conn, nil
case N.NetworkUDP:
conn, err := h.ListenPacket(ctx, destination)
if err != nil {
return nil, err
}
return bufio.NewBindPacketConn(conn, destination), nil
default:
return nil, E.Extend(N.ErrUnknownNetwork, network)
}
}

// 这里将 v1.5.5 同函数的代码复制了过来。
// 同时对已经变更的方法名做了下替换
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)
outConn, err := h.dialer.DialContext(ctx, N.NetworkUDP, h.serverAddr)
if err != nil {
return nil, err
}
packetConn := h.cipher.PacketConn(bufio.NewUnbindPacketConn(outConn))
packetConn = h.protocol.PacketConn(packetConn)
packetConn = &ssPacketConn{packetConn, outConn.RemoteAddr()}
return packetConn, nil
}

// 这个是新版的接口,可以不用填写代码
func (h *Outbound) InterfaceUpdated() {
return
}

// 这个是新版的接口,可以不用填写代码
func (h *Outbound) Close() error {
return nil
}

// ssPacketConn 为 v1.5.5 中的原本代码
type ssPacketConn struct {
net.PacketConn
rAddr net.Addr
}

func (spc *ssPacketConn) WriteTo(b []byte, addr net.Addr) (n int, err error) {
packet, err := socks5.EncodeUDPPacket(socks5.ParseAddrToSocksAddr(addr), b)
if err != nil {
return
}
return spc.PacketConn.WriteTo(packet[3:], spc.rAddr)
}

func (spc *ssPacketConn) ReadFrom(b []byte) (int, net.Addr, error) {
n, _, e := spc.PacketConn.ReadFrom(b)
if e != nil {
return 0, nil, e
}

addr := socks5.SplitAddr(b[:n])
if addr == nil {
return 0, nil, errors.New("parse addr error")
}

udpAddr := addr.UDPAddr()
if udpAddr == nil {
return 0, nil, errors.New("parse addr error")
}

copy(b, b[len(addr):])
return n - len(addr), udpAddr, e
}

然后在 include/registry.go 中导入刚刚的 ssr 的包

GOLANG
1
import "github.com/sagernet/sing-box/protocol/shadowsocksr"

OutboundRegistry 中注册 ssr 协议

GOLANG
1
shadowsocksr.RegisterOutbound(registry)

同时注释移除的 Outbound 调用

GOLANG
1
// registerStubForRemovedOutbounds(registry)

sing-box构建

Linux版本

BASH
1
2
3
4
5
6
7
8
9
10
11
12
# 打包的时候可能会因为某个依赖还没有本地缓存或需要校验和而特别缓慢,例如如下入职
#
# cd /home/easul/workspace/go-project/pkg/mod/cache/vcs/f7f6bc889a01f0647ec10a8079625d04ae3d8338e033cabfa8048ec385e1443a; git ls-remote -q origin
#
# 可以加如下标识来查看构建时的详细信息,去查看哪里卡住了
# -v -x
#
# 或者直接设置一下命令行的代理
# 或者使用如下命令来设置国内的代理网址
# export GOPROXY=https://goproxy.cn
cd ~/Desktop/sing-box-with-ssr/sing-box
go build -tags "with_gvisor with_quic with_dhcp with_wireguard with_utls with_reality_server with_acme with_clash_api with_ech" ./cmd/sing-box

安卓版本

这里构建的是 Android ARMv7 版本的二进制包,所以需要开启 CGO 以及设置相应的构建环境信息。
同时设置 NDK 交叉编译器的信息,我这里是给安卓10使用的,所以 Android API 选择的是 29

BASH
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# 下载ndk的开发包
mkdir -p ~/software/android-ndk
cd ~/software/android-ndk
wget "https://dl.google.com/android/repository/android-ndk-r27c-linux.zip?hl=zh-cn" -O "android-ndk-r27c-linux.zip"
unzip android-ndk-r27c-linux.zip
rm -rf android-ndk-r27c-linux.zip
# 设置环境信息
export CGO_ENABLED=1
export GOOS=android
export GOARCH=arm
export GOARM=7
export CC=/home/easul/software/android-ndk/android-ndk-r27c/toolchains/llvm/prebuilt/linux-x86_64/bin/armv7a-linux-androideabi29-clang
export CXX=/home/easul/software/android-ndk/android-ndk-r27c/toolchains/llvm/prebuilt/linux-x86_64/bin/armv7a-linux-androideabi29-clang++
# 进行构建,默认会将 ssr 的代码一起打包到最后的二进制包中
cd ~/Desktop/sing-box-with-ssr/sing-box
go build -tags "with_gvisor with_quic with_dhcp with_wireguard with_utls with_reality_server with_acme with_clash_api with_ech" ./cmd/sing-box

sing-box相关学习建议

 评论