10
24
2024
1

为团队部署邮件服务

给服务器上的程序部署邮件服务十分简单,装个 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」。把垃圾邮件通过命令行手动喂给它又会提示已经学过该邮件了。

Category: Linux | Tags: linux mail
8
27
2024
24

使用 nftables 屏蔽大量 IP

本来我是用 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')

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

8
23
2024
8

YubiKey 初体验

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,有点复杂。暂时不想研究。

Category: 硬件 | Tags: 硬件 linux 安全 外部设备
6
22
2024
12

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

之前我写过一篇文章,讲述我使用 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 的留言~

Category: Linux | Tags: linux 音频 外部设备 lua
1
23
2024
3

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

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

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

[Manager]
DefaultOOMScoreAdjust=0

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

[Service]
OOMScoreAdjust=0

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

 

Category: Linux | Tags: linux systemd
1
13
2024
12

使用 atuin 管理 shell 命令历史

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 请求才意识到问题的)。总之经过我不懈地反复运行,最终它只比远程多几条记录了,并且绝大部分历史记录已经两边都有了。我猜它可能没想到我会从不同的系统上同步已有且已分歧的数据吧……

Category: shell | Tags: linux shell Rust zsh atuin
7
25
2023
2

btrfs 元数据满了怎么办

上一篇《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,你也不知道它会什么时候运行,所以我暂时不建议使用。自己写个定时脚本,在系统空闲的时候运行也不错的。


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

Category: Linux | Tags: linux btrfs
7
6
2023
8

btrfs 翻车记

如标题所言,我用了多年的 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 元数据满了怎么办》,记录大多数人遇到的元数据满的问题如何解决。

Category: Linux | Tags: linux btrfs
6
29
2023
2

在 nspawn 里运行 docker

有个服务器需要维护,因此需要将其上的所有服务暂时迁移走。打算直接扔到另一台比较闲的服务器上,直接拿 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

Category: Linux | Tags: linux systemd docker

Mastodon | Theme: Aeros 2.0 by TheBuckmaker.com