WinSock2 LSP AppId_Catalog 小分析(一):代理软件和 docker 的冲突

winsock / lsp / docker / proxy / wsl

我最近遇到了一个很严重的问题,docker 打不开了!经过一段分析,这问题还挺有意思的, 而且很久我们没有开资源分析的坑了,顺带回忆一下。

导航

本篇已完结,列出目录方便查阅

  1. 代理软件和 docker 的冲突
  2. NoLSP.exe 主要做了什么
  3. WSCSetApplicationCategory 如何操作注册表

本文是第一部分

问题

相信很多爱折腾又爱干净的用户会用 docker 来管理各种环境,表面上我的系统很干净, 只装了一个 docker,而通过 docker 我配置了很多运行不同应用的系统, 这符合计算机科学中一个中间层解决所有问题的原则,我也是这样, 不过最近我想用 docker 玩一下 stable diffusion 时, docker desktop 连开机都开不出来了:

docker error

分析

如果只是单纯按照关键字搜索,或者按 docker desktop 给出的建议, 我们会搜到各种五花八门的解决方案,多数是重开重启重装的三连招,也有些其他的花样, 都有人声称这样解决了这个问题,看来这也是一个口袋报错: 很多原因都可能导致这个问题。

经过多次经验判断和手工试错,我找到了一个解决方案,运行以下命令:

sudo netsh winsock reset

然后它会提示我们重启,但是其实不重启,docker desktop 也可以打开了, 所以这是一个网络问题。我开始闭眼回忆, 安装 docker 之后我都做了些什么与网络相关的事情。于是一个嫌疑人浮出水面:网易 uu。

网易 uu

所以我们可以优化一下我们的搜索关键字:uu netsh docker, 很快我们就找到了组织: 很难想象,2019 年 wsl 就一直有这个问题,只要我们同时使用了 wsl 和 uu / proxifier 之类的代理软件,wsl 就会无法启动,看上去学习和游戏我只能选一个。

左右为难的软件工程师

当然 wsl 的开发者认为,这不是 wsl 的问题, 而是代理软件没有正确处理虚拟机的网络, proxifier 的开发者分析了这个原因:

Apparently, wsl.exe displays this error if Winsock LSP DLL gets loaded into its process.

The easiest solution is to use WSCSetApplicationCategory WinAPI call for wsl.exe to prevent this.

Under the hood the call creates an entry for wsl.exe at HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Services\WinSock2\Parameters\AppId_Catalog

This tells Windows not to load LSP DLLs into wsl.exe process.

简单说就是在装了代理软件的前提下,如果启动 wsl,winsock 的 LSP 动态库就会加载, 然后 wsl 就会报错,所以要阻止启动 wsl 时加载 LSP 动态库。 而阻止方法就是为 wsl 增加上文提到的注册表配置。

但是如果 wsl 是从 windows store 安装的,那么这个事情会更复杂一些, 由于 WinSock 会给每一个程序生成一个 AppId_Catalog,这个值是根据程序的路径变化的, 而每次商店更新 wsl 后,wsl 的路径都会发生变化,这时我们就要重新添加注册表配置。 proxifier 的开发者也给出了一个小程序 NoLSP.exe 来自动添加注册表配置, 每次 wsl 更新后,运行一下这个程序就可以添加新的注册表配置。

appid catalog sample

解决

其实到这里这个问题差不多可以结束了,但是我想多聊一点的是, 盲目以管理员权限运行一个程序是非常不安全的,除非我们十分确定这个程序会干什么, 比如,如果我在这里放一个做了手脚的 NoLSP.exe, 来到这里且急于解决问题的朋友可能非常容易就中招了。

如果你仔细阅读了上面提到的 issue, 已经有人提供了脚本的范例。 这个方法我是比较推荐的,因为我们很清楚, 这个脚本只是调用了 WinSock 的一个函数而已:

Requires -RunAsAdministrator
# Fix for https://github.com/microsoft/WSL/issues/4177

$MethodDefinition = @'
[DllImport("ws2_32.dll", CharSet = CharSet.Unicode)]
public static extern int WSCSetApplicationCategory([MarshalAs(UnmanagedType.LPWStr)] string Path, uint PathLength, [MarshalAs(UnmanagedType.LPWStr)] string Extra, uint ExtraLength, uint PermittedLspCategories, out uint pPrevPermLspCat, out int lpErrno);
'@

$Ws2Spi = Add-Type -MemberDefinition $MethodDefinition -Name 'Ws2Spi' -PassThru

$WslLocation = Get-AppxPackage MicrosoftCorporationII.WindowsSubsystemForLinux | Select-Object -expand InstallLocation

$Executables = ("wsl.exe", "wslservice.exe");

foreach ($Exe in $Executables) {
    $ExePath = "${WslLocation}\${Exe}";
    $ExePathLength = $ExePath.Length;

    $PrevCat = $null;
    $ErrNo = $null;
    if ($Ws2Spi::WSCSetApplicationCategory($ExePath, $ExePathLength, $null, 0, [uint32]"0x80000000", [ref] $PrevCat, [ref] $ErrNo) -eq 0) {
        Write-Output "Added $ExePath!";
    }
}

如果您只是想解决 wsl 和代理软件的兼容问题,那么看到这里基本就可以结束了, 终于我们不用像小孩子一样做选择了。

我全都要

到这里其实这个问题已经很清楚了:一般情况下,代理软件为了用户方便操作, 默认会接管所有网络,采用的技术是 LSP,而这个技术已经被官方弃用了, 而且 wsl 为了管理子系统,也需要接管子系统的网络,同时也不兼容 LSP, 代理软件要求加载 LSP,而 wsl 不兼容,所以两者产生了冲突, 而解决这个问题的方法是写入注册表,告诉 WinSock,这是一个系统程序,无需加载 LSP。

不过这时我有了一个新的问题,NoLSP.exe 到底是如何操作注册表, 尤其是确定这个键值的呢,我们下次再聊吧。