12
11
2024
2

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

我喜欢用本地文件听歌:没有广告、没有延迟、没有厂商锁定。但是有个问题:有的歌曲文件音量挺大的,比如 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 ::: 文件们 就好了嘛……

Category: Linux | Tags: linux 音频
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 安全 外部设备
7
9
2024
20

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

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

桌面版

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

篡改猴
使用用户脚本自由地改变网络
复制链接/标签名称和地址
将链接名称和地址复制到剪贴板
复制链接地址
使用快捷键 "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'))
Category: 火狐 | Tags: web 火狐 WebExtensions
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

Mastodon | Theme: Aeros 2.0 by TheBuckmaker.com