Repository Wiki
genshen/wssocks-plugin-ustb

代理任务、关闭控制与错误处理

本页说明 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

Loading diagram...

架构的关键约束是 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 时,配置更新和首次注册的并发安全性未被实现保证;调用方应在外部串行化启动或确保只有一个启动流程同时运行。

go
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 中没有重试逻辑,任何连接或协商错误都会直接向上返回。

go
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 入口当前不提供其他认证方式的选择。

go
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;停止只发送关闭通知并始终返回空字符串。

Loading diagram...

上图中的顺序是源码强制的顺序:插件注册失败不会进入 URL 解析;连接失败不会执行版本协商;版本协商失败不会创建 once 或启动客户端。这种早返回结构让启动错误在 ABI 边界被立即报告,但也意味着调用者需要自行决定是否重试。

等待与停止

go
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 → LocalSocks5AddrC string → string调用方提供本地 SOCKS5 地址
httpEnable → HttpEnabledC._Bool → bool调用方提供是否启用 HTTP 代理
httpLocalAddr → LocalHttpAddrC string → string调用方提供本地 HTTP 地址
skipTSLVerify → SkipTLSVerifyC._Bool → bool调用方提供是否跳过 TLS 校验;字段名按源码保留 TSL 拼写
remoteAddr → RemoteAddrC string → string必须非空远端服务地址,之后经 url.Parse 解析
vpnEnable → UstbVpn.EnableC._Bool → bool调用方提供启用 USTB VPN 能力
vpnForceLogout → ForceLogoutC._Bool → bool调用方提供VPN 强制登出选项
vpnHostEncrypt → HostEncryptC._Bool → bool调用方提供VPN 主机加密选项
vpnHostInput → TargetVpnC string → string调用方提供目标 VPN 主机输入
vpnUsername / vpnPasswordC string → PasswdAuth调用方提供密码认证凭据
AuthMethodVpnAuthMethod固定 VpnAuthMethodPasswd当前 wrapper 固定使用密码认证
RemoteHeadershttp.Headermake(http.Header)启动时重置为空 header
context timeouttime.Durationtime.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 不暴露底层关闭结果。

失败模式、边界条件与并发

已确认的失败路径

  1. 插件注册失败:client.AddPluginOption、client.AddPluginRequest 或 client.AddPluginVersion 返回错误时,StartWssocks 立即返回。
  2. 远端地址为空:返回固定错误 empty remote address,不会创建连接。
  3. 远端地址解析失败:直接返回 url.Parse 的错误。
  4. 服务端连接失败:CreateServerConn 的错误直接传播。
  5. 版本协商失败:NegotiateVersion 的错误直接传播,客户端不会调用 StartClient。
  6. 运行期等待失败:WaitClientWrapper 把 hp.Wait() 的错误转成 C 字符串。

ABI 层没有错误码枚举,调用者必须检查返回的 C 字符串是否为空并读取其中的文本。源码也没有看到日志记录、自动重试或错误分类,因此运维侧不能仅凭 wrapper 返回值区分网络错误与插件错误,除非进一步解析错误文本或在底层 client 层处理。

关闭和状态边界

Loading diagram...

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 函数签名和错误返回约定;具体调用者代码不在仓库中,因此这里只引用导出函数本身。

go
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

启动后等待或停止

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

  • extra/background.go:任务句柄、插件加载、连接启动和关闭包装。
  • extra/go-api/wssocks_client_wrapper.go:C ABI 句柄和错误返回入口。
  • 对于 VPN 认证、主机加密和版本协商的具体协议行为,请参阅仓库中对应的 plugins/vpn 与 plugins/ver 页面;本页只记录它们在任务启动链路中的接入点。

Sources

(2 files)