阅读视图

发现新文章,点击刷新页面。
🔲 ☆

使用wayvnc远程访问无头Wayfire会话

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

显示器送去维修了,因此我的台式机变成无头设备啦。现在用它来编译软件是没啥问题的,但是我的GUI软件的窗口全部访问不了啦,编译出来的wayfire也没法调试了。本来已经存在的窗口我打算等显示器修好回来再用的,但是Vim刚出了一个公开利用方式的RCE,我怕我的GVim留在那里忘记了然后不小心中招,直接关又不记得它有没有打开什么需要处理的文件。于是想到了远程桌面。

首先想到的是群友的reframe。之前显示器还在的时候它挺好用的,但是现在显示器不在了,它就连不上去啦。读了文档才知道需要配置一个虚拟显示器,要动内核参数所以需要重启,我的窗口们就回不来啦。另外等显示器回来,要去掉虚拟显示器还要再次重启。

其实我还有另一个显示器,也就是大上的E-ink屏幕。接上去之后,我的wayfire桌面回来了,但是我的窗口并没有……lswin发现它们还在「unknown」显示器上。于是我开始研究Wayfire IPC看看怎么把它们弄回来。

花了些时候,去Matrix频道里问了一下,窗口们是用configure_view找回来了,但是发现我怎么几百个叫「input-method-popup」的窗口啊(其实这就是我要调试Wayfire的原因)。另外发现有创建无头输出的接口,也就是Wayland compositor层级的虚拟显示器啦。

墨水屏用来阅读和写代码挺好的,但是用来测试GUI软件就不太适合了,更不用提打游戏了。所以就想着既然能创建无头输出了,那我是不是可以把无头输出VNC出来、在笔记本屏幕上看?于是读了一下wayvnc的文档,发现还真可以。

创建无头输出:

import wayfire, os
addr = os.environ.get('WAYFIRE_SOCKET', os.environ['XDG_RUNTIME_DIR'] + '/wayfire-wayland-1-.socket')
sock = wayfire.WayfireSocket(addr)
sock.create_headless_output(1920, 1080)

然后记下创建出来的输出的名字(HEADLESS-开头的那个)。几经尝试,最终使用的wayvnc命令是:

wayvnc -f 60 -r -g -o HEADLESS-2 IP PORT

其中-f 60指定帧率,默认是30fps;-r指定渲染光标,否则在服务端使用鼠标时不显示光标;-g启用需要GPU的特性;-o HEADLESS-2是选择之前创建的无头输出。后边的监听地址我指定了与笔记本直连的有线网口的IP,这样不需要设置密码、也不用加密就很安全。

然后使用tigervnc连接:

LANGUAGE=en_US vncviewer -Maximize IP:PORT -PreferredEncoding H.264

vncviewer显示中文有问题,因此使用英文界面。这里指定了编码方式为H.264,希望能比默认更好一些。我原本还想用Raw来着,但是问了一下Gemini,说是1Gbps不够1080p60用,另外序列化延迟也会更高。不过wayvnc和vncviewer都没有使用VAAPI的样子,不知道是不支持还是哪里有问题。

实际占用的带宽最高为27 MiB/s。延迟挺低的,反正我感觉很不明显,玩游戏也没有问题。不过可能是因为有压缩,文字的显示不是很清晰。

哦对了,因为vncviewer是Xwayland软件,因此它自己的键盘捕获是没有用的。我使用Wayfire的shortcuts-inhibit设置让它默认捕获键盘了,需要的时候再按快捷键临时取消一下。

[shortcuts-inhibit]
break_grab = <ctrl> <alt> <shift> KEY_ESC
inhibit_by_default = app_id is "Vncviewer"

至于为什么不用别的VNC客户端,gtk-vnc慢死了,remmina界面好复杂,virt-viewer我不知道怎么叫它连远程VNC。

🔲 ☆

给论坛用上了文本嵌入模型

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

偶然间发现Discourse论坛支持利用文本嵌入模型来生成「相关话题」列表、提供语义化搜索。于是我给Arch Linux中文论坛试过了好几个模型,记录一下经验。

文本嵌入,英文叫「text embedding」,指的是将一段文本编码成语义空间中的向量,从而可以判断不同文本的语义相关性。编码出来的向量少则512维,多的能有4096维。而判断相关性有「余弦距离」(看两个向量的夹角大小)和「负内积」(一个向量和另一个向量的转置相乘,然后取负)两种方法,我都是看模型文档和示例来决定用哪个的。至于这些向量的存储和索引,Discourse使用的是pgvector这个PostgreSQL插件。

Discourse启用这个功能之后,会在每个话题下方推荐几个「相关话题」,很适合看看是不是有人问过相同的问题。语义化搜索则需要在搜索页面点按钮来显示。在搜索框里按两下回车,就能到搜索页面了(这时候语义化搜索就会进行了,虽然用户还看不到结果),或者点搜索框右边的按钮也行。

因为论坛以中文为主,所以没多少可以抄Discourse官方文档的地方。一开始我挑了好几个来尝试,bge-m3、all-mpnet-base-v2、gte-multilingual-base等。但是没想到它们体积不大,但跑起来却很吃资源。E5-2678 v3辛辛苦苦跑了好久,结果去数据库里一看,已索引的话题数量才几个、十几个,而且不见涨……后来写了API转换代理我才知道,原来是因为Discourse会批量并发请求,并发度会高达45左右,于是很容易导致本来就慢的请求因为排队太久而超时被放弃,CPU都白算了。

最终我找到gte-base-zh这个模型,是针对中文特化的。很小,才0.1B,但这CPU跑得动它。效果也还能接受。

后来了解到最近新出的Qwen3-Embedding系列,看评分效果是最好的。又有群友愿意提供显卡算力,于是试了试。

Qwen3-Embedding提供8B、4B、0.6B三种参数规模的模型。8B很重,我的6650XT的8G显存勉强能放下它的Q4_K_M量化版本。0.6B的只有Q8_0的量化版本,我的显卡跑起来轻松不少,就是不知道为什么它占了我4G+的显存,导致剩下的显存不够原神用了。另外运行的时候如果不用systemd的CPUWeight之类的手段降一下CPU优先级,会导致我的桌面也很卡——我没找到调整GPU优先级的方法,不过调整CPU优先级也管用。

这些模型在群友提供的RYZEN AI MAX+ 395上跑得就比较惨。这台设备有算力不错的核显——至少比用Linux的Apple M2 Ultra算得更快一些,也有核显能够使用大量内存的优势,但是!amdgpu驱动会在高负载时崩溃重置!这么久过去了,amdgpu依旧不待见核显啊(不过听说Intel那边新的xe驱动也有不少bug)。不过断断续续跑了几天之后,终于把大部分话题都索引好了。

后来我还是换0.6B模型了,因为群友提供的算力并不稳定,我想要更容易替代的方案。可能Qwen3-Embedding系列模型对我的用途来说实在是太优秀了,以至于不管是0.6B还是8B,我都没发现结果有什么明显的差异。但0.6B对性能的需求低很多,甚至编译机上的7950X3D也能跑——虽然编译机没那么多时间能跑它就是了。

我还尝试过Google家的embeddinggemma-300M模型。它的MTEB评分比gte-base-zh要高,但只比gte-base-zh大一倍。但实际用下来,呃,效果差很多,基本上没啥用,可能分数都得在别的语言上了吧。遂放弃。

目前的论坛文本嵌入算力主要由群友的RYZEN AI MAX+ 395提供。在它不在线的时候,则由另一位群友提供的Apple M2 Ultra编译机兼职。哪天要是它也有事不在了,还能由x86编译机接棒。在历史话题索引完毕之后,平时的请求其实挺少的。

哦对了,最近还接触过一个叫all-MiniLM-L6-v2的模型,超级小,只有22.7M参数,是火狐新加的地址栏语义化搜索用的。但是它只支持英文,对于中文来说纯粹在增加噪音,可以在about:config里搜索places.semanticHistory.featureGate关闭之。

最后说说运行这些模型的方式。对于给sentence-transformers用的模型,可以用ghcr.io/huggingface/text-embeddings-inference:cpu-latest这个容器来运行。缺点是,它只有支持CPU和CUDA的版本。所以我更喜欢找gguf格式的模型,然后用llama.cpp来运行,可以使用Vulkan或者ROCm。不过我测试发现llama.cpp用ROCm还不如用Vulkan的来得快,而ROCm有着极其巨大的依赖库群,我就不用它了。要是乐意用ROCm的话,也可以用ollama来跑,支持动态加载和卸载模型——但这对于长期运行的服务型用途来说并不是很适合,我还得传个参数让它不要一直加载卸载。

🔲 ☆

使用 Restic 备份数据

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

我很早就知道 Restic 这个备份工具了,但是因为我有 rsync 和 btrfs send/receive 方案所以一直没用过。某天,突然有个走 AWS s3 协议的服务器备份需求,这才把它翻了出来。

既然是 s3,首先设置环境变量 AWS_ACCESS_KEY_IDAWS_SECRET_ACCESS_KEY,然后仓库的地址是 s3:域名/存储桶名。好像一个存储桶里只能初始化一个 Restic 仓库?我还不清楚,也没有能给我玩的环境,就不管了。仓库地址可以设置到环境变量 RESTIC_REPOSITORY 中,这样就不用每次用 -r 参数指定了。执行 restic init 进行初始化。Restic 只支持加密备份,所以必须设置一个备份密码——倒也可以设置为空字符串,但是加密仍旧会进行,并且需要额外的参数来允许空密码。密码也可以放在 RESTIC_PASSWORD 环境变量里。

然后就是执行备份命令了,很简单的,比如:

restic backup -x --exclude-caches --exclude='/home/*/.cache/' --exclude=/var/cache/pacman/pkg/'*' ... /

这样就备份了整个系统,并排除了一些目录。可以先加 -v --dry-run 参数测试一遍。确定好备份的参数之后,就可以设定计划、反复执行了:

[Unit]
Description=backup the system
After=network-online.target

[Service]
Type=oneshot
Environment=HOME=/root
EnvironmentFile=/etc/restic.conf
ExecStart=restic backup -x --exclude-caches ... /
[Unit]
Description=backup system daily

[Timer]
OnCalendar=daily
RandomizedDelaySec=1h
AccuracySec=6h
Persistent=true

[Install]
WantedBy=timers.target

仓库地址和各种密码之类的放环境变量文件里了。把这个文件设置为只有 root 才能读,避免别的用户看到不该看的内容。然后 systemctl enable --now sysbackup.timer 就好了。

之后可以用 restic snapshots 查看备份的数据。使用 restic forget 删除旧的备份,比如 restic forget -d 30 -w 10 -m 10 -l 5 在最近30天、10周、10个月中各保留一份,外边最新的五个。这个命令只是删除这个备份的元数据,其关联的数据不会被删除。之后还要执行 restic prune 来实际删除数据——这个过程会比较费时费力。加 -v 可以查看诸如减小了多少空间占用的数据。

Restic 的备份是打包成块、压缩、加密存储的,带去重功能。但是增量备份会和主机名和路径相同的上一个备份进行比较。一个仓库里可以备份多台机器的相同或者不同的目录,但为了有效率地增量备份,每次增量备份时的目录不能变。这最后一点给我出了个难题。

我把 Arch Linux 中文论坛(以及维基)也用 Restic 备份到 sftp 目标里了。但这俩都是重数据库应用,因此直接对着 / 执行 restic backup 不行,数据有可能因为数据库的不同文件来自于不同的时间点而损坏。该系统在 btrfs 上并且有快照,对着快照备份就好了。问题是 Restic 执意不支持设置源路径,只能从文件系统上读取。我只好用 mount 命名空间把快照弄到 / 上再执行备份。脚本在这里:https://github.com/archlinuxcn/misc_scripts/blob/master/wiki/restic-backup-snapshot。由于权限的问题,用 bwrap 是不行的。对快照再做一个固定路径的可写快照来备份倒是能把路径固定到一个指定的位置,就是不太优雅,事后删掉快照也有额外的性能消耗。

关于备份数据的大小,restic snapshots 显示的是所有文件的名义大小的总和——是压缩前的大小,并且硬链接会算多次。restic stats --mode=restore-size 则是按硬链接去重后的未压缩大小。restic stats --mode=raw-data 才是压缩后的、备份实际占用的空间。

Restic 的备份仓库是加密的,因此无法直接查看,但它有 restic mount 子命令可以把备份仓库挂载了查看内容,就是性能比较差,只适合检查和恢复特定的少量文件。恢复大量数据推荐用 restic restore。另有 restic diff 命令可以查看不同快照之间的文件差异(新增、修改、删除了哪些文件,不含内容),方便在大小出现异常时查找元凶。

最后说说配置只能使用 sftp 的用户的方法。

首先在 sshd_config 里设置 Subsystem sftp internal-sftp。因为之后要进行 chroot,访问不到外部 sftp 服务的可执行文件的。然后设置指定的用户组只能用 sftp:

Match Group sftp-users
     ChrootDirectory /srv/sftp
     X11Forwarding no
     AllowTcpForwarding no
     AllowAgentForwarding no
     ForceCommand internal-sftp -d /%u

最后创建用户,按常规设置 authorized_keys,并按之前配置的路径,在 /srv/sftp 下创建该用户与其用户名同名的存储数据的目录,记得要 chown 到该用户。注意 ChrootDirectory 的目标目录(也就是这里的 /srv/sftp)应当只能由 root 写入(就跟 /home 目录那样)。

🔲 ☆

Arch Linux 中文论坛迁移杂记

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

本篇只是心得体会加上赞美和吐槽。技术性的迁移记录在这里。

一个多月前,肥猫在 Arch Linux 中文群里说:

(希望有好心人研究一下php74,最好加到archlinuxcn里,因为咱中文社区论坛卡在这个老版本了

然后话题自然就又到了论坛迁移的事情上来——毕竟 FluxBB 年久失修也不是一两天的事了。Arch Linux 官方讨论了几年还没结果,但中文社区这边并不是卡在往哪迁上,而是

基于什么的都可以迁移,但得有人干活

然后又过了些天,论坛使用的本就递送困难的 Sendgrid 停止了免费服务,导致中文论坛完全无法注册新用户了。虽然这件事通过更改为使用我们自己的邮件服务器就解决了,但迁移论坛的想法在我脑中开始成长。

至于迁移到哪个软件,我早已有了想法——包括 OpenSUSE 中文社区Debian 中文社区NixOSOpenSUSEFedoraUbuntuManjaroGaruda LinuxCachyOSKDEGNOMEPythonRustAtuinF-DroidOpenWrtLet's EncryptMozillaCloudflareGrafanaDocker 等等(还有一堆我没有那么熟悉的就不列了)大家都在用的 Discourse。这么多开源社区和商业组织都选择了它,试试总不会错的,用户也会比较熟悉。令我惊喜的是,它甚至有个 FluxBB 导入脚本。

于是在虚拟机里安装尝试。安装过程是由他们的脚本驱动构建的 docker 镜像,没什么特别的——除了比较耗资源。Docker 嘛,硬盘要吃好几 GiB,然后它是现场编译前端资源文件,CPU 和内存也消耗不少。默认的构建是包含 PostgreSQL、Redis、Nginx 的,但 PostgreSQL 也可以用外边的,就是得监听 TCP,并且构建出来的镜像里依旧会存在 PostgreSQL 的服务文件。Nginx 可以改成监听 UNIX 域套接字,然后让外边我自己的 Nginx proxy_pass 过来,这样证书也按自己的方式管理。Redis 我本来也是想拆出来的,但是为安全起见要设个密码嘛,然后构建就失败了……不过算了,反正也没别人用 Redis。

本地测的没有问题,于是去服务器上部署。由于发现它比较吃资源,所以给服务器加了几十G内存、100G 硬盘,还有闲置的 CPU 核心也分配上去了。后来发现运行起来其实也还好,就是内存吃得比较多——每个 Rails 进程大几百,32个加起来就快 10G 了。运行起来之后 CPU 不怎么吃,甚至性能比 MediaWiki 要好上不少,以至于我把反 LLM 爬虫的机制给降级到大部分用户都不需要做了。哦它的 nginx 会起 $(nproc) 个,太多了,被我 sed 了一下,只留下了八只(但其实也完全用不上,毕竟是异步的;Rails 那些进程可是同步的啊)。

run:
  - exec: sed -i "/^worker_processes/s/auto/8/" /etc/nginx/nginx.conf

说回部署。跑起来是没什么问题的。问题出在那个 FluxBB 导入脚本——本来导入就不快,它还跑到一半崩了,修了还崩。然后它不支持导入个性签名、用户头像、置顶帖,还遇到著名的 MySQL「utf8 不 mb4」的问题。来来回回修了又改,花了我好些天。等保留数据测试的时候,发现有好多帖子的作者变成「system」了。一查才发现我刻意没有导入被封禁的用户造成的。都已经配得差不多了,实在是不想删库重来,研究了一下,直接在 Rails 控制台写了段脚本更正了。这个 Rails 控制台能在 Rails 的上下文里交互式地执行代码,很方便改数据,我很喜欢,比 PHP 方便太多了!

另外这个导入脚本有一点好的是,它能够反复执行来更新数据——虽然这种支持反复执行的操作在我写的脚本里是常有的事,基本上从头开始成本太高的都会有,但别人写的脚本能考虑到这一点的可太少了。

用户的密码也导入了!帖子的重定向服务也写好了!虽然后来发现用户个人页面是登录用户才能访问的,给它重定向没什么用……反而是 RSS 重定向更有用,后来也补上了!

后来还发现有些用户名包含空格或者特殊符号啥的,被自动改名了。好在可以用邮箱认用户,不管了,等受影响的用户出现了再改。另外正式迁移之后才发现还有些数据没有迁移——版块的对应关系、用户的主题订阅,不是很重要,算了。

然后就迁移结束啦~所有到旧论坛的访问全部重定向到新论坛啦~不过我还是保留了一个不跳转的后门以便有需要时回去看看。为此我折腾了好久神奇的 nginx 的配置文件,最终得到以下片段:

set $up 'redir';
if ($http_cookie ~ "noredir=1") {
    set $up 'noredir';
    proxy_pass https://104.245.9.3;
}
if ($up = redir) {
    proxy_pass http://127.0.0.1:9009;
}

就是根据 cookie 来 proxy_pass 到不同的服务啦。这样就可以访问一下 /noredir 设置上 cookie,就可以访问旧论坛,再访问一下 /yesredir 清一下 cookie 就恢复跳转到新论坛了。

说起 nginx,Discourse 还在这方面坑了我一下。它文档里给的设置是:

proxy_set_header Host $http_host;

这个配置在 HTTP/3 时是坏的,应该用 $host。别问我 HTTP/2 也没有 Host 头啊,为什么它在用 HTTP/2 时就不会出错。我也不知道 ¯\(ツ)/¯。

于是新论坛上线啦~很多中国大陆用户的首次加载时间变成几十秒啦……还好这只是无缓存加载的时间,就当是下载软件了吧。之后每个标签页大约需要一两秒加载整个 SPA,在不同页面之间跳转并不慢。而这代价付出之后的回报是更现代的界面、丰富的功能。相比于旧论坛,现在:

  • 终于有手机版啦,甚至还支持 PWA,体验非常丝滑。
  • 实时预览的 markdown + bbcode 编辑器,还支持上传图片!再也不会有用户问论坛怎么传图片和日志了!
  • 编辑器还支持草稿功能!不用怕写一半弄丢了,甚至可以换个设备接着写。
  • 代码块角落里有个复制按钮,我再也不需要拖半天鼠标来复制日志然后粘贴进 Vim 里分析了!
  • 快速、简单的搜索体验,用户再也不会找不到搜索功能在哪里了!
  • 实时显示在回复的人。有人在回复的时候就可以等一等,发布帖子之后会立刻出现。发帖不需要跳转到不知道干什么的跳转页面了,读帖也不需要反复刷新了。
  • 有收藏功能了,用户不用发一个根本没什么用的「mark 一下」的帖子了。
  • 有「标记为已解决」的功能了。用户再也不需要问怎么把帖子标记为「已解决」了。
  • 不用自己跑脚本解析 HTML 来向群里发新帖通知了。Discourse 的「聊天集成」功能配一下就好了。
  • 甚至还有「RSS Polling」插件,可以把主站新闻转到论坛里,方便大家讨论。

Discourse 的邮件集成功能也挺不错的。配好之后,可以检测到退信,也可以直接回复邮件通知来回帖。甚至还有个邮件列表模式,就是把所有帖子都给用户发一遍,用户也可以直接回帖。通过邮件发布新主题的功能也有,但我没有启用——不同版块需要配不同的收件地址,有点麻烦,我不觉得有人会想用……就是这个邮件传回 Discourse 部分坑了我一把,但不是 Discourse 的错。

是 maddy 的文档太缺欠了。我要把 forum+...@archlinuxcn.org 这种地址给重写到 noreply@archlinuxcn.org,按例子像这样

table.chain local_rewrites {                                                                                                                   
    optional_step regexp "forum\+(.+)@(.+)" "noreply@$2"
    optional_step regexp "(.+)\+(.+)@(.+)" "$1@$3"
    optional_step static {
        entry postmaster postmaster@$(primary_domain)
    }
    optional_step file /etc/maddy/aliases
    step sql_query {
        driver postgres
        dsn "user=maddy host=/run/postgresql dbname=maddy sslmode=disable"
        lookup "SELECT mailname FROM mailusers.mailinfo WHERE $1 = ANY(alias) and new = false"
    }
}

这里第二行是我加的(虽然一开始把 $2 照着下边那个已有的写成了 $3)。结果是报错「用户不存在」、被退信。我开 debug 选项研究了好久,才意识到最后一步写的是 step,所以它总是要执行的——然而 noreply 这个用户并不在数据库里,所以就找不到了。

那把最后一行改成 optional_step 就好啦——我是这么想的,也是这么做的。然后就有人报告说 admin 邮箱拒收邮件了……又是一通研究,才发现因为这里的步骤全是 optional_step,所以 maddy 第一次用整个邮件地址来查的时候,无论如何都是会通过的——不会返回「目标不存在」,所以也就不会触发去掉域名、只用用户名查询的步骤,而数据库里记录的只有用户名,就导致 admin 邮箱的映射查不到了(映射到它本身,然而并没有以它为名的邮箱)。把 SQL 查询那一行改成这样子就好了:

lookup "SELECT mailname || '@archlinuxcn.org' FROM mailusers.mailinfo WHERE regexp_replace($1, '@archlinuxcn.org$', '') = ANY(alias) and new = false"

然后是把收件的邮件交给 Discourse。他们有个 mail-receiver 容器用来干这事,但这个容器的主要部分其实是 Postfix。我读了一下它的代码,实际上只需要把邮件通过 API 发过去就行了。于是我用 Python 写了一个服务——imap2discourse。这部分的坑在于,这个 API 是把邮件全文用指定的参数 base64 或者不 base64 用 form-data 传过去的,我以为是用上传文件的方式来传,搞了半天它都报奇奇怪怪的错,后来一步一步在 irb 里按 mail-receiver 的代码对照检查,才发现原来是按传字符串的方式传的……

Discourse 的中文翻译不怎么样,好多随意的空格,也好多看不懂的翻译。好在它像 MediaWiki,支持修改界面文本。于是就一点一点地修了好多。上游使用的是 Crowdin 翻译平台,并不能直接 pr 翻译,所以等我什么时候研究一下才能把翻译贡献给上游了。

Discourse 的通知功能挺全面的。可以选择回帖之后要不要通知,邮件通知是只在没访问时发、完全不要还是全都要,网站图标上要不要显示通知计数,还可以开启浏览器的推送通知(然后我就发现 Android 火狐的推送通知无法切换到 PWA 窗口)。

至于管理功能,比 FluxBB 丰富好用太多啦~有各种访问统计报表。设置项有搜索功能。有管理员操作日志,也有选项变更日志,还有邮件收发日志(看看谁又把自己的邮箱域名拼错了)。能给用户添加备注,也能切换成指定的用户看看他们看到的论坛是什么样子的。能给用户添加字段,让用户填写他们用的操作系统和桌面环境,省得回帖时经常要询问。还能加载自定义的 JavaScript 和 CSS,甚至是加强版本。还有暂时用不上的 API 和 webhook。哦对了,我发现它还会自己拒绝一些常见的讨厌爬虫。

除了 FluxBB 之外,还有一个叫 planetplanet 的 RSS 聚合软件也是死了好多年,导致 planet.archlinuxcn.org 多年不更新了。Discourse 正好有从 RSS 发帖的功能,于是将星球也复活了一下,将大家的 RSS 作为帖子在专门的版块发出。虽然界面不是很理想,但将就着用啦。RSS 聚合也是有的。Discourse 的 RSS 功能相当完善,几乎在所有合理的网址后边添加 .rss 就能订阅。

也给旧论坛做了个静态存档站。暂时还没上线,因为肥猫又跑掉了

Discourse 的备份功能会报错,因为它的容器里的 pg_dump 版本比较旧,和我在外边 Arch Linux 里运行的版本不一致。不过我觉得这样也挺好的——因为管理员是可以生成和下载备份文件的,也就是说,如果有管理员的权限被人恶意获取,那么他就能通过下载备份文件的方式获取整个 Discourse 数据库的内容。备份不了就少了这么个风险啦。当然备份我肯定是做了的,至于是如何做的,就等下一篇啦。

☑️ ⭐

pacfiles: 高速的 pacman -F 替代品

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

缘起

Linux 发行版的软件包管理器通常都会提供这么一个功能——查找文件在哪个仓库中存在的软件包里。实现起来也挺简单:仓库维护一个每个软件包里都有哪些文件的数据库,软件去查就可以了——假如用户不介意性能问题的话。

最开始,我使用的是 pkgfile。它是使用 C++ 编写的,会把 Arch 官方提供的 .files 数据库(压缩的 tar 归档)转成 cpio 归档再用(压缩可以靠 btrfs,问题倒是不大)。它比 pacman -F 可快多了,但是我后来不用了,因为它当时不支持多架构——即在 pacman.conf 里把 Architecture 设置为多个值,比如我用的 x86_64 x86_64_v3。现在等我写好了 pacfiles,才发现它终于在大半年之前支持多架构了……不过它看起来开发还是不太活跃,选项和输出格式也和 pacman -F 有很大的差别。

效果对比

最主要的功能是按文件名搜索,因此让我们先看看这个:

pacman -F vim 截图

pkgfile vim 截图

pacfiles -F vim 截图

pacman -F 和 pkgfile 都是遍历整个数据库。pacman -F 和 pacfiles 是单线程的,pkgfile 是多线程,但我不知道为什么 pacman -F 会慢那么多。pkgfile 比 pacfiles 快一些,毕竟它提供的信息少、又不好看、还是多线程并行工作。另外值得注意的是,pacman -F 由于会预先加载整个数据库到内存,因此内存占用了近 3G。

有时候也会想要按完整路径搜索

pacman -F /usr/bin/vim 截图

pkgfile /usr/bin/vim 截图

pacfiles -F /usr/bin/vim 截图

这次 pacfiles 因为有索引的帮助,并且不需要检查软件包是否已安装,比 pkgfile 快了不少。pacman -F 依旧又慢又吃内存。

接下来看看输出软件包的文件列表。这个由于输出结果多、输出格式又都差不多,我就重定向扔掉了,只看性能数据。

pacman -Fl vim-lily 截图

pkgfile -l vim-lily 截图

pacfiles -l vim-lily 截图

这次 pkgfile 比 pacfiles 略快。

有时候也会想用正则搜索

pacman -F --regex '.*libpython3\.11.*'

pkgfile --regex '.*libpython3\.11.*'

pacfiles -F --regex '.*libpython3\.11.*'

这次 pkgfile 比 pacfiles 快了不少。使用正则搜索时,pacfiles 没有使用索引,也是遍历数据,所以快不起来了。

不过 pacfiles 是支持通配符搜索的,也能用上索引,很快的。pacman -F 不支持这个。而 pkgfile 嘛……它不仅慢,好像还又出 bug 了。

pkgfile -g '*libpython3.11*'

pacfiles '*libpython3.11*'

如果我写 pacfiles 之前得知 pkgfile 修了多架构那个 bug,我也许就不会写 pacfiles 了。不过现在对比下来,我也不后悔啦。

另外值得注意的是,pacfiles 无论是输出、还是命令行选项,都尽力兼容 pacman -F 的,以方便用户迁移。

幕后

其实我很早就想弄一个更快的 pacman -F 了。我首先想到的是,把数据塞进 SQLite3 里让它查。性能确实是好得不得了,但是一看生成的数据库,好几个 G……后来又尝试像 pacman -F 那样直接读压缩包,但是不一次性加载到内存,因此不需要那么多内存。但结果并不理想:解压和遍历搜索都不太能快得起来,最多并行处理多个数据库而已。plocate 是很快啦,但是它的数据结构是自己定制的,并不是库,不能直接拿来用。于是此事便放下了。

直到前不久,我读到《Succinct data structures》一文,特别是文中提到的 FM-index——这不正好能用来搜索文件名吗?不过,plocate 用的是什么数据结构来着?于是我去翻代码恢复了一下久远的记忆。哦,是 zstd 压缩的 trigram 倒排索引啊。好像也不错,还支持通配符呢。正则搜索它倒是没用上索引,因为作者认为「使用 locate 进行正则搜索太小众了」所以没有花精力去实现。

但是,以上关于数据结构的内容都不是重点!重点是,我发现了个 plocate-build 命令!它支持从纯文本创建 plocate 数据库!那我不是直接把文件名传给它就好了嘛~唯一有点遗憾的是,它不支持从管道读取文件名列表,因此需要先输出到临时文件中再给它使用,过程中会占用不少内存(/tmp 空间)。至于查询,调用 plocate 命令拿到结果再稍微处理一下就好了。于是想到就做,这就有了现在的 pacfiles(其实早期版本也在 git 历史里有)。

项目地址:https://github.com/lilydjwg/pacfiles。AUR 有 pacfiles-git 包。也可以 cargo install pacfiles 安装。

🔲 ☆

用 Android 手机当电脑的话筒

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

我之前是使用 ROC 来做这件事的。手机上安装 roc-droid,电脑上安装 pipewire-roc 然后执行 pactl load-module module-roc-source source_name=roc-source 就行。

但是这样会有一个问题:手机上的 roc-droid 会被休眠。换手机之前用的 Android 10 还好一点,可以设置半小时的「超长」关屏时间,并且屏幕关闭之后 roc-droid 还能活跃一段时间。现在换 Android 14 了,关屏之后 roc-droid 会立刻被休眠,也不能把 roc-droid 切到后台,否则录音会停止。为了让录音不中断,只能让手机「喝点咖啡因」来保持亮屏,于是不光网络和录音费电,屏幕也要费电。其实这个问题不是不能解决,放个持久通知就可以了,但是我不会 Android 开发呀。

ROC 方案另外的小问题有:网络会持续占用,即使没在使用。手机要么录音、要么播放,需要手工切换。roc-droid 时不时会崩溃。

后来从群友那里了解到可以在 termux 里跑 PulseAudio,我试了试,比 ROC 方案好用多啦。

手机上除了需要安装 termux 和 pulseaudio 外,还需要安装 Termux:API。为了方便启动,我还安装了 Termux:Widget。记得给 Termux:API 话筒权限。然后编辑 PulseAudio 配置文件 /data/data/com.termux/files/usr/etc/pulse/default.pa.d/my.pa:

load-module module-sles-source
load-module module-native-protocol-tcp auth-ip-acl=电脑的IP地址 auth-anonymous=true

这里的 sles 模块是用来录音的。

编辑 /data/data/com.termux/files/usr/etc/pulse/daemon.conf 文件,设置一小时不用才自动退出(默认20秒太短了):

exit-idle-time = 3600

然后在需要的时候执行 pulseaudio 命令就可以了。

电脑上的话,其实设置 PULSE_SERVER 环境变量就可以用上了。不过为了更好的集成,我们创建个 tunnel:

pactl load-module module-tunnel-source server=tcp:手机的IP地址

source 就是把手机当话筒用,改成 sink 的话则是把手机当音箱用了。

执行之后,在 PulseAudio / PipeWire 里就会多出来相应的 source(或者 sink)设备了。想怎么用就可以怎么用了~

但若是要同时使用另外的音箱来播放声音的话,手机话筒会把音箱播放的声音录进去,造成「回声」。这时候,就需要设置一下回声消除了。我参考了 ArchWiki,PipeWire 配置如下:

context.modules = [
    {   name = libpipewire-module-echo-cancel
        args = {
            monitor.mode = true
            source.props = {
                node.name = "source_ec"
                node.description = "Echo-cancelled source"
            }
        }
    }
]

然后去 pavucontrol 里设置一下它生成的两个录音操作的设备(一个是选话筒,另一个是选外放的音箱的 monitor 设备),并把消除了回声的 source 设备设置为默认音频输入设备就好了。

🔲 ⭐

使用 ffmpeg 对音频文件进行响度归一化

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

我喜欢用本地文件听歌:没有广告、没有延迟、没有厂商锁定。但是有个问题:有的歌曲文件音量挺大的,比如 GARNiDELiA 和桃色幸运草Z的都感觉特别吵,需要调小音量,但有的音量又特别小,以至于我时常怀疑音频输出是不是出了问题。

这时候就要用到响度归一化了。响度衡量的是人的主观感知的音量大小,和声强——也就是声波的振幅大小——并不一样。ffmpeg 自带了一个 loudnorm 过滤器,用来按 EBU R128 标准对音频做响度归一化。于是调整好参数,用它对所有文件跑一遍就好了——我最初是这么想的,也是这么做的。

以下是我最初使用的脚本的最终改进版。是的,改进过好多次。小的改进如排除软链接、反复执行时不重做以前完成的工作;大的改进如使用 sem 并行化、把测量和调整两个步骤分开。之所以有两个步骤,是因为我要线性地调整响度——不要让同一个音频不同部分受到不同程度的调整。第一遍是测量出几个参数,这样第二遍才知道怎么调整。只过一遍的是动态调整,会导致调整程度不一,尤其是开头。

至于参数的选择,整体响度 I=-14 听说是 YouTube 它们用的,而真峰值 TP=0 和响度范围 LRA=50 是因为我不想给太多限制。

#!/bin/zsh -e

for f in **/*.{flac,m4a,mp3,ogg,opus,wma}(.); do
  json=$f:r.json
  if [[ -s $json || $f == *_loudnorm.* ]]; then
    continue
  fi
  echo "Processing $f"
  export f json
  sem -j+0 'ffmpeg -i $f -af loudnorm=print_format=json -f null /dev/null </dev/null |& sed -n ''/^{$/,/^}$/p'' > $json; echo "Done with $f"'
done

sem --wait

for f in **/*.{flac,m4a,mp3,ogg,opus,wma}(.); do
  json=$f:r.json
  output=$f:r_loudnorm.$f:e
  if [[ ! -f $json || -s $output || $f == *_loudnorm.* ]]; then
    continue
  fi
  echo "Processing $f"
  export f json output
  sem -j+0 'ffmpeg -loglevel error -i $f -af loudnorm=linear=true:I=-14:TP=0:LRA=50:measured_I=$(jq -r .input_i $json):measured_TP=$(jq -r .input_tp $json):measured_LRA=$(jq -r .input_lra $json):measured_thresh=$(jq -r .input_thresh $json) -vcodec copy $output </dev/null; echo "Done with $f"'
done

sem --wait

不得不说 zsh 的路径处理是真方便。相对地,sem 就没那么好用了。一开始我没加 </dev/null,结果 sem 起的进程全部 T 在那里不动,strace 还告诉我是 SIGTTOU 导致的——我一直是 -tostop 的啊,也没见着别的时候收到 SIGTTOU。后来尝试了重定向 stdin,才发现其实是 SIGTTIN——也不知道 ffmpeg 读终端干什么。另外,给 sem 的命令传数据也挺不方便的:直接嵌在命令里,空格啥的会出问题,最后只好用环境变量了。

等全部处理完毕,for f in **/*_loudnorm.*; do ll -tr $f:r:s/_loudnorm//.$f:e $f; done | vim - 看了一眼,然后就发现问题了:有的文件变大了好多,有的文件变小了好多!检查之后发现是编码参数变了:mp3 文件全部变成 128kbps 了,而 flac 的采样格式从 s16 变成了 s32。

于是又写了个脚本带上参数重新处理。这次考虑到以后我还需要对单个新加的歌曲文件处理,所以要处理的文件通过命令行传递。

#!/bin/zsh -e

doit () {
  local f=$1
  local json=$f:r.json
  local output=$f:r_loudnorm.$f:e

  echo "Processing $f"

  if [[ -s $json || $f == *_loudnorm.* ]]; then
  else
    ffmpeg -i $f -af loudnorm=print_format=json -f null /dev/null </dev/null |& sed -n '/^{$/,/^}$/p' > $json
  fi

  if [[ ! -f $json || -s $output || $f == *_loudnorm.* ]]; then
  else
    local args=()
    if [[ $f == *.mp3 || $f == *.m4a || $f == *.wma ]]; then
      local src_bitrate=$(ffprobe -v error -select_streams a:0 -show_entries stream=bit_rate -of json $f | jq -r '.streams[0].bit_rate')
      args=($args -b:a $src_bitrate)
    fi
    if [[ $f == *.m4a ]]; then
      local src_profile=$(ffprobe -v error -select_streams a:0 -show_entries stream=profile -of json $f | jq -r '.streams[0].profile')
      if [[ $src_profile == HE-AAC ]]; then
        args=($args -acodec libfdk_aac -profile:a aac_he)
      fi
    fi
    if [[ $f == *.opus ]]; then
      local src_bitrate=$(ffprobe -v error -select_streams a:0 -show_entries format=bit_rate -of json $f | jq -r '.format.bit_rate')
      args=($args -b:a $src_bitrate)
    fi
    if [[ $f == *.ogg ]]; then
      local src_bitrate=$(ffprobe -v error -select_streams a:0 -show_entries stream=bit_rate -of json $f | jq -r '.streams[0].bit_rate')
      if [[ $src_bitrate == null ]]; then
        src_bitrate=$(ffprobe -v error -select_streams a:0 -show_entries format=bit_rate -of json $f | jq -r '.format.bit_rate')
      fi
      args=($args -b:a $src_bitrate)
    fi
    if [[ $f == *.flac ]]; then
      local src_sample_fmt=$(ffprobe -v error -select_streams a:0 -show_entries stream=sample_fmt -of json $f | jq -r '.streams[0].sample_fmt')
      args=($args -sample_fmt:a $src_sample_fmt)
    fi
    ffmpeg -loglevel error -i $f -af loudnorm=linear=true:I=-14:TP=0:LRA=50:measured_I=$(jq -r .input_i $json):measured_TP=$(jq -r .input_tp $json):measured_LRA=$(jq -r .input_lra $json):measured_thresh=$(jq -r .input_thresh $json) $args -vcodec copy $output </dev/null
    touch -r $f $output
  fi

}

for f in "$@"; do
  doit $f
done

然后我就神奇地发现,sem 不好用的问题突然没有了——我直接 parallel loudnorm ::: 文件们 就好了嘛……

☑️ ☆

为团队部署邮件服务

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

给服务器上的程序部署邮件服务十分简单,装个 Postfix 就搞定了。然而给人用的话就远远不够了。之所以要干这事,主要原因是之前使用的 Yandex 邮箱老出问题,丢邮件都算小事了,它还不让我登录 Web 界面,非要我填写我从未设置的密保问题的答案……

准备工作

要部署邮件服务,首先当然要有域名和服务器了。需要注意的是,最好使用可以设置 PTR 记录的服务器,有些邮件服务器会要求这个。

邮件传输代理

这是最重要的部分。邮件传输代理,简称 MTA,是监听 TCP 25 端口、与其它邮件服务器交互的服务程序。我最常用的是 Postfix,给服务器上的程序用的话,它相当简单易用。但是要给它配置上 IMAP 和 SMTP 登录服务、以便给人类使用的话,就很麻烦。好在之前听群友说过 maddy,不仅能收发邮件,还支持简单的 IMAP 服务。唯一的缺点是不支持通过 25 端口发送邮件——需要走 465 或者 587 端口,登录之后才能发件。它的账号系统也是独立于 UNIX 账号的,给程序使用需要额外的配置。

具体配置方面,首先是域名和 TLS 证书。我不知道为什么,它在分域名证书的选择上有些问题,最后我干脆全部用通配符证书解决了事。数据库我使用的是 PostgreSQL。要使用本地 peer 鉴权的话,需要把 host 的值设置为 PostgreSQL 监听套接字所在的目录,比如我是这样写的:

dsn "user=maddy host=/run/postgresql dbname=maddy sslmode=disable"

PostgreSQL 监听套接字所在目录是编译时确定的。maddy 是 Go 写的,并不使用 libpq,因此它无法自动确定这个目录在哪里,需要手动指定。

关于邮箱别名,可以使用文本文件配置,也可以使用数据库查询指定。别名功能可以用来实现简单的邮件列表功能——发往某一个地址的邮件会被分发到多个实际收件人的邮箱中。但是它不支持去重,也就是说,往包含自己的别名地址发送邮件,自己会额外收到一份。设置起来大概是这样子的:

table.chain local_rewrites {
    optional_step regexp "(.+)\+(.+)@(.+)" "$1@$3"
    optional_step static {
        entry postmaster postmaster@$(primary_domain)
    }
    optional_step file /etc/maddy/aliases
    step sql_query {
        driver postgres
        dsn "user=maddy host=/run/postgresql dbname=maddy sslmode=disable"                                                                                                                 
        lookup "SELECT mailname FROM mailusers.mailinfo WHERE $1 = ANY(alias) and new = false"
    }
}

哦对了,那个 postmaster 地址需要手动合并,不然就要每个域名创建一个账号了。在别名文件里写上 postmaster@host2: postmaster@host1 就行了。

maddy 会经常检查别名的修改时间然后自动重新加载,数据库查询当然是查出来是什么就是什么,所以还是比 Postfix 每次跑 postalias 命令要方便不少。

DNS 配置

邮件域名的 MX 记录当然要设置上的。邮件服务器 IP 的 PTR 记录也要设置到服务器的域名上(A / AAAA 记录指到服务器)。SPF 的记录也不能忘。DMARC 和 DKIM 的记录没那么重要,不过推荐按 maddy 的文档设置上。

我还给域名设置 imap、imaps 和 submission 的 SRV 记录,但似乎客户端们并不使用它们。

这些设置好之后就可以去 https://email-security-scans.org/ 发测试邮件啦。

反垃圾

maddy 内建对 rspamd 的支持,所以就用它好了。直接在 smtpcheck 节里写上 rspamd 就好了。rspamd 跟着官方教程走,也基本不需要什么特别的设置,就是官方给的 nginx 配置有些坑人。我是这样设置的:

    location /rspamd/ {
            alias /usr/share/rspamd/www/;
            expires 30d;
            index index.html;
            try_files $uri $uri/ @proxy;
    }
    location @proxy {
            rewrite ^/rspamd/(.*)$ /$1 break;
            proxy_pass  http://127.0.0.1:11334;
            proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
            proxy_set_header Host $http_host;
    }

注意这里给静态文件设置了过期时间,不然每次访问都要下载那些文件,非常慢。我是挂载在子路径下的,需要通过 rewrite 配置把子路径给删掉再传给 rspamd,不然会出问题。

邮件客户端自动配置

上边提到了 SRV 记录并不管用。实际上管用是在 https://autoconfig.example.org/mail/config-v1.1.xml 的配置文件。具体可以看 Lan Tian 的《编写配置文件,让 Thunderbird 自动配置域名邮箱》这篇文章。

Web 邮件客户端

使用的是 Roundcube,是一个 PHP 软件。可以跟着 ArchWiki 的教程配置。注意最好别跟着配置 open_basedir,因为会影响同一 php-fpm 实例上的其它服务。另外记得配过期时间,不然每次都要下载静态资源,很慢的。

因为上边部署了 rspamd 反垃圾服务,所以也可以给 Roundcube 启用一下 markasjunk 插件,并在 /usr/share/webapps/roundcubemail/plugins/markasjunk/config.inc.php 配置一下对应的命令:

$config['markasjunk_spam_cmd'] = 'rspamc learn_spam -u %u -P PASSWORD %f';
$config['markasjunk_ham_cmd'] = 'rspamc learn_ham -u %u -P PASSWORD %f';

不过我配置这个之后,命令会按预期被调用,但是 rspamd 的统计数据里不知为何总显示「0 Learned」。把垃圾邮件通过命令行手动喂给它又会提示已经学过该邮件了。

☑️ ☆

使用 nftables 屏蔽大量 IP

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

本来我是用 iptables 来屏蔽恶意IP地址的。之所以不使用 ipset,是因为我不想永久屏蔽这些 IP。iptables 规则有命中计数,所以我可以根据最近是否命中来删除「已经变得正常、或者分配给了正常人使用」的 IP。但 iptables 规则有个问题是,它是 O(n) 的时间复杂度。对于反 spam 来说,几千上万条规则问题不大,而且很多 spam 来源是机房的固定 IP。但是以文件下载为主、要反刷下行流量的用途,一万条规则能把下载速率限制在 12MiB/s 左右,整个 CPU 核的时间都消耗在 softirq 上了。perf top 一看,时间都消耗在 ipt_do_table 函数里了。

行吧,临时先加补丁先:

iptables -I INPUT -m state --state RELATED,ESTABLISHED -j ACCEPT

这样让已建立的连接跳过后边上万条规则,就可以让正常的下载速度快起来了。

此时性能已经够用了。但是呢,还是时不时需要我手动操作一下,删除计数为零的规则、清零计数、合并恶意 IP 太多的网段。倒不是这些工作自动化起来有困难(好吧,让我用 Python 3.3 来实现可能是有些不便以至于至今我都没有动手),但是这台服务器上有新工具 nftables 可用,为什么不趁机试试看呢?

于是再次读了读 nft 的手册页,意外地发现,它竟然有个东西十分契合我的需求:它的 set 支持超时!于是开虚拟机对着文档调了半天规则,最终得到如下规则定义:

destroy table inet blocker

table inet blocker {
    set spam_ips {
        type ipv4_addr
        timeout 2d
        flags timeout, dynamic
    }
    set spam_ips6 {
        type ipv6_addr
        timeout 2d
        flags timeout, dynamic
    }

    chain input {
        type filter hook input priority 0; policy accept;

        ct state established,related accept
        ip saddr @spam_ips tcp dport { 80, 443 } update @spam_ips { ip saddr timeout 2d } drop
        ip6 saddr @spam_ips6 tcp dport { 80, 443 } update @spam_ips6 { ip6 saddr timeout 2d } drop
    }
}

nftables 是自己创建 table 的,不用和别人「共用一张桌子然后打架」啦。然后定义了两个动态的、支持超时的、默认超时时间是两天的 set。nftables 的 table 可以同时支持 IPv4 和 IPv6,但是规则和 set 不行,所以得写两份。在 chain 定义中设置 hook,就跟 iptables 的默认 chain 一样可以拿到包啦。然后,已建立的连接不用检查了,因为恶意 IP 还没学会连接复用。接下来,如果源 IP 位于 set 内并且是访问 HTTP(S) 的话,就更新 set 的超时时间,然后丢弃包。限制端口是为了避免万一哪天把自己给屏蔽掉了。nftables 的规则后边可以写多个操作,挺直观、易于理解的。

然后让自己的恶意 IP 识别脚本用 nft add element inet blocker spam_ips "{ $IP }" 这样的命令向 set 里添加要屏蔽的 IP 就可以啦。两天不再有请求过来的 IP 会被自动解除屏蔽,很适合国内的三大运营商的动态 IP 呢。

跑了几天,被屏蔽的 IP 数量稳定在 26k—28k 之间。有昼夜周期,凌晨零点多和早上六七点是爆发期,晚间是静默期。性能非常好,softirq 最高占用不到 10%。

nftables 也很好用。虽然 nft 的手册页有点难懂,多看几遍、了解其写作结构之后就好很多了。不过要是支持 IP 地址到 counter 的动态 map 就好了——我想统计各 IP 的流量。nftables 还自带 Python 绑定,虽说这 API 走 JSON 感觉怪怪的,libnftables-json(5) 这文档没有超链接也很难使用,但至少弄明白之后能用。我用来写了个简单的统计脚本:

#!/usr/bin/python3

import os
from math import log10
from itertools import groupby

import nftables

def show_set(nft, name):
  ret, r, error = nft.json_cmd({'nftables': [{'list': {'set': {'family': 'inet', 'table': 'blocker', 'name': name}}}]})
  if ret != 0:
    raise Exception(ret, error)
  try:
    elements = r['nftables'][1]['set']['elem']
  except KeyError: # empty set
    return
  ips = [(x['elem']['val'], x['elem']['expires']) for x in elements]
  ips.sort(key=lambda x: x[1])

  histo = []
  total = len(ips)
  for k, g in groupby(ips, key=lambda x: x[1] // 3600):
    count = sum(1 for _ in g)
    histo.append((k, count))
  max_count = max(x[1] for x in histo)
  w_count = int(log10(max_count)) + 1
  w = os.get_terminal_size().columns - 5 - w_count
  count_per_char = max_count / w
  # count_per_char = total / w
  print(f'>> Histogram for {name} (total {total}) <<')
  for hour, count in histo:
    print(f'{hour:2}: {f'{{:{w_count}}}'.format(count)} {'*' * int(round(count / count_per_char))}')
  print()

if __name__ == '__main__':
  nft = nftables.Nftables()
  show_set(nft, 'spam_ips6')
  show_set(nft, 'spam_ips')

最后,我本来想谴责用无辜开源设施来刷下行流量的行为的,但俗话说「人为财死」,算了。还是谴责一下运营商不顾社会责任、为了私利将压力转嫁给无辜群众好了。自私又短视的人类啊,总有一天会将互联网上的所有好东西都逼死,最后谁也得不到好处。

☑️ ⭐

YubiKey 初体验

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

YubiKey 支持多种协议,或者说使用方式、模式,ykman 里称作「application」(应用程序)。很多程序支持多种 application。本文按 application 分节,记录一些自己的研究结果,并不全面。要全面的话,还请参考 ArchWiki 的 YubiKey 页面或者 YubiKey 官方文档

在 Arch Linux 上,YubiKey 插上就可以用了,不需要特别的驱动方面的设置。有可能某些程序需要装个 libfido2 包来使用它。

Yubico OTP

插上之后,它会有一个键盘设备。摸一下,它就发送一长串字符然后回车。这串字符每次都不一样,并且需要与 Yubico 的服务器通信来验证是否有效。这串字符使用 AES 对称加密,也就意味着 Yubico 服务器也有私钥(你也可以自建服务器自己用)。

所以这是个没什么用的功能。并且在拔下设备时很容易误触,插到 Android 设备上之后输入法的屏幕键盘还可能不出来。所以我把它给禁用了:

ykman config mode FIDO+CCID

FIDO2 / U2F

这个 application 在 Android 上第一次使用的时候会提示设置 PIN。我已经设置好了,也不知道在电脑上会如何。需要注意的是,这个 PIN 可以使用字母,并不需要是纯数字。最多可以连续输错八次,但没有 PIN 被锁之后用来解锁的 PUK。

WebAuthn / Passkey

插上就可以在火狐和 Google Chrome 里使用了。可以在 https://webauthn.io/ 测试。作为可以直接登录的 passkey 使用的话,会要求输入 PIN 和触摸。如果仅仅是作为二步验证使用(比如 GitHub),则只需要触摸即可。

Android 上也是差不多的。不过 Android 支持把 passkey 存储在设备里(还会通过 Google 账号同步),使用 YubiKey 时需要从弹窗中选取「使用其它设备」。如果网站已经在设备里存储了 passkey,那么没有使用 YubiKey 的机会。

SSH

OpenSSH 客户端需要安装 libfido2 包才能支持这些 -sk 结尾的 key。服务端不需要这个库。

有多个选项,具体参见 SSH Resident Key Guide。我总结了两种比较好的使用方式:

ssh-keygen -t ed25519-sk -O resident -O verify-required
ssh-keygen -t ed25519-sk -O no-touch-required

可以选择的 key 类型只有ecdsa-sked25519-sk,并不支持 RSA。resident选项是把 key 存储到 YubiKey 上,之后可以通过ssh-keygen -K下载回来。如果不加这个选项的话,那么仅凭 YubiKey 是无法使用的,得同时有生成的文件。verify-required是验证 PIN。默认是需要触摸的,可以用no-touch-required选项关闭,但是需要服务端在 authorized_keys 里设置这个选项。

从安全角度考虑,如果 YubiKey 丢失,那么仅凭该设备不应当能获得任何权限——所以在使用 resident 密钥时必须验证 PIN(我总不能赌偷或者捡到的人猜不中我的用户名或者访问的服务器吧)。这与自动化执行 SSH 命令相冲突。另一种使用方式,不需要 PIN、不需要触摸,倒是很方便自动化,也可以防止私钥被运行的程序偷走或者自己失误泄露,但是需要服务端设置no-touch-required选项,而 GitHub 和 GitLab 并不支持。倒是可以不同场合使用不同的 key,但是管理起来太复杂了。

resident 密钥倒是可以使用 ssh-add 加载到 ssh-agent 里,之后应该就不需要交互即可使用了。但我现在启动系统要输入硬盘密码,登录到桌面并日常使用的话,还要输入用户密码和火狐主密码,已经够多了,不想再加一个 PIN。所以我还是不用了吧。

我倒是想给 termux 里的 ssh 用 YubiKey,毕竟手机上一堆乱七八糟的闭源程序,外加系统已经失去更新,感觉不怎么安全。但是搜了一圈看起来并不支持。

PAM

安装 pam_u2f 包,然后使用 pamu2fcfg 生成个文件。最后去改 PAM 配置就好啦,比如在 /etc/pam.d/sudo 开头加上

auth            sufficient      pam_u2f.so cue userpresence=1

这样会用触摸 YubiKey 来认证用户。如果把 YubiKey 拔了,pam_u2f 会被跳过。但是 YubiKey 正常的情况下,没有办法跳过 pam_u2f,所以通过 ssh 登录的时候会很难受……好吧,用 pam_exec 还是有办法跳过的,但是它似乎读不到环境变量,只能放个文件来控制,所以还是很麻烦。最好的办法是我在 pam_u2f 运行的时候按一下 Ctrl-C,它就放弃掉就好了,但这个 issue 已经等了快要六年了。

LUKS

cryptsetup 并不直接支持 FIDO2。要使用 systemd-cryptenroll 来添加 keyslot:

sudo systemd-cryptenroll --fido2-device=auto /dev/disk/by-partlabel/XXX

可以用sudo cryptsetup luksDump /dev/disk/by-partlabel/XXX命令看到 systemd 不光添加了一个 keyslot,还同时添加了一个 token 用于存储一些配置信息。

解密:

sudo systemd-cryptsetup attach XXX /dev/disk/by-partlabel/XXX '' fido2-device=auto

或者用 cryptsetup open 也行。但因为添加的 slot 是需要 PIN 的,cryptsetup open 不加 token 相关的选项时会跳过该 slot,直接问你密码。

sudo cryptsetup open --token-only /dev/disk/by-partlabel/XXX XXX

配置好之后,解密 LUKS 设备就不需要输入又长又复杂的密码啦。不过最好还是时不时验证一下自己还记得密码,要是需要用的时候才发现密码因为长期不用而遗忘了就不妙了。我的系统硬盘本来解密的次数就少,就不用它了,只给备份硬盘用了。

OpenPGP

ykman 要管理 OpenPGP 智能卡应用,需要启用 pcscd 服务,但是 GnuPG 可以不用它。

sudo systemctl enable --now pcscd.socket

要让 ykman 和 GnuPG 能同时访问 YubiKey,可能还需要以下设置:

pcsc-driver /usr/lib/libpcsclite.so
card-timeout 5
disable-ccid
pcsc-shared

YubiKey 所有不同 application 的 PIN 是分开的。OpenPGP application 有 PIN 和管理 PIN,默认各能试三次。使用 key 的时候会用到 PIN,导入 key 的时候会用到管理 PIN。初次使用的时候记得用ykman openpgp access命令把默认的 123456 和 12345678 都给改了(不知道为什么我没找到在gpg --card-edit里更改管理 PIN 的方法)。导入的教程可以参考官方文档的 Importing keys。我的型号是 YubiKey 5C Nano,是支持 ed25519 / cv25519 算法的。

把 key 导入到 YubiKey 之后,可以再用ykman openpgp keys set-touch设置一下哪些操作需要触摸。默认是都不需要的。然后正常使用就可以了。

要注意的是,YubiKey 只存储了私钥,所以本地要有公钥才可以正常使用。所以要换个系统使用的话,一种办法是把公钥上传到 OpenPGP 服务器上然后导入,另一种办法是自己导出成文件再导入。

SSH 也可以用 OpenPGP 密钥,所以也能用 YubiKey 上的 OpenPGP 密钥。甚至还能把现有的 ed25519 SSH key 导入进去用(不过我没有尝试)。

PIV

这个 PIV 涉及 PKCS#11,有点复杂。暂时不想研究。

☑️ ⭐

我正在使用的火狐扩展(2024年版)

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

距离上次分享好久了,于是又来啦~

桌面版

每一项第一行是扩展标题和链接,第二行是扩展自己的描述信息,第三行(如有)是我为写本文添加的介绍和评论。

篡改猴
使用用户脚本自由地改变网络
复制链接/标签名称和地址
将链接名称和地址复制到剪贴板
复制链接地址
使用快捷键 "a" 来复制链接地址
对着链接点右键,然后按 a 键就可以复制到链接啦。
书签搜索
使用已加为书签的搜索引擎搜索选定文本
我在访问哪个 Cloudflare® 数据中心?
显示正在访问的 Cloudflare® 名称信息
云盘万能钥匙
您的云盘智能助手
大概没什么用了吧……
About Sync
Show information about Firefox Sync.
同步出现问题时用过。它也可以直接发送请求、修改服务端的信息,比如删掉已卸载扩展的同步数据啥的。
Auto Tab Discard
如果您打开了很多标签页,这个扩展能提升浏览器速度并减少内存占用。
就是标签页休眠啦。
Behind The Overlay Revival
Click to close any overlay popup on any website.
一键关弹窗,不用找关闭按钮在哪里。
Bypass Paywalls
Bypass News Sites' Paywalls
cliget
Download login-protected files from the command line.
为下载的文件生成 wget / curl 的命令行。我现在很少用了,主要用途是在服务器上下载不能直接下载的文件。
Control Panel for YouTube
Gives you more control over YouTube by adding missing options and UI improvements
这个扩展功能不少,我主要用的地方有:隐藏短视频(浪费时间)、自动生成的音乐合集(我从来不听这个)、即将开播的视频(又不能看,显示着干嘛)、已观看完毕的视频。隐藏视频结尾总是挡到我看内容的卡片、结束时的推荐视频。将短视频播放器重定向到有进度条的正常播放器。
Cookie Quick Manager
An addon to manage (view, search, create, edit, delete, backup, restore) cookies.
Dark Reader
适用于所有网站的暗色主题。关爱眼睛,就使用 Dark Reader 进行日常浏览。
Decentraleyes
保护您免受集中式的内容交付网络(CDN)的跟踪。
Discard Tab
Adds Discard action to tab right-click
手动休眠标签页,避免浪费系统资源。
Flagfox
显示描述当前服务器位置的国旗。
Foxy Gestures
适用于 Firefox 的鼠标手势
FoxyImage
Collection of Image Related Actions
Google™ Translator
A handy multi-language translator built on top of Google translate.
Header Editor
管理浏览器请求,包括修改请求头和响应头、重定向请求、取消请求
用来做一些 hack 操作的,比如添加 referrer、跨域头;在新标签页中查看 imgur 的图片(不要给我网页);让 Grafana 不走代理、直连数据源以加快加载速度。这扩展在火狐上还能修改响应体。
I don't care about cookies
Get rid of cookie warnings from almost all websites!
Image Max URL
Finds larger or original versions of images
Link Status Redux
Shows an indicator on a popup panel along with the link address when the mouse cursor is over a link to a page you bookmarked or visited before.
显示链接的上次访问时间用的。
matrix.to opener
在你的 Matrix 客户端中直接打开 matrix.to 链接
MergEase • GitHub Code Review
Diff tool for GitHub pull requests
更准确地 diff GitHub 提交和 pull request,有点像 difft,是把 diff 发给服务端来生成的。
Mind the Time
Keep track of how much time you spend on the web, and where you spend it. A ticker shows the time spent at the current site or total time spent on the web today. A summary page shows data for today and recent history.
Octotree - GitHub code tree
GitHub on steroids
给 GitHub 的侧边栏文件树。
Popup window
將 Tab 彈出至獨立視窗,去除頁籤列、網址列和書籤列等介面
这扩展在 Wayfire 上不太好用,弹窗和原本窗口会跟在一起,而且关闭的时候容易关到弹窗后边的窗口。
Push to Kindle
Send web articles to your Kindle
哦,这个应该没用了……
Redirect Link
Redirect a link to somewhere else.
用于打开网页对应的互联网档案馆或者 archive.today 存档用的。
Redirector
Automatically redirect content based on user-defined rules.
和上边那个名字相似、但功能完全不同。自动重定向用的,比如看图要看原图、绕开 link.zhihu.com、统一中文维基百科中间的语种路径、把移动版 URL 重定向到桌面版、去掉 b23.tv 的小尾巴等等。
Reload PAC button
A button to reload the PAC definitions
Rotate and Zoom Image
Allows to rotate and zoom images directly on any website from context menu.
RSS Reader Extension (by Inoreader)
Build your own newsfeed
装了这个才能用快捷键让 InoReader 在后台打开文章。
RSSPreview
Preview RSS feeds in browser
ScrollAnywhere
使用鼠标中键在页面上的任何位置拖动滚动条。还支持“抓取和拖动”样式和动画。
横着滚、竖着滚、滚来滚去~
SingleFile
将一个完整的页面保存到单个 HTML 文件中
Snap Links
Select multiple links, checkboxes and other elements and act on them such as open them in new tabs or check/un-check them.
SponsorBlock for YouTube - 跳过赞助商广告
跳过 YouTube 视频中的赞助广告、订阅提醒等片段。标记视频中的赞助广告来节约大家的时间。
Stylus
Stylus 是一个调整网页外观的用户样式管理器。它可以让您轻松为许多热门网站安装主题和皮肤。
给网页加自定义 CSS 用的,我的用途有:叫网页不要使用奇奇怪怪的 Windows / MacOS 系字体;把暗色网页弄亮堂一点,避免在白天看不清;在各种文档网页里标记访问过的链接,免得老是点过去才发现内容已经读过了,或者不容易找到自己频繁访问的链接;去掉讨厌的圆角。
Textarea Cache
Allows to save automatically the content in a text input field.
不小心关掉了正在编写、尚未提交的内容,可以用它来恢复。
Tile Tabs WE
Take tabs from parent windows and arrange them in layouts of tiled sub-windows.
这扩展在 Wayland 上不能移动窗口,不过还是可以把窗口调整为合适平铺的大小,并可选加上滚动同步啥的。
Tree Style Tab - 树状标签页管理
以树状结构显示标签页。
uBlacklist
在谷歌的搜索结果中屏蔽特定的网站显示。
内容农场走开!
uBlock Origin
一款高效的网络请求过滤工具,占用极低的内存和 CPU。
Unpaywall
Legally get full text of scholarly articles as you browse.
User-Agent Switcher and Manager
Spoof websites trying to gather information about your web navigation to deliver distinct content you may not want
有时候还是不得不假装自己在用 Google Chrome 或者 Windows。
v2ex plus
优雅便捷的 V2EX 扩展
Vimium
The Hacker's Browser. Vimium provides keyboard shortcuts for navigation and control in the spirit of Vim.
wxIF
View the EXIF/IPTC/XMP data for images.
YouTube Anti Translate Updated
A small extension to disable YT video titles autotranslation.
机器翻译太难懂啦。Google 从来都意识不到人是可以会多种语言的。

移动版

由于获取方式的差异,这个列表没有扩展描述。不过大部分都和桌面版是重复的。

篡改猴
我在访问哪个 Cloudflare® 数据中心?
ClearURLs
Control Panel for YouTube
Cookie Quick Manager
Dark Reader
Decentraleyes
Google Search Fixer
Header Editor
Push to Kindle
Stylus
Text Reflow WE
uBlacklist
uBlock Origin
Unpaywall
Video Background Play Fix
在后台继续播放视频和音频,可以用于在后台播放 YouTube Music。
Web Archives
打开当前页面的存档页面。移动版没有右键菜单所以用不了 Redirect Link。

代码

桌面版的列表是在 about:addons 页面,打开 devtools 执行以下代码取得的:

const r = $$('addon-card').map(
  (el) => {
    return {
      title: el.querySelector('h3').textContent,
      desc: el.querySelector('.addon-description').textContent,
      id: el.getAttribute('addon-id'),
    }
  }
)

let parts = []
for(let ext of r) {
  parts.push(`<dt><a href="https://addons.mozilla.org/firefox/addon/${encodeURIComponent(ext.id)}/">${ext.title}</a></dt>\n<dd>${ext.desc}</dd>`)
}

console.log(parts.join('\n'))

而移动版是在 about:debugging 页面,连接上移动版火狐之后,执行以下代码获取的:

const r = $$('[data-qa-target-type="extension"]').map(
  (el) => {
    return {
      title: el.querySelector('[title]').title,
      id: el.querySelector('dd').textContent,
    }
  }
)

let parts = []
for(let ext of r) {
  parts.push(`<dt><a href="https://addons.mozilla.org/android/addon/${encodeURIComponent(ext.id)}/">${ext.title}</a></dt>`)
}

console.log(parts.join('\n'))
🔲 ⭐

使用 PipeWire 实现自动应用均衡器

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

之前我写过一篇文章,讲述我使用 EasyEffects 的均衡器来调整 Bose 音箱的音效。最近读者 RNE 留言说可以直接通过 PipeWire 实现,于是前几天我实现了一下。

先说一下换了之后的体验。相比于 EasyEffects,使用 PipeWire 实现此功能获得了以下好处:

  • 少用一个软件(虽然并没有多大)。
  • 不依赖图形界面。EasyEffects 没有图形界面是启动不了的。
  • 占用内存少。EasyEffects 有时候会占用很多内存,不知道是什么问题。
  • 自己实现的切换逻辑,更符合自己的需求。EasyEffects 只能针对指定设备加载指定的配置,不能指定未知设备加载什么配置。因此,当我的内置扬声器名称在「alsa_output.pci-0000_00_1f.3.analog-stereo」、「alsa_output.pci-0000_00_1f.3.7.analog-stereo」或者「alsa_output.pci-0000_00_1f.3.13.analog-stereo」等之间变化时,我需要一个个名称地指定要加载的配置。
  • 只要打开 pavucontrol 就能确认均衡器是否被应用了。EasyEffects 需要按两下Shift-Tab和空格(或者找找鼠标)来切换界面。

缺点嘛,就是我偶尔使用的「自动增益」功能没啦。不过自动增益的效果并不太好,我都是手动按需开关的。没了就没了吧。

配置方法

首先要定义均衡器。创建「~/.config/pipewire/pipewire.conf.d/bose-eq.conf」文件,按《Linux好声音(干净的均衡器)》一文的方式把均衡器定义写进去就好了。我的文件见 GitHub

然后需要在合适的时候使用这个均衡器。实际上上述配置加载之后,PipeWire 里就会多出来一对名叫「Bose Equalizer Sink」的设备,一个 source 一个 sink。把 source 接到音箱,播放声音的程序接到 sink,就用上了。别问我为什么 source 的名字也是「Sink」,我不会分开定义它们的名字……

自动化应用使用的是 WirePlumber 脚本。它应该放在「~/.local/share/wireplumber/scripts」里,但是我为了方便放到 dotconfig 仓库里管理,在这里放了个到「~/.config/wireplumber/scripts」的软链接。脚本干的事情很简单:在选择输出设备的时候,看看当前默认设备是不是 Bose 音箱;如果是,就选择之前定义的「Bose Equalizer Sink」作为输出目标。不过因为文档匮乏,为了干成这件事花了我不少精力,翻看了不少 WirePlumber 自带脚本和源码。最终的脚本也在 GitHub 上

结语

PipeWire 挺强大的,就是文档太「瘦弱」啦。能用脚本配置的软件都很棒~

再次感谢读者 RNE 的留言~

☑️ ☆

如果你发现你的 OOM Killer 在乱杀进程

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

请对 systemd 做如下设置,避免它将你的用户进程调整为更容易被杀:

建立 /etc/systemd/user.conf.d/oom.conf 文件,并写入:

[Manager]
DefaultOOMScoreAdjust=0

建立 /etc/systemd/system/user@.service.d/resources.conf 文件(及其中间目录),并写入:

[Service]
OOMScoreAdjust=0

systemd 的默认设置也会干扰火狐浏览器自己的设定,造成与预期相反的行为。

 

🔲 ☆

使用 atuin 管理 shell 命令历史

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

atuin 是最近在群里看到的工具。功能和我自己用 skim 糊的脚本一样,搜索并执行 shell 的命令历史用的。但是,它的数据存储使用的是 SQLite3,并且它是使用 Rust 编程语言编写的。于是事情有了一些好的变化。

首先,因为 atuin 并不像 Web 服务那样,会持续打开并操作数据库,所以 SQLite3 并发容易报错的问题并不需要担心。而 atuin 会记录执行时间、耗时、工作目录和退出码等信息。更多的元信息,能给之后的搜索和分析提供更多帮助。

其次,因为搜索走的数据库查询,因此并不需要像我用 skim 那样,每次把全部历史加载到内存。这样就可以保留更多的历史记录而不用怕越用越慢了。不知道 SQLite3 的搜索功能效率如何,但我几万条记录,已经可以明显感觉到加载耗时的差异了。

最后,它是 Rust 写的——这点很重要,因为这大大地方便了我对它进行修改(而不像某 Zig 写的工具,我翻了半天文档都没改对最后只好放弃了)。

当然,让我没多犹豫就决定尝试 atuin 的最重要的原因是:它独立于 shell 原本的历史记录功能,并不会取而代之。所以它要是不合我意,我只要把它删掉就好了,原本的历史记录丝毫不受影响。

于是我到现在已经用半个月了,结果非常满意。不过也已经对它做了好多修改了。比较重要的如下:

  1. 支持 Shift+Del 键删除记录。有时候会不小心打错命令。这种命令记在历史里时不时被翻出来,不但占用显示空间,还容易不小心选错然后相同的错误又犯一遍,甚至因为没看清命令细节而不小心删掉好多文件(还好我有快照)。atuin 的作者最近也加了删除功能,但是是在另一个界面操作,对于我这种经常在找命令的时候要删除多条命令的用法来说,不光麻烦,而且上下文切换的代价很大,会忘记自己原本是要干什么的。

  2. 精确匹配模式,这是 skim 的叫法。你叫它多子字符串匹配也行。自从 fzf 流行以来,大家都迷上了子序列匹配的所谓「模糊匹配(fuzzy match)」。但是我不喜欢这种匹配方法,会给出太多不相关的结果,加大脑力负担。真正好的模糊匹配是 agrep 那种基于编辑距离的算法,打错点字没关系那种。所以我给 atuin 也加上了精确匹配模式,同时还提升了查询性能呢。

  3. 反转 UI 的 --invert 命令行选项。像我之前使用 skim 那样,当光标位于终端窗口的上半部分时,我希望我搜索时打字的地方在上方;而当光标来到终端窗口的下半部分时,搜索时光标也放下边。这样关注的焦点就不会跳很远,有连贯性,节省认知脑力。atuin 本身有反转 UI 的功能,但是是写在配置文件里的,而我需要视情况决定要不要反转 UI,所以还是得加个命令行选项。

  4. 更改了选中项的颜色。atuin 原本用的是红色,我总觉得是哪里有报错……

还有些不太重要的修改,可以来我的分支查看:https://github.com/lilydjwg/atuin/commits/lilydjwg。注意这个分支我会经常 rebase 的。

另外我修改过的 zsh 插件在 https://github.com/lilydjwg/dotzsh/blob/master/plugins/atuin.zsh

值得一提的是,atuin 还支持同步。同步的数据本身是加密的,但是还是会泄露一堆诸如什么时候在跑命令之类的元数据,所以我自己跑了个服务。服务很好跑,但是同步似乎有些问题。我两个系统,两边都导入了之前的历史记录并同步,但是后同步的那个系统上的历史,很难被同步过去。甚至 atuin 发现本地比远程多,就从最新开始慢慢上传,直到两边一样多;如果远程比本地多,那就把远程的删掉一些(我也不知道它删了哪些,我是看到访问日志里巨量的 DELETE 请求才意识到问题的)。总之经过我不懈地反复运行,最终它只比远程多几条记录了,并且绝大部分历史记录已经两边都有了。我猜它可能没想到我会从不同的系统上同步已有且已分歧的数据吧……

☑️ ☆

btrfs 元数据满了怎么办

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

上一篇《btrfs 翻车记》记叙了我们服务器上的 btrfs 出事的情况,好像吓到一些用户了 QAQ。其实那次情况比较特殊啦。一般来说,就算元数据用满了,也不至于改内核代码才能救回来。不过元数据满的问题确实困扰了许多用户,正好这些天群里有不少人遇到了,本文就记录一下元数据满了之后如何处置。

问题和处置

问题的现象是部分文件操作报错「No space left on device」,但是 df 等工具明明报告还有空间。btrfs filesystem usage 的输出是这样:

btrfs filesystem usage 截图

我们可以看到,还有 373G 的空闲空间(Free)呢。但是呢,「Device unallocated」已经不足 1G 了。在充分大的文件系统上,btrfs 会以 1G 为单位来分配块组(block group,简称 bg)。所以现在这个情况,已经无法分配新的 bg 啦。然后我们再往下看,「Metadata」的部分,总共 4.5G,已经用了 4G。还剩下 512M,刚好是 Global reserve 的大小。也就是说,不算保留空间的话,元数据已经没有空间可用了,所以才会报错。

那现在怎么办呢?如果文件系统上有不需要的很大的文件,并且没有快照,删除后空间可以立即释放的话,可以删除试试,看看能不能刚好空出来一个 bg。不然就试着跑一下 balance,像这样:

btrfs balance -dusage=0 截图

这个命令是说,把使用率为 0% 的数据 bg 整理一下。从输出「had to relocate 0 out of ...」可以看出,没有这样的 bg,操作没有效果。可以试试增加 -dusage 的值,看看能不能成功。很遗憾,这个案例中未能成功:

btrfs balance -dusage=1 截图

那只有另外添加一些空间来腾挪数据了。如果有闲置的分区(或者暂时用不上的 swap 分区)就可以拿来用。不然的话插个U盘也行。不需要多大,几个 G 就行。挪好数据就可以去掉了,不会长期使用的。另一种很有风险的做法是使用内存来暂存数据,但这样一旦死机或者断电,整个文件系统就完蛋了,不建议使用。

准备好空闲分区后,就可以 btrfs device add 添加上去了。这里通常要加 -f 参数,抹除分区里原有的数据。注意不要添加错设备了哦。

btrfs device add 截图

可以看到,设备添加上去之后,又可以往文件系统里写数据了。接下来跑 btrfs balance -dusage=10 之类的命令腾些数据 bg 出来就好了。这里要注意只 balance 数据 bg,不要动元数据的 bg,因为元数据越是集中存放,将来就越可能需要分配新的 bg,就越有可能遇到没 bg 可以分配的情况。

btrfs-heatmap 工具可以查看 bg 的分布和使用情况。以下是 balance 好之后的状态(使用 --curve linear 参数):

btrfs heatmap

图中,白色的是数据 bg,蓝色的是元数据 bg。颜色越亮,使用率越高。纯黑的是未分配空间。可以看到,这里有大量用得不多的数据 bg。balance 操作就是把它们给合并了一些,空出来不少黑色区域(最下方的黑色部分是新添加的设备上的未分配空间)。这是 usage 截图:

btrfs filesystem usage 截图

算一算,除去新添加设备的空间,原存储设备上也能有 46G 的未分配空间了(看上去新添加设备并没开始使用,只是让 btrfs 相信它有足够的空间用)。接下来把之前添加的设备删除就可以了:btrfs device del /dev/sdb1 /。等它运行完毕就可以拔掉该设备了(如果是可移动介质的话)。

预防

这个问题的本质就是 bg 的碎片化导致明明看上去有空间,但是元数据用不了,因此报错,需要手动处理。要识别即将出问题的文件系统也很简单:btrfs filesystem usage 看一看,如果「unallocated」很小(不足 1G)就要赶紧 balance 一下了(当然前提是有不少碎片化的空闲空间)。注意,这个时候千万不要删快照!删快照可能会快速消耗保留的元数据空间,从而导致添加设备都加不上、还报错只读的情况。

btrfs 最近添加了自动块组回收(automatic block group reclaim)功能,但默认并没有启用。因为是新功能,可能会有 bug,你也不知道它会什么时候运行,所以我暂时不建议使用。自己写个定时脚本,在系统空闲的时候运行也不错的。


本文中的图像素材均由遇到问题的群友提供。

☑️ ☆

btrfs 翻车记

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

如标题所言,我用了多年的 btrfs,终于还是遇到翻车啦。由于文件系统翻车了,相关日志内容缺失,所以本文我仅凭记忆描述事件,就不提供准确的日志输出了。

事件经过

出事的是 archlinuxcn 的编译机。那天中午时分我就收到了 Grafana 给我发送的莫名其妙的报警邮件,称某个监控项无数据了。我去面板上瞅了半天,明明有数据的啊。不解,但是忙别的事情去了,也没有细究。晚些时候,我又收到了好些同类告警,遂登录机器打算检查 Grafana 日志。但操作过程中,退出 zsh 的时候我好像看到了写命令历史时出现「read-only filesystem」的字样?于是检查了一下,发生大事不好了,文件系统真变只读了!

这个 btrfs 是我上次迁移机器的时候换上的,因为我觉得过了这么多年,btrfs 挺稳定了啊,而定时快照很方便,devtools 也支持通过快照来快速创建打包用的 rootfs(虽然大部分时候都在 tmpfs 上打包了用不上)。但我们编译机一直以来有个问题:硬盘有多少用多少。前一任编译机用的 1T 硬盘,是刚刚够用,现在换 2T 了,结果用着用着又只剩下200多GiB啦……肥猫最近开始玩 bees 去重了,听说在其它机器上效果显著。不过这台编译机快照多,这「蜂群」嗡嗡了几天都没完事,还出事了。

btrfs filesystem usage 一看就发现,空闲200多G数据是真的(因此没有触发相关报警),但是元数据满了,也没有未分配空间了。这种事群里有多位群友遇到过了,问题不大,加个设备再 balance 一下就好了。我一开始是这么想的,刚好有两个挺大的 swap 分区能够用来腾挪。结果 btrfs 告诉我,文件系统只读,添加设备失败!那好,我 remount rw 一下。结果 btrfs 说文件系统出毛病了,不支持 remount rw!

这个时候我才感觉事情有点难办了。这个 btrfs 文件系统是 /,并不能卸载啊。没有找到在线修理的办法,只好呼叫凤凰卷,通过 iDRAC 进入 archiso。期间服务器重启了一次,但连过去依旧是只读的。进到 archiso 之后,尝试抢在报错之前添加设备,但是并没有成功。会卡住一会儿,然后报错「No space left on device」。按 farseerfc 的建议,clear_cache 和 zero-log 都试过了,但并没有解决问题。有人建议把大文件 truncate 一下,看看能不能刚好释放出 1G 的连续空间出来,但是我有定时快照呀,truncate 了也不会立即释放空间。最后卷直接下单了新机器,开始 btrfs send……

事后

服务器迁移还比较顺利。数据接收完毕,网络配置更新一下,引导器装好,重启,熟悉的编译机就回来啦~除了 nvchecker 好像跑得有点慢?怎么 ping Google 要 60ms 的?原来是忘记更新 /etc/resolv.conf 了,里边还写着旧 ISP 的 DNS 服务器地址呢。systemd-resolved 这次做了回好事,把 DNS 服务 fallback 到了 9.9.9.9。DNS 解析慢是 fallback 过程造成的,而 ping Google 延迟高,是因为 9.9.9.9 不知道怎么回事,给解析到比较远的地方去了。

新编译机 CPU 比之前那台快了不少,硬盘也增加到了 3.4T。挺好的,除了这时机不太好,旧编译机还有近一个月到期……另外由于是突发状况,所以没有及时缩短 DNS TTL,导致迁移完成之后 DNS 解析没有及时跟上(隔天我陆陆续续从另一台使用这台编译机转发邮件的机器那里收到了好些邮件,都是抱怨这编译机连不上的)。

蜂群(bees)也重新开始工作了。这次快照较少,我还专门为了它们暂停过自动快照,过了一段时间之后首次扫描终于完成了。之后它们就能很快跟上进度,不会消耗大量 CPU 了。

我添加了定时任务来执行 btrfs balance start -dusage=10 /,每周释放一些使用率低的数据块组,避免空间分配了又不怎么用,到最后明明有剩余空间却让元数据无处可写。

farseerfc 对出事的 btrfs 进行了更多不同方案的修复尝试,但依然未能修好。

一些抱怨

没想到我用了这么多年的 btrfs,还是被坑到了。明明还有不少空闲空间,但是 btrfs 不知道用。我看到最近有个「automatic block group reclaim」特性,支持自动回收块组了,但是搜索结果第一项结果是今年年初有人在邮件列表上报告说它有问题……出现问题 ro 挺好的,但是这个状态下不让进行维护操作就太难受了。作为 / 文件系统使用时,对于远程机器来说,即使有 iDRAC 或者 IPMI 之类的东西,用起来也费事,还不得不中断可能还活着的服务。而对于不支持远程访问的机器就更麻烦了,比如在家办公时办公室的机器,或者出差旅游探亲时在家的机器。我也考虑过在 initramfs 里配网络、开 sshd,但是并没有现成的工具,事发时再配的话,一次性成功的可能性太低了。

至于事发原因,蜂群(bees)只是加快了元数据空间的使用(dedupe 快照的结果),其本身并没有问题。出事重启之后,在再次被挂载为只读之前,还是写入了不少数据,包括一次成功的快照(后来查 pacman 数据库损坏的问题时发现的)。这可能是后续添加设备都无法成功的原因。

以前用的 ext4,在文件系统快满时只是碎片化严重、效率降低,它甚至还会给 root 保留一部分空间来处理问题。后来用 zfs,快满了就 0B/s,等于废掉。现在 btrfs 遇到空间不足也没有好太多,变只读了。(我还打算抱怨一下新文件系统可靠性不如旧的来着,想想前不久在群里看到 btrfs 抓到了位反转,还是不抱怨了。大家各有千秋。)


2023年07月08日更新:farseerfc 把它救活了!核心方法是把这里的 global reserve 大小由 512M 改成 2G。因为之前重启了一次,那时不仅成功创建了一个新快照,还删掉了一个旧的。然后它删着删着就把 512M 的 global reserve 给用完了,就报错、事务回滚,于是就过不去了。和邮件列表上这个问题是一样的:Global reserve and ENOSPC while deleting snapshots on 5.0.9 — Linux BTRFS

2023年07月25日更新:其实本文所述内容是罕见情况啦,并没有多少人会遇到的,大家不用害怕。另外新写了一篇《btrfs 元数据满了怎么办》,记录大多数人遇到的元数据满的问题如何解决。

☑️ ☆

在 nspawn 里运行 docker

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

有个服务器需要维护,因此需要将其上的所有服务暂时迁移走。打算直接扔到另一台比较闲的服务器上,直接拿 systemd-nspawn 跑起来得了。简单方便,除了网络之外不需要额外配置。但问题是,这些服务里包含一个使用 docker 运行的 ElasticSearch,在同为容器的 nspawn 里跑会有问题吗?

试了一下,还真有的问题。dockerd 会报权限错误而跑不起来。但稍微搜一下就找到了解决方案:

SYSTEMD_SECCOMP=0 systemd-nspawn --capability=all --network-bridge=br0 --boot -D rootfs

nspawn 默认会限制一些权限。这样可以让其不做任何限制,相当于 docker 的 --privileged 参数。然后就可以嵌套着跑啦。

跑起来之后检查一下,所有服务均正常运作了,没有任何问题。把网络配好,外边的 nginx 负责一下转发(因为懒所以没配外网 IP),就可以接替工作啦。

PS: 迁移的过程中遇到了一个小坑。rsync 不加 --numeric-ids 的话会尽量保持用户名不变,等里边的系统跑起来就各种权限问题了。所以转移 rootfs 的时候一定得记着加上 --numeric-ids

☑️ ☆

Linux 上的字体配置与故障排除

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

常见汉字字体

电脑系统要显示字,首先得有字体。现在 Linux 上常用的、在维护的开源中文字体就一套,同时被 Noto思源两个项目收录。Noto 系列字体是 Google 主导的,名字的含义是「没有豆腐」(no tofu),因为缺字时显示的方框或者方框被叫作「tofu」。思源系列字体是 Adobe 主导的。其中汉字部分被称为「思源黑体」和「思源宋体」,是由这两家公司共同开发的,两个字体系列的汉字部分是一样的。

Noto 字体在 Arch Linux 上位于以下软件包中:

  • noto-fonts: 大部分文字的常见样式,不包含汉字
  • noto-fonts-cjk: 汉字部分
  • noto-fonts-emoji: 彩色的表情符号字体
  • noto-fonts-extra: 提供额外的字重和宽度变种

Noto 系列字族名只支持英文,命名规则是 Noto + Sans 或 Serif + 文字名称。其中汉字部分叫 Noto Sans/Serif CJK SC/TC/HK/JP/KR,最后一个词是地区变种。

思源系列则有:

  • adobe-source-sans-fonts: 无衬线字体,不含汉字。字族名叫 Source Sans 3 和 Source Sans Pro,以及带字重的变体,加上 Source Sans 3 VF
  • adobe-source-serif-fonts: 衬线字体,不含汉字。字族名叫 Source Code Pro,以及带字重的变体
  • adobe-source-code-pro-fonts: 等宽字体,不含汉字。字族名叫 Source Code Pro,以及带字重的变体,加上 Source Code Variable。
  • adobe-source-han-{sans,serif,mono}-{cn,hk,jp,kr,tw}-fonts: 五个地区的汉字之黑体、宋体和等宽版本
  • adobe-source-han-{sans,serif,mono}-otc-fonts: 所有地区合体了的汉字之黑体、宋体和等宽版本

其中等宽版本的中文字体位于 [archlinuxcn] 仓库中。

思源汉字字体的字族名有两种,「独立包装」的版本(非 OTC 版本),是「Source Han Sans/Serif」或本地化名称、空格、地区代码(CN/HK/TW/JP/KR)。比如「思源黑体 CN」、「源ノ角ゴシック JP」等。也有带字重的别名。

而全部打包的 OTC 版本,字族名是本地化名称或者英文的「Source Han Sans/Serif」空格再加上「HC/TC/HC/K」变种代码。如果没有变种代码,则是日文变种。为了区分,香港繁体的版本附带「香港」字样,比如黑体叫「思源黑體 香港」。这些字体也有不同字重的别名。另外有个半宽的版本,是在字族名的变种代码前加「HW」字样,仅有少数几个字符是半宽的。

OTC 版本有趣的地方在于,对于大多数软件来说,不管你叫它的哪个地区的名字,它都会以设定的语种来显示。比如网页声明语种为日文(<html lang=ja>),那么不管字体指定为「源ノ角ゴシック」还是「思源黑体」或者「본고딕」,它都会「门上插刀、直字拐弯、天顶加盖、船顶漏雨」。所以用这个字体的话,不妨一律写「Source Han Sans」,然后加好语种标记。我知道的唯一例外是 mpv 的 ass 字幕文件,里边指定本地化名称的话,会使用那个语种的变体显示。

早些年还没有 Noto 和思源的时候,Linux 系统上通常使用文泉驿正黑或者文泉驿微米黑。后者是基于 Android 系统上的 Droid Sans Fallback 字体,体积较小。再之前是文鼎系列字体,也就是名字「AR PL」开头、包名叫 ttf-arphic-{uming,ukai} 的那些。

字体的属性

字体有很多属性,常用的有字族(family)、倾斜(slant)、字重(weight)。后两者合一起叫样式(style)。

字族就是它的名字啦。常见的指代字体的方式除了字族之外还有 Postscript 名,它不含空格、使用短横线将样式附加在名称之后,比如「DejaVuSans-BoldOblique」。后者是 CSS @font-face 规则中使用 local唯一指定样式的方法(除非该字体把样式也写到了字族名里)。

倾斜就是斜不斜,英文叫「Roman」「Italic」或者「Oblique」,Italic 是专门的斜体写法(更接近手写样式), Oblique 是把常规写法倾斜一下完事。

字重就更简单了,就是笔划的粗细。常见的有 Regular、Normal、Medium、Bold、Semibold、Black、Thin、Light、Extralight 等。

详细信息可以 man 5 fonts-conf 查询。

通用字族名

很多时候,程序并不在乎用户具体使用的是哪款字体,像很多网站的 CSS 那样把各个平台的常见字体全部列出来太傻了,又容易出问题。所以,人们发明了「通用字族名」,也就是 sans-serif (sans)、serif 和 monospace (mono) 这些。中文分别叫无衬线字体、衬线字体和等宽字体。但是中文字体不讲衬线不衬线的,而是叫「黑体」和「宋体」(有些地区叫「明体」)。黑体常用于屏幕显示的正文,而宋体常用于印刷文本的正文。

另外,中文没有斜体。英文使用斜体的场合,中文通常是使用仿宋或者楷体。中文本也没有粗体。传统上,强调的时候,中文使用着重号,也就是在字的下方或者右方加点,像这样子(如果你看到的着重号在文字上方,那是因为你用的 Chrome/Chromium 浏览器不听页面指示,执意将它作为日文处理了)。

最近有一个新加的通用字族名叫作「emoji」。Pango 渲染表情符号的文本时,会自动使用 emoji 字体。但是 Qt 尚不支持,导致有时会出问题,而将 emoji 字体排到常规字体之前的做法,又会导致数字和空格显示为全角。火狐自带了一个 SVG 格式的 emoji 字体,会自动使用。很多软件(比如 Telegram)也会使用图片来取代 emoji 字符。

CSS 4 又加了一套 ui- 开头的字族名但是除了 Safari 没浏览器支持。fontconfig 倒是可以通过配置来支持上,但是由于火狐的一个 bug 导致 ui-sans-serif 无效。

fontconfig 配置

大部分 Linux 桌面软件都或多或少地使用 fontconfig 来获取字体配置信息。其中 Pango(GTK 使用的文字渲染库)的支持是最好的。很多简陋的图形界面库则只用来读取默认字体,可能完全不支持字体回落,造成部分文字明明有字体却显示为「豆腐」。

了解了通用字族名,我们就可以为它们指定我们喜欢的字体啦。在 ~/.config/fontconfig/fonts.conf 里为每一个通用字族名像这样写即可:

  <match target="pattern">
    <test qual="any" name="family">
      <string>sans-serif</string>
    </test>
    <edit name="family" mode="prepend" binding="strong">
      <string>DejaVu Sans</string>
      <string>文泉驿正黑</string>
      <string>Twemoji</string>
      <string>Font Awesome 6 Free</string>
      <string>Font Awesome 6 Brands</string>
      <string>Source Han Sans</string>
    </edit>
  </match>

因为我并没有完全采用思源字体来显示汉字,所以我还是为不同语言和地区变种分别匹配了不同的字体。我完整的配置文件见:https://github.com/lilydjwg/dotconfig/tree/master/fontconfig。其中,web-ui-fonts.conf 文件用于提供 CSS 4 新增的字族名,而 source-han-for-noto-cjk.conf 则使用思源系列字体来代替 Noto CJK 系列字体。

查看浏览器使用的字体

排查字体问题时,一个常见的要知道的事实是,软件究竟在用什么字体来显示这些文本?想知道这个通常很难,但是对浏览器来说却很简单。所以字体匹配问题首先看浏览器能不能复现。

火狐浏览器,对着有疑问的字点右键,选择「检查」(也可以按 Q 键),然后看弹出的开发者工具右边的「字体」选项卡即可。鼠标悬停到下方灰色的字体名上时还能将使用该字体的字高亮显示。

在火狐中查看网页所使用的字体

Google Chrome 浏览器及其变种类似,对着有疑问的字点右键,选择「检查」(也可以按 N 键),然后看弹出的开发者工具右边的「计算样式」选项卡,拖动到最下面,可以看到使用的字体名以及有多少个字形。

在 Google Chrome 中查看网页所使用的字体

至于这个字体是怎么选上的,可以切换到「规则」(火狐)或者「样式」(Google Chrome)选项卡来看 CSS 规则。搜索「font-family」看看具体被应用上的规则是哪一条。通常这里会写上一大排字体名。火狐会将正在使用的那个加上下划线,但是有时候不准确(比如该 HTML 元素使用了多种字体)。更好的除错方法是,从头到尾一个个删字体,删到哪一个时网页上的字体变动了,就说明在使用的是哪一个。我通过这种方式找出了好些我学生时代不懂事从 Windows 下复制过来的字体导致的问题。

Google Chrome 默认的字体比较奇怪,是「Times New Roman」、「Arial」和「Monospace」。见《Google Chrome 中的字体设置》一文。

Qt

https://z.sh/qtfontbugs。其中最著名的 bug 是 QTBUG-80434 (https://z.sh/434)。

小技巧

使用 gucharmap 软件可以检查所有字符使用指定的字体时的渲染效果,以及它回落到什么字体上了。找到要查看的字符,然后对着它按住右键即可。

使用 fc-match -s NAME:charset=HHHH 可以查看针对指定字符的字体优先顺序,包含这个字符的字体会优先。如果不加 -s 就是看指定的模式会匹配上的字体了。其中 HHHH 是该字符的 Unicode 码点之十六进制值。如 fc-match :charset=7684 查看默认字体下「的」字会用什么字体,而 fc-match serif:charset=7684:lang=ja 查看在语种为日文的时候,使用 serif 字族名会使用哪个字体来显示「的」字。使用 fc-list :charset=HHHH 则是查看包含该字符的所有字体。

参考资料

❌