9
7
2022
11

让离线软件真正离线

本文来自依云's Blog,转载请注明。

去年我做了个索引 Telegram 群组的软件——落絮,终于可以搜索到群里的中文消息了。然而后来发现,好多消息群友都是通过截图发送的,落絮就索引不到了。也不能不让人截图嘛,毕竟很多人描述能力有限,甚至让复制粘贴都能粘出错,截图就相对客观真实可靠多了。

所以落絮想要 OCR。我知道百度有 OCR 服务,但是我显然不会在落絮上使用。我平常使用的 OCR 工具是 tesseract,不少开源软件也用的它。它对英文的识别能力还可以,尤其是可自定义字符集所以识别 IP 地址的效果非常好,但是对中文的识别能力不怎么样,图片稍有不清晰(比如被 Telegram JPEG 压缩)、变形(比如拍照),它就乱得一塌糊涂,就不说它给汉字之间加空格是啥奇怪行为了。

后来听群友说 PaddleOCR 的中文识别效果非常好。我实际测试了一下,确实相当不错,而且完全离线工作还开源。但是,开源是开源了,我又没能力审查它所有的代码,用户量太小也不能指望「有足够多的眼睛」。作为基于机器学习的软件,它也继承了该领域十分复杂难解的构建过程,甚至依赖了个叫「opencv-contrib-python」的自带了 ffmpeg、Qt5、OpenSSL、XCB 各种库的、不知道干什么的组件,试图编译某个旧版 numpy 结果由于太旧不支持 Python 3.10 而失败。所以我决定在 Debian chroot 里安装,那边有 Python 3.9 可以直接使用预编译包。所以问题来了:这么一大堆来源不明的二进制库,用起来真的安全吗?

我不知道。但是我知道,如果它联不上网的话,那还是相对安全的。毕竟我最关心的就是隐私安全——一定不能把群友发的图片泄漏给未知的第三方。而且联不上网的话,不管你是要 DDoS 别人、还是想挖矿,收不到指令、传不出数据,都行不通了嘛。我只要它能从外界读取图片,然后把识别的结果返回给我就好了。

于是一个简单的办法是,拿 bwrap 给它个只能访问自己的独立网络空间它不就访问不了互联网了吗?不过说起来简单,做起来还真不容易。首先,debootstrap 需要使用 root 执行,执行完之后再 chown。为了进一步限制权限,我使用了 subuid,但这也使得事情复杂了起来——我自己都难以访问到它了。几经摸索,我找到了让我进入这个 chroot 环境的方法:

#!/bin/bash -e

user="$(id -un)"
group="$(id -gn)"

# Create a new user namespace in the background with a dummy process just to
# keep it alive.
unshare -U sh -c "sleep 30" &
child_pid=$!

# Set {uid,gid}_map in new user namespace to max allowed range.
# Need to have appropriate entries for user in /etc/subuid and /etc/subgid.
# shellcheck disable=SC2046
newuidmap $child_pid 0 $(grep "^${user}:" /etc/subuid | cut -d : -f 2- | tr : ' ')
# shellcheck disable=SC2046
newgidmap $child_pid 0 $(grep "^${group}:" /etc/subgid | cut -d : -f 2- | tr : ' ')

# Tell Bubblewrap to use our user namespace through fd 5.
5< /proc/$child_pid/ns/user bwrap \
  --userns 5 \
  --cap-add ALL \
  --uid 0 \
  --gid 0 \
  --unshare-ipc --unshare-pid --unshare-uts --unshare-cgroup --share-net \
  --die-with-parent --bind ~/rootfs-debian / --tmpfs /sys --tmpfs /tmp --tmpfs /run --proc /proc --dev /dev \
  -- \
  /bin/bash -l

这里给了联网权限,是因为我需要安装 PaddleOCR。没有在创建好 chroot 之后、chown 之前安装,是因为我觉得拿着虽然在 chroot 里但依旧真实的 root 权限装不信任的软件实在是风险太大了。装好之后,再随便找个图,每种语言都识别一遍,让它下载好各种语言的模型,接下来它就再也上不了网啦(为避免恶意代码储存数据在有网的时候再发送):

#!/bin/bash -e

dir="$(dirname $2)"
file="$(basename $2)"

user="$(id -un)"
group="$(id -gn)"

# Create a new user namespace in the background with a dummy process just to
# keep it alive.
unshare -U sh -c "sleep 30" &
child_pid=$!

# Set {uid,gid}_map in new user namespace to max allowed range.
# Need to have appropriate entries for user in /etc/subuid and /etc/subgid.
# shellcheck disable=SC2046
newuidmap $child_pid 0 $(grep "^${user}:" /etc/subuid | cut -d : -f 2- | tr : ' ')
# shellcheck disable=SC2046
newgidmap $child_pid 0 $(grep "^${group}:" /etc/subgid | cut -d : -f 2- | tr : ' ')

# Tell Bubblewrap to use our user namespace through fd 5.
5< /proc/$child_pid/ns/user bwrap \
  --userns 5 \
  --uid 1000 \
  --gid 1000 \
  --unshare-ipc --unshare-pid --unshare-uts --unshare-cgroup --unshare-net \
  --die-with-parent --bind ~/rootfs-debian / --tmpfs /sys --tmpfs /tmp --tmpfs /run --proc /proc --dev /dev \
  --ro-bind "$dir" /workspace --chdir /workspace \
  --setenv PATH /usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin \
  --setenv HOME /home/worker \
  -- \
  /home/worker/paddleocr/ocr.py "$1" "$file"

kill $child_pid

这个脚本会把指定文件所在的目录挂载到 chroot 内部,然后对着这个文件调用 PaddleOCR 来识别并通过返回结果。这个调用 PaddleOCR 的 ocr.py 脚本位于我的 paddleocr-web 项目

不过这也太复杂了。后来我又使用 systemd 做了个服务,简单多了:

[Unit]
Description=PaddleOCR HTTP service

[Service]
Type=exec
RootDirectory=/var/lib/machines/lxc-debian/
ExecStart=/home/lilydjwg/PaddleOCR/paddleocr-http --loglevel=warn -j 2
Restart=on-failure
RestartSec=5s

User=1000
NoNewPrivileges=true
PrivateTmp=true
CapabilityBoundingSet=
IPAddressAllow=localhost
IPAddressDeny=any
SocketBindAllow=tcp:端口号
SocketBindDeny=any
SystemCallArchitectures=native
SystemCallFilter=~connect

[Install]
WantedBy=multi-user.target

这里的「paddleocr-http」脚本就是 paddleocr-web 里那个「server.py」。

但它的防护力也差了一些。首先这里只限制了它只能访问本地网络,TCP 方面只允许它绑定指定的端口、不允许调用 connect 系统调用,但是它依旧能向本地发送 UDP 包。其次运行这个进程的用户就是我自己的用户,虽然被 chroot 到了容器里应该出不来。嗯,我大概应该给它换个用户,比如 uid 1500,应该能起到跟 subuid 差不多的效果。

顺便提一句,这个 PaddleOCR 说的是支持那么多种语言,但实际上只有简体中文等少数语言支持得好(繁体都不怎么样),别的语言甚至连语言名和缩写都弄错,越南语识别出来附加符号几乎全军覆没。

Category: Linux | Tags: linux 安全 隐私 网络 | Read Count: 2398
c 说:
Sep 08, 2022 12:47:06 PM

podman 好像有个特性比较适合用在这种场景:
https://www.redhat.com/sysadmin/socket-activation-podman
https://www.redhat.com/sysadmin/podman-systemd-limit-access

Avatar_small
依云 说:
Sep 09, 2022 09:36:48 AM

这个就是用 systemd 的 RestrictAddressFamilies 呀。这个限制太弱了,因为大家都用 IPv4 / IPv6。

Vifly 说:
Sep 09, 2022 07:41:53 PM

依云考虑过 [chineseocr_lite](https://github.com/DayBreak-u/chineseocr_lite) 吗,这个的依赖比 PaddleOCR 靠谱(至少能在 Python 3.10 工作),而且速度比 PaddleOCR 快

Avatar_small
依云 说:
Sep 10, 2022 10:57:20 AM

才知道这个项目。它的演示地址打不开。我关心以下几点:

支持繁体和日文识别吗?以中文模式识别英文时带空格吗?结果包含文本方向信息吗?

Avatar_small
依云 说:
Sep 10, 2022 11:15:01 AM

它写着「部署简单」,但是我没看明白它要怎么用。它提供了一个 Web 前端,但是没别的了。具体识别一张图片方法混杂在这个 Web 前端中了。感觉它是个 application 而不是 library。另外代码质量处于 ML 界的平均水平,我实在是懒得去阅读与整合了。

Vifly 说:
Sep 10, 2022 02:26:20 PM

在我的测试当中,繁体中文不行,日文也不行;虽说可以识别英文,但不会给中英文间自动加上空格;代码里有识别文本方向,但没有输出相关信息。所以可能不满足你的需求。

Vifly 说:
Sep 11, 2022 06:43:41 PM

说到部署简单,安利一下我从 chineseocr_lite 中分离出来的 C++ 应用,能满足临时的中文 OCR 需求:https://github.com/vifly/chineseocr-lite-ncnn

Avatar_small
依云 说:
Sep 11, 2022 11:04:45 PM

我测试了一下,效果不理想啊,甚至比 tesseract 都要差……

Vifly 说:
Sep 11, 2022 11:58:53 PM

我怀疑原始模型训练时用了大量的长边长度为600的图片,使用参数 -l 600 把输入图片缩放到长边为 600 进行 OCR 可能会好上不少(如果输入图片不是太小)

c 说:
Sep 16, 2022 04:41:58 PM

不是呀,你看一下第一个连接啊,这是 podman 的特性不是 systemd 的。

Avatar_small
依云 说:
Sep 17, 2022 10:55:19 AM

哦哦,知道了。这是用 podman 弄了个独立的 netns,然后 systemd 通过 socket 激活把 fd 传过去。这个方案挺不错的,有空我实现一下。


登录 *


loading captcha image...
(输入验证码)
or Ctrl+Enter

| Theme: Aeros 2.0 by TheBuckmaker.com