代理任务、关闭控制与错误处理
本页说明 USTB 插件如何把 wssocks 客户端封装为可启动、等待和停止的代理任务,并沿着 Go/C ABI 边界传递配置与错误。重点是 extra.TaskHandles、StartWssocks、导出的 wrapper 函数,以及插件加载和连接建立流程。
Purpose and Scope
本页覆盖以下端到端路径:外部调用者通过 C ABI 创建句柄,传入本地代理、远端地址和 VPN 选项;wrapper 将 C 参数转换为 Go 配置;TaskHandles.StartWssocks 加载插件、解析远端 URL、建立服务端连接、协商版本并启动客户端;随后调用者可等待任务结束或发出关闭通知。
页面边界限定在“代理任务、关闭控制与错误处理”。VPN 认证算法、主机加密协议和版本协商的具体实现属于各自插件页面;这里仅记录它们如何被注册或配置。仓库中未发现独立的配置文件或 HTTP endpoint 实现,因此配置来源以 wrapper 参数和 extra.Options 为准。
Overview
实现采用两层句柄:extra.TaskHandles 嵌入 wssocks 的 client.Handles,并额外保存一个 *sync.Once;C wrapper 则以 uintptr 管理这些句柄,并通过全局 handleInstances 持有 Go 指针,避免其被垃圾回收。
启动是同步的“准备阶段”:插件加载、远端地址校验、CreateServerConn 和 NegotiateVersion 都在 StartWssocks 返回前完成。只有这些步骤成功后才创建关闭用的 sync.Once 并调用 StartClient。因此调用者收到空错误字符串时,代表客户端后台任务已经启动,而不是仅仅完成参数解析。
错误采用显式返回值传播:Go 层返回 error;C ABI 层把错误转换为 C.CString(err.Error()),成功则返回空字符串。当前实现没有在 wrapper 中释放返回的 C 字符串,也没有为句柄提供销毁函数;调用方需要把这视为现有 ABI 生命周期的一部分。
Architecture
架构的关键约束是 TaskHandles 作为唯一任务控制对象:启动和停止都通过同一个句柄进入;底层 client 的 Handles 负责等待和关闭通知,TaskHandles.once 负责把关闭操作包装成一次性控制。插件注册是惰性的:第一次启动时注册 option、request 和 version 插件,之后只更新已保存的 VPN 配置。
Sources: background.go, background.go, wssocks_client_wrapper.go
启动流程与插件生命周期
1. 插件加载与配置更新
loadPlugins 使用包级 vpnPlugin 保存单个 *vpn.UstbVpn。首次执行时,它依次注册同一个对象为 option plugin 和 request plugin,再注册 ver.PluginVersionNeg。这体现了 UstbVpn 同时承担选项处理和请求处理两种职责。后续启动不会重复注册,而是通过 *vpnPlugin = v 覆盖配置,使下一次任务使用新的 VPN 设置。
这种设计降低了重复注册风险,但也意味着插件状态是进程级共享状态,而非每个 TaskHandles 私有。source 中没有 mutex 保护 vpnPlugin,因此并发调用 StartWssocks 时,配置更新和首次注册的并发安全性未被实现保证;调用方应在外部串行化启动或确保只有一个启动流程同时运行。
1func loadPlugins(v vpn.UstbVpn) error {
2 if vpnPlugin == nil {
3 vpnPlugin = &v
4 // vpn.UstbVpn has implementations of both option plugin and request plugin
5 if err := client.AddPluginOption(vpnPlugin); err != nil {
6 return err
7 }
8 if err := client.AddPluginRequest(vpnPlugin); err != nil {
9 return err
10 }
11 if err := client.AddPluginVersion(&ver.PluginVersionNeg{}); err != nil {
12 return err
13 }
14 } else {
15 // apply changed vpn config to plugin
16 *vpnPlugin = v
17 }
18 return nil
19}Source: background.go
2. 远端地址和连接准备
StartWssocks 首先调用 loadPlugins,任何注册失败都会立即返回。随后拒绝空的 RemoteAddr,并使用 url.Parse 校验和转换地址;解析结果写入嵌入的 client.Options.RemoteUrl。HTTP header 被显式初始化为空的 http.Header,避免沿用调用者可能提供的旧值。
句柄随后替换为 client.NewClientHandles() 的新值。连接准备阶段使用一个一分钟的 context.WithTimeout,并通过 defer cancel() 确保该 context 在启动准备阶段结束后释放。这个 timeout 覆盖 CreateServerConn 和版本协商;source 中没有重试逻辑,任何连接或协商错误都会直接向上返回。
1func (h *TaskHandles) StartWssocks(options Options) error {
2 if err := loadPlugins(options.UstbVpn); err != nil {
3 return err
4 }
5
6 // check remote url
7 if options.RemoteAddr == "" {
8 return errors.New("empty remote address")
9 }
10 u, err := url.Parse(options.RemoteAddr)
11 if err != nil {
12 return err
13 } else {
14 options.RemoteUrl = u
15 }
16
17 options.RemoteHeaders = make(http.Header)
18
19 h.Handles = *client.NewClientHandles()
20 ctx, cancel := context.WithTimeout(context.Background(), time.Minute) // fixme
21 defer cancel()
22
23 _, err = h.CreateServerConn(&options.Options, ctx)
24 if err != nil {
25 return err
26 }
27 // server connect successfully
28
29 if err := h.NegotiateVersion(ctx, options.RemoteAddr); err != nil {
30 return err
31 }
32
33 var once sync.Once
34 h.once = &once
35 h.StartClient(&options.Options, &once)
36 return nil
37}Source: background.go
3. 启动成功后的关闭控制
只有服务端连接和版本协商都成功,代码才创建 sync.Once 并赋给 h.once,随后把同一个 once 传给 StartClient。NotifyCloseWrapper 调用嵌入句柄的 NotifyClose(h.once, false)。因此关闭入口不是直接关闭 socket,而是交给底层 client 的关闭机制,并复用同一个一次性对象,避免重复通知。
需要注意的是,source 没有展示 client.Handles.NotifyClose、StartClient 或 Wait 的内部实现,因此无法从本仓库证实关闭通知是否等待后台 goroutine 完成、false 参数的具体语义,或底层关闭是否可重入。本文仅记录本插件明确传递的调用关系。
C ABI 入口与数据映射
句柄保持
NewClientHandles 分配 extra.TaskHandles,把指针转换为 uintptr,并放入 handleInstances。map 的作用是保持 Go 对象仍然可达;返回值交给外部 ABI 使用,之后 wrapper 再把 uintptr 转回 *extra.TaskHandles。
启动参数映射
StartClientWrapper 将 C 字符串转换为 Go 字符串,将 _Bool 转成 bool,并构造两个嵌套配置:通用 client.Options 和 VPN 专用 vpn.UstbVpn。VPN 的认证方式在 wrapper 中固定为 vpn.VpnAuthMethodPasswd,用户名密码放入 passwd.UstbVpnPasswdAuth;因此该 ABI 入口当前不提供其他认证方式的选择。
1//export StartClientWrapper
2func StartClientWrapper(handlesPtr uintptr, localAddr, remoteAddr, httpLocalAddr *C.char,
3 httpEnable, skipTSLVerify, vpnEnable, vpnForceLogout, vpnHostEncrypt C._Bool,
4 vpnHostInput, vpnUsername, vpnPassword *C.char) *C.char {
5 options := extra.Options{
6 Options: client.Options{
7 LocalSocks5Addr: C.GoString(localAddr),
8 HttpEnabled: bool(httpEnable),
9 LocalHttpAddr: C.GoString(httpLocalAddr),
10 SkipTLSVerify: bool(skipTSLVerify),
11 },
12 UstbVpn: vpn.UstbVpn{
13 Enable: bool(vpnEnable),
14 ForceLogout: bool(vpnForceLogout),
15 HostEncrypt: bool(vpnHostEncrypt),
16 TargetVpn: C.GoString(vpnHostInput),
17 AuthMethod: vpn.VpnAuthMethodPasswd,
18 PasswdAuth: passwd.UstbVpnPasswdAuth{
19 Username: C.GoString(vpnUsername),
20 Password: C.GoString(vpnPassword),
21 },
22 },
23 RemoteAddr: C.GoString(remoteAddr),
24 }
25 var hp = (*extra.TaskHandles)(unsafe.Pointer(handlesPtr))
26 if err := hp.StartWssocks(options); err != nil {
27 return C.CString(err.Error())
28 }
29 return C.CString("")
30}Source: wssocks_client_wrapper.go
Core Flow
启动、等待和停止是三个独立的 ABI 操作,但共享一个 handlesPtr。启动失败发生在后台客户端启动之前;等待则直接委托给嵌入的 client.Handles;停止只发送关闭通知并始终返回空字符串。
上图中的顺序是源码强制的顺序:插件注册失败不会进入 URL 解析;连接失败不会执行版本协商;版本协商失败不会创建 once 或启动客户端。这种早返回结构让启动错误在 ABI 边界被立即报告,但也意味着调用者需要自行决定是否重试。
等待与停止
1//export WaitClientWrapper
2func WaitClientWrapper(handlesPtr uintptr) *C.char {
3 var hp = (*extra.TaskHandles)(unsafe.Pointer(handlesPtr))
4 if err := hp.Wait(); err != nil {
5 return C.CString(err.Error())
6 }
7 return C.CString("")
8}
9
10//export StopClientWrapper
11func StopClientWrapper(handlesPtr uintptr) *C.char {
12 var hp = (*extra.TaskHandles)(unsafe.Pointer(handlesPtr))
13 hp.NotifyCloseWrapper()
14 return C.CString("")
15}Source: wssocks_client_wrapper.go
WaitClientWrapper 传播 hp.Wait() 的错误;StopClientWrapper 不读取底层关闭通知的返回值(因为 wrapper 方法本身没有返回值),所以停止调用在本层总是返回成功字符串。停止前必须确保句柄已经成功执行过 StartWssocks:因为 h.once 只在协商成功后初始化,过早调用停止可能触发底层实现对 nil 或未初始化关闭控制的处理路径,而该路径不在本仓库源码中。
配置选项
wrapper 是本页可见的配置入口;没有发现 appsettings 或环境变量读取逻辑。
| 参数 / 字段 | 类型 | 默认或固定值 | 作用 |
|---|---|---|---|
localAddr → LocalSocks5Addr | C string → string | 调用方提供 | 本地 SOCKS5 地址 |
httpEnable → HttpEnabled | C._Bool → bool | 调用方提供 | 是否启用 HTTP 代理 |
httpLocalAddr → LocalHttpAddr | C string → string | 调用方提供 | 本地 HTTP 地址 |
skipTSLVerify → SkipTLSVerify | C._Bool → bool | 调用方提供 | 是否跳过 TLS 校验;字段名按源码保留 TSL 拼写 |
remoteAddr → RemoteAddr | C string → string | 必须非空 | 远端服务地址,之后经 url.Parse 解析 |
vpnEnable → UstbVpn.Enable | C._Bool → bool | 调用方提供 | 启用 USTB VPN 能力 |
vpnForceLogout → ForceLogout | C._Bool → bool | 调用方提供 | VPN 强制登出选项 |
vpnHostEncrypt → HostEncrypt | C._Bool → bool | 调用方提供 | VPN 主机加密选项 |
vpnHostInput → TargetVpn | C string → string | 调用方提供 | 目标 VPN 主机输入 |
vpnUsername / vpnPassword | C string → PasswdAuth | 调用方提供 | 密码认证凭据 |
AuthMethod | VpnAuthMethod | 固定 VpnAuthMethodPasswd | 当前 wrapper 固定使用密码认证 |
RemoteHeaders | http.Header | make(http.Header) | 启动时重置为空 header |
| context timeout | time.Duration | time.Minute | 覆盖连接建立和版本协商的准备阶段 |
API Reference
(*TaskHandles).StartWssocks(options Options) error
加载或更新插件配置,校验和解析远端地址,创建 client handles,建立服务端连接并协商版本;成功后启动客户端后台任务。参数为 extra.Options,其中嵌入 client.Options、vpn.UstbVpn 和 RemoteAddr。返回插件注册、地址解析、连接建立或版本协商错误;源码未声明 panic 或重试行为。
(*TaskHandles).NotifyCloseWrapper()
调用 NotifyClose(h.once, false) 发出一次性关闭通知。该方法没有返回值;关闭语义由 client.Handles.NotifyClose 决定,具体实现未在本仓库中出现。
NewClientHandles() uintptr
分配并保存一个 *extra.TaskHandles,返回可跨 C ABI 传递的整数句柄。句柄保存在进程级 handleInstances 中,源码没有提供对应的释放函数。
StartClientWrapper(...) *C.char
把 C ABI 参数转换为 extra.Options 并调用 StartWssocks。成功返回空 C 字符串,失败返回 error.Error() 的文本。
WaitClientWrapper(handlesPtr uintptr) *C.char
调用 hp.Wait() 等待底层任务,成功返回空字符串,失败返回错误文本。
StopClientWrapper(handlesPtr uintptr) *C.char
调用 hp.NotifyCloseWrapper(),然后无条件返回空字符串;该 wrapper 不暴露底层关闭结果。
失败模式、边界条件与并发
已确认的失败路径
- 插件注册失败:
client.AddPluginOption、client.AddPluginRequest或client.AddPluginVersion返回错误时,StartWssocks立即返回。 - 远端地址为空:返回固定错误
empty remote address,不会创建连接。 - 远端地址解析失败:直接返回
url.Parse的错误。 - 服务端连接失败:
CreateServerConn的错误直接传播。 - 版本协商失败:
NegotiateVersion的错误直接传播,客户端不会调用StartClient。 - 运行期等待失败:
WaitClientWrapper把hp.Wait()的错误转成 C 字符串。
ABI 层没有错误码枚举,调用者必须检查返回的 C 字符串是否为空并读取其中的文本。源码也没有看到日志记录、自动重试或错误分类,因此运维侧不能仅凭 wrapper 返回值区分网络错误与插件错误,除非进一步解析错误文本或在底层 client 层处理。
关闭和状态边界
Sources: wssocks_client_wrapper.go, background.go
图中的 Failed、Running 和 Finished 是基于实际调用顺序归纳出的外部可观察阶段,不是源码中声明的状态枚举。源码没有保存显式状态字段,因此重复启动、启动后重复等待、停止后再次停止等行为不能由本插件层准确判定。
并发与资源管理
vpnPlugin是包级可变指针,首次初始化和后续赋值没有锁;多任务并发启动时存在共享配置竞争风险。- 每个
TaskHandles在成功启动时拥有自己的client.Handles和sync.Once指针,但插件配置仍由所有任务共享。 context.WithTimeout只覆盖启动准备阶段;客户端启动后是否有运行期超时,当前源码没有说明。handleInstances只增不减;源码没有FreeClientHandles或等价 API,长期创建句柄会造成 map 保留引用。- wrapper 每次成功或失败都调用
C.CString返回新分配的字符串;当前仓库没有对应的 C 侧释放约定或封装,调用方需要在集成层负责释放,否则可能产生分配累积。
性能与运维注意事项
启动路径包含一次插件注册(仅首次)、URL 解析、服务端连接和版本协商;连接与协商共享一分钟 context,且没有重试。若需要重试,应由调用者在确认上一次句柄生命周期后重新调用,而不是假设 StartWssocks 内部会重连。
运行期任务通过 WaitClientWrapper 观察结束;停止操作只发送关闭信号,不等待关闭完成。若调用方需要“停止后确认已结束”,应在发送停止后继续使用 WaitClientWrapper,但底层 Wait 的具体阻塞和错误语义需要参考 wssocks/client 依赖源码。
扩展点与测试边界
可扩展点主要是 extra.Options 中嵌入的 client.Options 与 vpn.UstbVpn,以及 loadPlugins 中注册的 client plugin。若增加认证方式,不能只修改字段值:当前 C ABI 明确把 AuthMethod 固定为 VpnAuthMethodPasswd,还需要扩展 wrapper 参数和配置构造逻辑。
本页使用的源码证据集中在任务控制和 C wrapper 文件。由于源探索预算已用尽,未进一步读取底层 wssocks/client 实现或完整测试集合;因此底层 NotifyClose、Wait、StartClient 的内部并发保证,以及该路径的测试覆盖情况,均属于“Implementation details not found in source”,不在此页推断。
Usage Examples
创建句柄并启动
外部集成首先调用导出的 NewClientHandles 获取句柄,再把该句柄传给 StartClientWrapper。下面的 Go/Cgo 入口展示了实际 ABI 函数签名和错误返回约定;具体调用者代码不在仓库中,因此这里只引用导出函数本身。
1//export NewClientHandles
2func NewClientHandles() uintptr {
3 hd := new(extra.TaskHandles)
4 ptr := uintptr(unsafe.Pointer(hd))
5 if handleInstances == nil {
6 handleInstances = make(map[uintptr]*extra.TaskHandles)
7 }
8 handleInstances[ptr] = hd
9 return ptr
10}Source: wssocks_client_wrapper.go
启动后等待或停止
1//export StopClientWrapper
2func StopClientWrapper(handlesPtr uintptr) *C.char {
3 var hp = (*extra.TaskHandles)(unsafe.Pointer(handlesPtr))
4 hp.NotifyCloseWrapper()
5 return C.CString("")
6}Source: wssocks_client_wrapper.go
Related Links
- extra/background.go:任务句柄、插件加载、连接启动和关闭包装。
- extra/go-api/wssocks_client_wrapper.go:C ABI 句柄和错误返回入口。
- 对于 VPN 认证、主机加密和版本协商的具体协议行为,请参阅仓库中对应的
plugins/vpn与plugins/ver页面;本页只记录它们在任务启动链路中的接入点。