qfhl.vim を作った

quickfix は便利ですが、それがバッファ内のどの位置を指しているのか、視覚的には分かりづらいです。

これを解決するための vim-hier というプラグインがあります。このプラグインは、quickfix の位置をハイライトで視覚的に表示してくれます。 実用には十分ではあるのですが、長らくメンテされておらず、またプラグインの構造やハイライトの仕組みが古いことが気になっていました。

一方、現在の Vim には text property という仕組みがあります。これを使えば、いい感じのものが作れそうな気がしました。

というわけで作ってみました。

github.com

(作ってみましたと軽く言ってますが、放置期間も含めると数年越しのリリースです…。)

qfhl.vim は、quickfix および location list の場所をバッファ上でハイライトします。

ハイライトには text property を使用しています。text property はバッファの一部に埋め込まれ、バッファ編集に合わせて位置が移動します。 これにより、ハイライト後にバッファを編集して行がずれたりした場合でも、ハイライトの位置も適切に移動します。

quickfix には行と桁と言う位置情報があり、それを元にハイライトを行っていますが、これだけだとどこまでハイライトをすればいいのかが判断できません。

これは不便なので、Vim 本体に PR を出しました。

github.com

quickfix が持つ位置情報に、オプショナルな終了位置の行と桁の情報を持たせられるようにする変更です。この PR は無事 v8.2.3019 で取り込まれました。

と言うわけで、もし quickfix が終了位置を持っていた場合は qfhl.vim も適切な範囲をハイライトします。 とは言うものの、quickfix を設定する多くの grep 系のコマンドやプラグインは、まだ終了位置を設定してくれないでしょう。終了位置がない場合は、仕方ないので行末までハイライトをします。

現時点で手軽に試すには :vimgrep コマンドが使えます。:vimgrep は組み込みコマンドだったので、上記の PR で終了位置を設定するように対応しています。

f:id:thinca:20210731135546p:plain
qfhl でハイライトを行った様子

最後に注意点ですが、Vim script の新しめの機能を使って書いているため、恐らく Neovim では動きません…。

以上、もし需要が合う方はお試しください。

Btrfs メモ

Btrfs についていろいろ調べたので使い方のメモ。

参考ページ:

このメモの内容はほとんど参考ページのままなので、そちらを見た方がよさそう。

Copy on Write

Btrfs では CoW (Copy on Write) が使える。ファイルを単にコピーしただけでは容量を消費せず、ファイルを書き換えた際に初めてディスク領域を消費する。

ただしこれはただ cp しただけではダメで、cp --reflink=always(常に CoW) や cp --reflink=auto(可能なら CoW) を使う必要がある。

そのため、私は以下のようなラッパコマンドを用意して PATH に置いた。

#!/bin/sh -
exec /bin/cp --reflink=auto "$@"

シェルの alias を使う手も考えられるが、コマンドごと置き換えることで例えば make install 時の cp の挙動も変えられるので、コマンド自体を上書きすることにした。

無効化

逆に CoW を無効にしたい場合もある。データベースが扱うファイルのように、1つのバイナリファイルを何度も上書きするような使い方をする場合、CoW と相性が悪いらしい。

ディレクトリやファイル単位で CoW を無効にする場合は以下のように chattr を使う。

$ chattr +C </dir/file>

また、マウント時に nodatacow オプションを指定することで対象を丸ごと CoW 無効にできる。後述する subvolume のマウントの際などに使える。

圧縮

透過的に圧縮ができる。zliblzozstd を選択できる。今だと lzozstd を選んでおくのが良さそう。ここベンチマークの結果がある。

マウントオプションで compress=lzocompress=zstd を指定する。compress=zstd:3 のようにすることで、圧縮レベルも指定できる。zstd の場合は 1 から 15 で、デフォルトは 3。

また、一緒に space_cache も指定するとよい。これは新規の割り当て領域を毎回探すのではなく、予めある程度キャッシュしてくれる。

圧縮は、圧縮を有効にした後に作成されたファイルにしか適用されない。既存のファイルにも適用し直すには以下のコマンドを使う。

$ btrfs filesystem defragment -r -v -czstd /

これは / から辿って再帰的(-r)に処理をするが、後述の subvolume や別の FS をマウントしている箇所等には適用されないので注意。

また、後述する snapshot を作っている場合も注意。全部のファイルを書き出し直しているのと同じになるので、snapshot とファイルを共有できずに容量を食ってしまう。

subvolume

ファイルツリーの単位として subvolume というものを作れる。見た目は普通のディレクトリのように見える。

$ btrfs subvolume create my-subvolume
Create subvolume './my-subvolume'

$ ls
my-subvolume

snapshot

subvolume 単位で snapshot を取れる。一瞬で作れる。

$ btrfs subvolume snapshot my-subvolume my-snapshot
Create a snapshot of 'my-subvolume' in './my-snapshot'

$ ls -1
my-snapshot
my-subvolume

CoW により、余計な容量を取らない。お手軽なバックアップに使える。

-r オプションを使うことで、read only な snapshot を作ることもできる。せっかく取ったバックアップを誤って書き換えてしまわないようにできるので便利だ。read only かどうかは後から変更もできる。

subvolume は入れ子にすることができるが、その場合、それぞれ独立した subvolume ということになる。snapshot を作った場合、入れ子になった subvolume までは snapshot の対象にはならない。

$ btrfs subvolume create my-subvolume/nested-subvolume
Create subvolume 'my-subvolume/nested-subvolume'

$ echo file1 > my-subvolume/file1.txt
$ echo file2 > my-subvolume/nested-subvolume/file2.txt
$ btrfs subvolume snapshot my-subvolume my-snapshot2
Create a snapshot of 'my-subvolume' in './my-snapshot2'

$ ls -1 my-snapshot2
file1.txt
nested-subvolume

$ ls -1 my-snapshot2/nested-subvolume
$

ネストした subvolume があった場所には空のディレクトリがあるが、中身はない。

subvolume のマウント

subvolume は、それ自体を通常のファイルシステムのようにマウントすることができる。マウントオプションに subvol={path}subvolid={id} を与える。subvolidbtrfs subvolume list で確認できる。

これにより、構成を工夫することで、本来のファイルシステムのルートとは別のところをルートディレクトリとして使い、

subvolume + snapshot を使って簡易タイムマシーン的な構成を作る

定期的に snapshot を取って、何かあったときに古いファイルを取り出せるようにしてみる。Mac のタイムマシーン、あるいは富豪的ゴミ箱みたいなイメージ。

構成

ArchWiki の Snapper のページにある推奨ファイルシステムレイアウトをほぼそのまんま採用した。

subvolid=5
   |
   |- @ (subvol)        ← / (root) をマウント
   |  |- /.snapshots    ← /@snapshots をマウント
   |  |- /usr
   |  |- /bin
   |  |
   |  |- ...
   |  |
   |  |- /home          ← 別パーティション
   |  |
   |  `- var/
   |     |- lib/
   |     |  |- docker/  ← /@var/lib/docker をマウント
   |     |  |- mysql/   ← /@var/lib/mysql をマウント
   |     |- log/        ← /@var/log をマウント
   |     |- tmp/        ← /@var/tmp をマウント
   |
   |- @snapshots/ (subvol)
   |  |- ... (snapshot)
   |
   `- @var/
      |  |- docker/ (subvol)
      |  |- mysql/  (subvol)
      |- log/ (subvol)
      `- tmp/ (subvol)

ファイルシステムの本当のルート(subvolid=5)を直接使わず、その直下の @ をシステムのルートディレクトリとしてマウントする。

log や tmp など、バックアップ対象にしたくない箇所はそれぞれ subvolume として外に出しておき、マウントする。

特定の snapshot に戻したい場合は、ArchWiki にまんま書いてあるが、USB で別システムでブートし、subvolid=5 をマウントしたあと、@ を削除 or 退避させて戻したい snapshot の snapshot を @ に作ればよい。

/home 以下も概ね同じだが、私はすでに /home/thinca を作ってしまっており、これをまるごと subvolume に変換する必要があったので以下の手順を行った。直接変換するようなコマンドはなく、cp するしかないらしい。

# cd /home
# btrfs subvolume create new
# cp -a -p -T --reflink=always thinca/ new/
# mv thinca old
# mv new thinca

snapshot の作成

snapshot を置いておく場所は用意できたので、ここに何かがあった時や定期的に snapshot を作っていきたい。

この手のことをするプログラムは、やはりみんな欲しいらしく結構な数が作られている模様。さきほどちらっと出た Snapper もその 1 つ。

試しに Snapper を使おうとしてみたが、動かし方がよくわからず…設定ファイルを書いてみても認識してくれない。

元々大したことをやろうというわけでもないので、やりたいことをやるスクリプトを書いてみた(たぶんこういうノリで謎スクリプトが量産されたのだろう…また1つ増えてしまった…)。

https://github.com/thinca/bsnap

使い方はざっくり README に書いたが、これで、//.snapshots に、/home/thinca/home/.snapshots/thinca に、それぞれ daily, weekly, monthly で snapshot を取る設定を書いた(weekly や monthly は記事執筆時点でまだ発動してないが…)。

例えば以下は / に関する daily の設定ファイル。形式は Bash script で、単にスクリプト変数を定義しているだけである。

# スナップショットのグループ
group=daily

# スナップショットを置くディレクトリ
snapshots_dir=/.snapshots

# スナップショットを取る対象のディレクトリ
target=/

# 最新のいくつを残すか
leave_count=30

# スナップショット作成時に cleanup を同時に行う
auto_cleanup=yes

これを /etc/bsnap/root-daily に置く。同じように home-daily やら root-weekly やらを置く。

で、これを定期的に実行するために systemd の service を作る。

# /etc/systemd/system/bsnap-create@.service

[Unit]
Description=Create a snapshot by bsnap

[Service]
Type=oneshot
ExecStart=/usr/local/bin/bsnap -c %i create

[Install]
WantedBy=default.target

この service を定期的に実行するために、timer を用意する。timer は daily、weekly、monthly で動かそうと思うので、それぞれのタイミングで発動するやつ、計3つ用意する。以下は daily のもの。

# /etc/systemd/system/bsnap-create-daily@.timer

[Unit]
Description=Create a snapshot by bsnap in daily

[Timer]
OnCalendar=04:00
Unit=bsnap-create@%i.service

[Install]
WantedBy=timers.target

あとは timer を有効にすれば、定期的に snapshot を取ってくれる。設定に従って古いものは自動的に削除してくれる、はず。

# 以下を、root/home それぞれと daily/weekly/monthly それぞれで計 6 個有効にする
$ sudo systemctl start bsnap-create-daily@root-daily.timer
$ sudo systemctl enable bsnap-create-daily@root-daily.timer

rm 時に snapshot を作成

また、rm を関数で wrap して、削除前に snapshot を取るようにした。

  • 一般ユーザー向けなので $HOME 以下のファイルを消そうとした場合のみ
    • 専用の home-rm という設定を作っておく
    • その場合は -fr がなくてもディレクトリを消せるようにする
      • こうしておくと、常に -fr なしでもファイルもディレクトリも消せる。そういう癖を付けておけば、うっかりゴミ箱がない環境で手軽にディレクトリを消そうとしたときに一旦エラーで止まって一呼吸置けるようになる
  • $HOME の中と外のファイルを同時に消そうとするとまずいが、そうそうしないと思うので目を瞑る
  • それ以外にも完璧に rm を置き換えるのは関数では難しく、妥協している
if [[ -e /etc/bsnap/home-rm ]]; then
  rm() {
    local arg snap=no will_err=no paths=()
    for arg in "$@"; do
      if [[ "${arg[1]}" != "-" ]]; then
        paths+=("${arg}")
        if [[ ! -e "${arg}" ]]; then
          will_err=yes
        elif [[ "$(realpath ${arg})" =~ ^"${HOME}" ]]; then
          snap=yes
        fi
      fi
    done
    if [[ "${snap}" == "yes" && "${will_err}" == "no" ]]; then
      bsnap -c home-rm create "$(basename "${paths[1]}")"
      command rm -fr "${paths[@]}"
    else
      command rm "$@"
    fi
  }
fi

雑感

Btrfs は調べれば調べるほど色んな機能が出てきて驚かされる。

まだ試せてない機能が他にもあるので、機会をみつけて触っていきたい。

nftables の設定を書いた時のメモ

前回に引き続き、新しいマシンの環境構築をしている。

以前のマシンではファイアウォールとして、以前は iptables を使用していたが、今回 nftables に移行してみる。元々 iptables 自体、かなり昔に書いた設定を使い回していて、設定方法はすっかり忘れてしまっていた。

あらかた設定できたので、その際のメモ。

ネットワーク環境

マシンが置かれているネットワークの環境は以下のような感じ。

  • ルータの内側にいるサーバマシン
    • なのでほとんどの通信はルータの時点でシャットアウトしてもらえる
    • とは言え、可能な限りインターネットに晒されている前提の設定を目指す
  • しかし IPv6 は今回は設定しない
    • 将来的には挑戦したい…
    • プロバイダから IPv6 アドレスを当てられていないので、IPv6 の通信は実際には来ないはず
  • 将来的には Wi-Fi の AP にもしたい
    • が、それはまたの機会に
    • こちらを開くとルータ内部の前提が崩れるので IPv6 も含めてしっかりやらないといけなさそう

設定方針

  • 公開するポートは http(80), https(443), ssh(22 は使わず、別のポートを使う) のみ
  • ssh ポートは、DOS アタックに対して一定時間のブロックを行う

nftables についての資料

あちこちのブログ記事等も参照したが、最終的には公式 Wiki が一番だった。

https://wiki.nftables.org/wiki-nftables/index.php/Main_Page

説明足らずな部分もあったけど、見付けられた資料の中では一番詳しかった。

コマンドライン

nft コマンドを使う。

このコマンドで 1 つずつルールを足していくこともできるが、今回はルールファイルを用意してそのファイルを読み込ませる方法のみを使った。

設定中に使っていたのは以下のコマンドのみ。

コマンド 説明
nft -f {ファイル名} 設定ファイルを読み込んで適用する。
nft list ruleset 現在適用されている設定を表示する。

作った設定ファイルは最終的には /etc/nftables.conf に配置し、systemctl start nftables.service でロードできる。systemctl enable nftables.service で起動時に設定を適用できる。

設定の書き方

特にここからは、個人的に理解したことをざっくりベースで書いていく。理解が間違っている可能性も大いにある。

nftables の ruleset は複数の table を持っており、table は複数の chain を持っており、chain は複数の rule を持っている。table と chain は名前を持っている。

設定の初期化

設定は nft -f で読み込まれる度に追記される。それは困るので、ファイルの先頭に全ての設定を削除するコマンドを入れておく。

flush ruleset

設定ファイルを読み直す度に最初にルールが全て削除されるので、常にファイルに書かれた状態にできる。

なお、nftables はルールをアトミックに変更してくれるので、「flush した一瞬だけルールがない状態でパケットが処理される」ということはない。この辺りも iptables に対する強みの1つ。

table

table はプロトコルファミリーごとに作成する。

# IPv4 用
table ip filter {
}

filter という名前の IPv4 プロトコル向けの table を作った。ファミリーは他に、ip6(IPv6 向け) inet(IPv4/IPv6 両方向け) 等々。詳細はココ

この table のブロック { ... } の中に chain を書いていく。

chain

chain は大きく分けて base chain と non-base chain がある。

base chain

base chain は処理の入口になる chain で、先頭に type <type> hook <hook> priority <value>; のようなものがある。これはこの chain が処理するパケットの条件で、これにマッチするパケットがその chain で処理される。

table ip filter {
  chain input {
    type filter hook input priority filter; policy drop;

    # ... rules ...
  }
}

input と言う名前の chain を作った。なお table もそうだが、名前はあくまで識別のための名前であり、名前自体に意味はない。好きな名前で大丈夫。

type に指定できるのは filter route nat、filter に指定できるのは prerouting input forward output postrouting ingress みたいな感じ。詳細は公式 Wiki の該当ページを参照。 入ってきたパケットは条件に合う chain を priority の小さい順に処理されていく。priority は数字で、一部の数字には別名がある。priority filterpriority 0 と同じ。負数もある。同値の場合は処理順は不定

non-base chain

non-base chain は base chain 以外のもの。先頭に条件がなければ non-base chain になる。jump や goto などのルールで他の chain から飛んでくる。飛んでこない限り chain は参照されない。

rule

chain の中には rule を順番に書いていく。上から順に処理される。

rule は matches による条件の絞り込みと statements によるパケットに対するアクションを好きな順序で並べていく。

# NEWだがSYNが立っていないパケットを破棄
tcp flags & (fin | syn | rst | ack) != syn ct state new counter log prefix "NEW not SYN:" drop
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ ~~~~~~~~~~~~ ======= ========================= ====
  + matches (TCP のフラグをチェック)        |            |       |                         |
                      matches (conntrack の状態が new)   |       |                         |
                      statements (通過したパケット数をカウント)  |                         |
                                                               statements (log に残す)     |
                                                                      statements (パケットを drop する)

個々の matches と statements の境界が文法上は特になく、わかってないとどこで切れているのかがわからないのでわかりづらい。

matches と statements はいくつでも書けるし、順番が混ざってもよい。左から順に処理される。例えば、

# 仮の文法
<stmt1> <match1> <stmt2> <match2> <stmt3>

みたいに書けば、まず stmt1 は常に実行され、次に match1 にマッチしたパケットには stmt2 が実行され、さらに match2 がマッチしたパケット(つまり match1match2 両方にマッチしたパケット)は stmt3 が実行される。

dropaccept などの一部の statements は Verdict statements と呼ばれていて、パケットの扱いが決まる。 この場合はものによって、chain での処理を中断して chain を抜けたりする。別の chain に飛ぶ、jump (終わったら戻ってくる)や goto (終わっても戻ってこない) も Verdict statements の 1 つ。

chain の最後まで行った場合、chain のポリシーによって扱いが決まる。base chain の先頭に書いてた policy drop; みたいなやつ。これは acceptdrop のどちらかで、書いてない場合はデフォルトで accept になる。

個人的な設定

最後に現時点での個人的な設定を貼っておく。コメントが英語だったり日本語だったり、割と雑。どなたか、もしなにかまずそうな部分を見付けたら、こっそり教えてください…。

#!/usr/bin/nft -f

flush ruleset

define localnet = 192.168.1.0/24

table ip filter {
  set dos_counter {
    type ipv4_addr . inet_service
    flags dynamic
  }
  set block_targets {
    type ipv4_addr . inet_service
    flags timeout
  }

  chain input {
    type filter hook input priority filter; policy drop;

    # セッション確立後のパケットは許可
    ct state established,related counter accept

    # early drop of invalid connections
    ct state invalid counter drop

    # allow from loopback
    iifname "lo" counter accept

    # SYN/ACKでNEWなパケットに対してRSTを送る
    tcp flags & (syn | ack) == syn | ack ct state new counter log prefix "SYN/ACK and NEW:" reject with tcp reset

    # NEWだがSYNが立っていないパケットを破棄
    tcp flags & (fin | syn | rst | ack) != syn ct state new counter log prefix "NEW not SYN:" drop

    # allow from local network
    ip saddr $localnet ct state new tcp dport . ip protocol {
      53 . tcp, 53 . udp,  # DNS
      123 . udp, # NTP
      137 . udp, 138 . udp, 139 . tcp,  # Windows
      445 . tcp,  # Active Dierctory
      22 . tcp, # ssh
      17500 . tcp, 17500 . udp, # Dropbox (LAN sync)
    } counter accept

    tcp dport 22 tcp flags & (fin | syn | rst | ack) == syn ct state new goto dos_detection

    ct state new tcp dport . ip protocol {
      80 . tcp, 443 . tcp,  # HTTP/HTTPS
    } counter accept

    # Ident(113)を拒否(DROPするとレスポンスが遅くなるのでReject)
    tcp dport 113 counter reject with tcp reset

    # ping
    icmp type { echo-reply, echo-request } counter accept
  }

  chain output {
    type filter hook output priority filter; policy accept;
    oifname "lo" counter accept
  }

  chain dos_detection {
    counter
    add @dos_counter { ip saddr . tcp dport limit rate over 10/minute burst 5 packets } counter jump add_block
    ip saddr . tcp dport @block_targets counter drop
    counter accept
  }

  chain add_block {
    # Logging only first time
    ip saddr . tcp dport != @block_targets log prefix "DOS Attacked:"
    update @block_targets { ip saddr . tcp dport timeout 30m }
  }
}

ssh はここでは 22 と書いているが、冒頭でも書いた通り実際には別のポートに変えている。

同じく冒頭で書いた DOS アタックのブロックは dos_detection chain と add_block 辺り。ただの rate limit ではなく、検出したら block_targets に放り込んで 30 分ずっと拒否する。

この検出の部分に limit rate を使ってみたのだけど、なんとなくそれっぽく動いているのだけど、実際にどういう処理になっているのかがいまいちよくわかっていない…。

余談: バグ

ところで、ポート番号とかを { ... } で列挙している部分は Set というもので、これを使うことで1つずつルールを書くよりもテーブルジャンプができることで処理が効率化できるみたい。 が、この文法を複数行で書く場合は注意が必要で、文法上のバグがある。

tcp dport {
  80,  # ここにはコメントが書ける
  # ここにはコメントが書けない!
  443,
} counter accept

コメントと書いたが、実際に書けないのは空行である(コメントだけの行も空行扱い)。 すでにバグトラッカーには登録されているみたいだけど、地味に不便なので直ってほしい…。 (opt_newline が任意の個数の改行を処理してくれればよさそうではあるけど、あまり深追いしてない)

Arch Linux インストールメモ

新しく PC を購入し、Arch Linux をインストールしたので、その作業の個人用メモ。 あとから思い出しながら書いているので、実際に実行したコマンド等は省略気味。実際には紆余曲折があったが、その辺りも省略して、最終的にやったことの記録(それも思い出せる範囲で)。

用途

ssh でログインして使う開発マシン兼おもちゃ用途の汎用自宅サーバ

すでに 1 台同じ用途のがあり、特に問題なく使えているが、5 年前に買ったものなのでスペックアップも兼ねて買い替えることにした。

以前は Gentoo を使っていたが、別のものも使ってみたいと思っていたので、今回は Arch Linux にしてみる。インストールは初めて。

マシン

パーツ 商品 スペック 価格 コメント
ベアボーンPC Intel BXNUC10I7FNH Core i7-10710U (CPU) ドスパラWEB 67,660 円 通称 NUC。あまり深く考えずに選んだ。前から気になっていたシリーズではある。
メモリ G.SKILL F4-2666C18S-32GRS x2 SO-DIMM DDR4-2666MHz 32GB ドスパラWEB 12,693 円 x2 気付いたら 2 枚買っていた。絶対使いきれないと思うが後悔はしていない。
ストレージ CT1000P1SSD8JP M.2 SSD 1TB アマゾン ジャパン 12,800 円 NUC には 2.5 インチ SSD も載るが、これで足りるのでそちらは空けてある。
電源コード KB-DM3S-1 3P ストレートプラグ 1m ヨドバシ.com 624 円 上記が一通り届いた後でこれも必要なのに気付いて買い足した…圧倒的確認不足。

計 106,470 円。メモリもりもりにした割には安い。

インストール方針

ざっくり方針。

なお、ドライブ暗号化や btrfs の導入は個人的には今回が初めて。

インストールメディア作成

Windows マシンで作業。

Arch Linux のダウンロードページからインストールイメージ(archlinux-yyyy.mm.dd-x86_64.iso のようなファイル)をダウンロード。

自宅にあった 4GB の USB メモリに Rufus を使ってイメージを書き込み。特筆すべき点はなし。

インストール

基本は ArchWiki のインストールガイドに従って進める。

パーティション作成

parted を使ってパーティションを切る。

最終的な構成。

モデル: CT1000P1SSD8 (nvme)
ディスク /dev/nvme0n1: 1000GB
セクタサイズ (論理/物理): 512B/512B
パーティションテーブル: gpt
ディスクフラグ:

番号  開始    終了    サイズ  ファイルシステム  名前      フラグ
 1    1049kB  1074MB  1073MB  fat32             efi-boot  boot, esp
 2    1074MB  550GB   549GB                     root
 3    550GB   1000GB  450GB                     home

なんとなく root と home は分けた。ざっくり 2 分割。

実は最初は /boot 用と /boot/efi 用でパーティションを分けてそれぞれ 512MB ずつ振っていたのだが、作業を進めるなかでブートローダーに systemd-boot を使おうとしたところ、そのような構成には対応していないとのことだったので、仕方なく 2 つのパーティションをがっちゃんこしたのだった。 root 用のパーティションはすでに暗号化 + 各種ファイルの配置が終わっていて、開始位置をずらすのは面倒だったし、512 MB くらいケチってもしゃーないので割とリッチなサイズの /boot になった。

ディスク暗号化

パーティションのうち、root と home は暗号化する。暗号化には dm-crypt + LUKS を使う。

何もわからないのでいつも通り ArchWiki の dm-crypt の項目を読んだり人に聞いたりしながら進めた。

鍵は外付けのストレージに置き、刺していないと起動できない構成にする。 外付けストレージは、自宅に転がっていた 2GB の SD カードを使う。もう 10 年近く前の代物…。余裕で壊れる可能性があるので当然鍵はバックアップする(暗号化されているデバイス上に保存されていなければよい)。

まずは鍵ファイルを作成する。これは適当なサイズのランダムなバイナリデータでよい。適当なサイズがどれくらいなのかが全然わからなかったので、適当に 8192 バイトの鍵ファイルを作った。

# mkdir /mnt/sd_card
# mount /dev/mmcblk0p1 /mnt/sd_card
# dd bs=512 count=16 if=/dev/urandom of=/mnt/sd_card/root.key
# dd bs=512 count=16 if=/dev/urandom of=/mnt/sd_card/home.key

この鍵を使ってパーティションを暗号化する。

# cryptsetup -v luksFormat /dev/nvme0n1p2 /mnt/sd_card/root.key
# cryptsetup -v luksFormat /dev/nvme0n1p3 /mnt/sd_card/home.key

作った暗号化デバイスをオープンする。

# cryptsetup open --type luks /dev/nvme0n1p2 crypt-root --key-file=/mnt/sd_card/root.key
# cryptsetup open --type luks /dev/nvme0n1p3 crypt-home --key-file=/mnt/sd_card/home.key

それぞれ名前を指定していて、 /dev/mapper/crypt-root のような名前でアクセスできる。このデバイスを操作すれば、暗号化されたデバイスを通常のデバイスのように操作できる。

ディスクマウントまで

まずはフォーマット。 /bootEFI System Partition なので FAT32 でフォーマットする。

# mkfs.fat -F 32 /dev/nvme0n1p1
# mkfs.btrfs /dev/mapper/crypt-root
# mkfs.btrfs /dev/mapper/crypt-home

そしてマウント。

# mkdir /mnt/arch
# mount /dev/mapper/crypt-root /mnt/arch
# mkdir /mnt/arch/home
# mount /dev/mapper/crypt-home /mnt/arch/home
# mkdir /mnt/arch/boot
# mount /dev/nvme0n1p1 /mnt/arch/boot

マウントをしたので fstab を生成しておく。

genfstab -U /mnt/arch >> /mnt/arch/etc/fstab

インストール

マウントしたデバイスに対して Arch Linux の必要なファイルをインストールする。

# pacstrap /mnt base btrfs-progs linux linux-firmware

今回は btrfs を採用したので、 btrfs-progs パッケージを指定する必要があった。

chroot

Arch Linux には arch-chroot というのがあるのでカンタン。Gentoo の時は結構面倒だった(今は変わってたりするんだろうか)。

# arch-chroot /mnt/arch

細かな設定

おおむねインストールガイド通り。

タイムゾーンの設定

# ln -sf /usr/share/zoneinfo/Asia/Tokyo /etc/localtime
# hwclock --systohc

言語設定

/etc/locale.gen に以下を追記。

ja_JP.UTF-8 UTF-8
en_US.UTF-8 UTF-8

以下のコマンドでロケールを生成。

# locale-gen

/etc/locale.conf に以下を書き込んで起動時のデフォルトにしておく。

LANG=ja_JP.UTF-8

コンソールの設定

普段は ssh で接続する前提なのでコンソールはインストール作業や緊急時くらいにしか使わないんですが。

コンソールではCaps Lock を Ctrl に置き換えておく。Arch Linux では keymap ファイルは /usr/share/kbd/keymaps/i386/qwerty/ 以下にあった。

以下のコマンドでキーボードのキー配列を変更する。jp106-caps-ctrl は時前で用意した Caps Lock を Ctrl に置き換えた配列。

# loadkeys jp106-caps-ctrl

/etc/vconsole.conf に以下を書き込んで起動時のデフォルトにしておく。

KEYMAP=jp106-caps-ctrl

ネットワーク設定

/etc/hostname/etc/hosts をサクッと設定する。

dhcpcd を入れておく。実はインストール中に入れ忘れて起動後にネットに繋がらなくて後から入れ直した。

# pacman -S dhcpcd

root のパスワード

忘れずに設定しておく。

# passwd

ブート設定

先に少し触れた通り、今回はブートローダーに systemd-boot を使う。個人的には今までは GRUB を使っていた。特に GRUB には不満はなかったが、最近は猫も杓子も systemd という風潮を感じるので、せっかくだから systemd に寄せてみるか、程度の動機。

ArchWiki の systemd-boot の項目に沿って進める。

まずは EFI システムパーティション(/boot)に systemd-boot をインストールする。

# bootctl --path=/boot install

Arch Linux を起動するためのローダーを追加する。GRUB では自動でやってくれた部分が手で書くしかないと知った時は絶望した。将来に期待したい…。

/boot/loader/entries/arch.conf に以下の内容のファイルを作成する。

title Arch Linux
linux /vmlinuz-linux
initrd /intel-ucode.img
initrd /initramfs-linux.img
options luks.uuid=84b7c414-4d5a-4958-ae55-c5beb137acd8
options luks.name=84b7c414-4d5a-4958-ae55-c5beb137acd8=encrypt-root
options luks.key=84b7c414-4d5a-4958-ae55-c5beb137acd8=/root.key:UUID=4081-FA31
options luks.uuid=fb18406d-4cee-404a-b33a-bd3163cef23d
options luks.name=fb18406d-4cee-404a-b33a-bd3163cef23d=encrypt-home
options luks.key=fb18406d-4cee-404a-b33a-bd3163cef23d=/home.key:UUID=4081-FA31
options root=UUID=09afad0f-0fd9-4464-8980-2fddd6f5e15c rw

ここが一番ハマった。

luks.uuid には、暗号化されたパーティションの UUID を指定する。 luks.name には、暗号化されたパーティションをオープンする際の名前(/dev/mapper/{name} で参照できる名前)を {UUID}={name} の形式で指定する。 luks.key には、暗号化に使った鍵ファイルの場所を {UUID}={path}:{partition} の形式で指定する。{partition} 内の {path} にある鍵ファイルが使われる。 root は通常通り。当然複合化されたパーティションを指定する必要がある。

参考までに私の環境での各パーティションの UUID なんかの情報を載せておく。

# blkid
/dev/nvme0n1p1: UUID="8FDB-7D9C" BLOCK_SIZE="512" TYPE="vfat" PARTLABEL="efi-boot" PARTUUID="56d44349-149f-4f0c-83b8-d81f15ce7afd"
/dev/nvme0n1p2: UUID="84b7c414-4d5a-4958-ae55-c5beb137acd8" TYPE="crypto_LUKS" PARTLABEL="root" PARTUUID="52179ba4-a678-48bc-b2f0-89609ddc7c25"
/dev/nvme0n1p3: UUID="fb18406d-4cee-404a-b33a-bd3163cef23d" TYPE="crypto_LUKS" PARTLABEL="home" PARTUUID="a0806d30-22bd-49a1-9de1-acffbad6321f"
/dev/mmcblk0p1: SEC_TYPE="msdos" UUID="4081-FA31" BLOCK_SIZE="512" TYPE="vfat"
/dev/mapper/encrypt-root: UUID="09afad0f-0fd9-4464-8980-2fddd6f5e15c" UUID_SUB="77041826-2e8c-49b9-b3d0-f7267ac8c231" BLOCK_SIZE="4096" TYPE="btrfs"
/dev/mapper/encrypt-home: UUID="6e980051-5837-4be6-aff2-00551a20339a" UUID_SUB="3a3e48ee-2c56-4a48-97b4-55f0d2779723" BLOCK_SIZE="4096" TYPE="btrfs"

最初は /etc/crypttab ファイルや /etc/crypttab.initramfs ファイルに記載してブートを試みたが、うまくいかず。恐らく書式が間違っていたのだと思う(調べるのをやめてしまったので真相は不明…)。起動時のオプションから渡すようにしたらうまくいった。

マイクロコード

前述の arch.conf 内で参照している intel-ucode.img を用意するために intel-ucode をインストールする。

# pacman -S intel-ucode

再起動

うまく起動するように祈る。

# systemctl reboot

実際には1度でうまく行く訳がなく、何度も再起動した(主にブートローダーの設定辺り)。

無事起動できたら、一般ユーザーの作成とかをやっていく。

謝辞

インストール作業にあたって、vim-jp Slack の #os-linux チャンネルの皆さんに大変御助力頂きました。本当にありがとうございます。ログはこの辺り

dein.vim でのプラグインの更新チェックを爆速にする

先日、Vimプラグインマネージャである dein.vim に、プラグインの更新チェックを劇的に高速にする変更が取り込まれた。使うには設定が必要なので、その方法を紹介する。

dein.vim を最新版にする

言うまでもないが、まずは dein.vim を最新版にする。

:call dein#update('dein.vim')

GitHub の Personal Access Token を生成する

この機能を使うには、GitHub の Personal Access Token を用意する必要がある。

https://github.com/settings/tokens

GitHub にログインして上記のページにアクセスし、Generate new token を押す。token の用途がわかりやすいように名前を付け、Generate token を押す。追加の権限は必要ない。生成された token をなくさないようにメモし(ページ遷移すると二度と見れないので注意。メモし忘れた場合は再生成する)、vimrc に以下のように設定する。

let g:dein#install_github_api_token = 'your token'

your token のところに生成した token を書く。言うまでもないが、この token は人に知られてはまずいものなので、vimrc を公開しているのであれば別のファイルに分離するなどの工夫が必要だ。

更新方法を変更する

通常、プラグインの更新には dein#update() を使っていたと思うが、新しい方法では dein#check_update() の第1引数に TRUE な値を渡すことで行う。

call dein#check_update(v:true)

特定のプラグインだけ更新したい場合はプラグインの一覧を第2引数に渡す。

call dein#check_update(v:true, ['dein.vim'])

プラグインを100個単位で入れていたとしても、あっという間に更新のチェックが終わる。

仕組み

ここからはオマケで、高速化の仕組みについて。

Vimプラグインはそのほとんどが GitHub 上で公開されている。今までは、プラグインの新しいバージョンが公開されているかどうかを、リポジトリ 1 つずつチェックしていた。当然 1 件ずつ通信を行うため、プラグインを100個も200個も入れていると、とても時間がかかる。

新しい方法では、GitHub の GraphQL API を使い、リポジトリを 100 件単位で更新があるかどうかをチェックする。このために Personal Access Token が必要になる。

GitHubREST API では 1 度のリクエストでアクセスできるリポジトリは 1 つだけになってしまうが、GraphQL API であれば 1 度の API リクエストで 100 件のプラグインのチェックができるため、更新されたプラグインがなければ非常に高速にチェックが終了する、というわけだ。

この手法については私が雑談レベルで提案していたものだが、今回 Shougo さんがものすごい速さで実装してくれた。ありがとうございます!

更にオマケ: 別の高速化の話

プラグインマネージャは更新処理の際、「今ローカルにインストールされているプラグインのバージョン(Git のコミット)はいくつか」を知る必要があるが、Windows では Linux などに比べると外部プロセスの起動が非常に遅く、リポジトリ毎に git コマンドを実行するだけでも数が多いとかなり時間がかかっていた。

別のプラグインマネージャである minpac ではこの問題を改善するために、「Git のコミットを得るために git コマンドを実行せずに直接 .git/ ディレクトリ内のファイルを読む」という手法を採用していたが、このたびこの方法が dein.vim にも取り込まれた

よって特に Windows ユーザーは以前に比べて更に高速になっている。

なお、こちらの変更は特に設定をしなくても恩恵を受けられる。

Vim で q を prefix キーにする

前置き

Vimプラグインをたくさんいれていると、それらを呼び出すためにキーマッピングを用意したくなることはよくある。

Vim はキーシーケンスに対してマッピングを割り当てられるので、何かのキーを prefix キーとして、そのキーに続けて何かしら機能を割り当てると言うテクニックがよく用いられる。

" スペースキーを prefix にする例

" スペースキー単体では何も起きないようにする
" これをしておかないと、うっかり <Space> + 割り当ててないキーを
" 押すと <Space> の元の機能が発動する
nnoremap <Space> <Nop>

" <Space>q でウィンドウを閉じる
nnoremap <silent> <Space>q :<C-u>quit<CR>

" ... 以下、<Space> + 何かにキーを割り当てられる

しかしこの方法をもってしても、割り当てたい機能はどんどん増えていくものである。prefix キーとして利用しやすいキーはいくつかあるが、それらが枯渇するほど機能を割り当てている人も世の中にはいるようだ。

q を prefix にすることで起きる問題

q は、マクロの記録を開始するコマンドである。q + レジスタのアルファベット1文字で、そのレジスタに対してマクロの記録を開始する。

しかし、26個もあるマクロを全て器用に使いこなしている人はおそらくまずいないと思う。私なんてだいたい1つしか使わない。どこに何が記録されているか、覚えられないからだ。

となると、残りの q + アルファベットは空いていることになる。q を prefix にすればこれらが使える。何ならアルファベット以外の文字も使えることになる。

nnoremap q <Nop>
nnoremap qa :<C-u>echo 'Awesome feature!'<CR>

こうすることで、qa は無事便利機能に割り当てることができる…が、こうしてしまうと、q<Nop> にすることで、マクロの開始が一切できなくなってしまう。

これはさすがにまずい。しかしこれは単に q<Nop> を割り当てなければ解決する。

nnoremap qa :<C-u>echo 'Awesome feature!'<CR>

マクロの記録も開始できる。これで何も問題はない…?

更に潜む問題

Vim のマクロの記録は q 単体で終了する。ところが、q で始まるキーマッピングがあると、押された q がマクロの終了であるのか、はたまたキーマッピングの呼び出しなのかを判断するため、Vim はしばらくの間キーマッピングの待受をする。

待受時間を待ってやり過ごすか、q で始まるキーマッピングではない何か別のキーのコマンドを実行すれば済むのだが、いかんせんやきもきしてしまう。やはり prefix 以外のキーマッピング待ちは発生させたくない。

解決策

今回、この問題を解決するために以下のように設定してみた。

nnoremap <script> <expr> q reg_recording() is# '' ? '<SID>(q)' : 'q'

nnoremap <silent> <SID>(q) q
nnoremap <silent> <SID>(q)a :<C-u>echo 'Awesome feature!'<CR>
nnoremap <silent> <SID>(q)<Space> :<C-u>quit<CR>

q を押した際に、マクロの記録中であれば即座に q を起動してマクロの記録を終了させ、そうでなければ prefix としての q を呼び出す。prefix の設定がない場合はそのまま q として機能させることで、マクロの記録開始も行えるようにする。

こうすることで、q を prefix として機能させつつマクロの記録開始/終了に影響を与えないようにすることができる。私は <Space>q をよく q<Space>誤爆することがあったので、同じ機能を割り当ててみた。

今回はなんとなく <SID> を使ってみたが、q prefix のキーマッピングを複数のスクリプトで定義したい場合は <Plug> を使っても問題ないだろう。

nmap <expr> q reg_recording() is# '' ? '<Plug>(q)' : 'q'

nnoremap <silent> <Plug>(q) q
nnoremap <silent> <Plug>(q)a :<C-u>echo 'Awesome feature!'<CR>
nnoremap <silent> <Plug>(q)<Space> :<C-u>quit<CR>

この場合は q のキーマッピングnnoremap ではなく nmap を使うことに注意する必要がある。

残る問題

ここまでやっても一応まだ問題は残ってる。ここで定義した q で始まるキーマッピングは、マクロの記録中に呼び出すことはできない。 しかしこれはある意味当然であり、マクロ記録中に呼び出したい機能を q に割り当てないようにする他はない。

Vim Short Tips Advent Calendar を開催しました

全然ブログを更新しない thinca です。

さて、今年も Advent Calendar の季節がやってきました。あちこちで大量の知見が出回っていますね。

記事が増えるのはいいことなんですが、一方で私はこの流れにあるつらみも感じていました。

書くのがつらい

長編記事は書くのが大変です。いや、別に誰も長編記事を書いてくれ、とは言っていません。

しかし上がってくる記事はどれも力作。こんな流れの中、しょぼい記事は上げづらい、なんて感じたことはないでしょうか。

特に日本人は空気を読むように訓練されている節があるので、ハードルが高いと感じてしまう人もいそうな気がします。というか私は大作記事は大変なのでできれば書きたくないです…。

読むのがつらい

特にこの時期に大量の記事が出回ることになるので、楽しみな反面、読むのが大変です。

途中で挫折して読むのをやめてしまったカレンダー、ありませんか。

試験的な試み

そこで今回試しにやってみたのが、Vim Short Tips Advent Calendar です。

内容を、Twitter の 1 ツイートに収まるものに限定し、書く手間と読む手間を下げよう、という試みです。テーマが Vim なのは私が Vim 界隈で活動しているからですね。

内容も、高度なものである必要はありません。というか短いので高度なことは書けません。知ってるとちょっと便利な Tips がメインです。

実はこれ、11 月中にカレンダーがあちこちで立てられるなかぼんやり考えていたものなんですが、vim-jp コミュニティ内でコンセプトを説明して参加者を募ったのが 11/30 の 20 時頃。実際にカレンダーを立てたのはその1時間後の 21 時頃というギリギリでの開始でした。

にも関わらず、参加者はあっという間に集まり、最初数日分は即座に埋まり、その辺りを走っているうちに最後まで全てメンバーが埋まりました。コンセプトの狙い通り、参加のハードルを下げたのが功を奏したのだと思っています。

参加者や各 Tips を見てくれた人からも概ね好評だったように思います*1。こういうやり方もありだな、と感じました。

Tips 一覧

せっかくなので Tips 一覧を引用してみたいと思います。短いので全部並べてもサクッと読めますね。お気に入りの Tips を見付けたら、ぜひいいねや RT をしてあげてみてください。

1日目

2日目 3日目 4日目 5日目 6日目 7日目 8日目 9日目 10日目 11日目 12日目 13日目 14日目 15日目 16日目 17日目 18日目 19日目 20日 21日目 22日目 23日目 24日目 25日目

おまけ

*1:アンケートを取ったわけではないので私が感じた限りでは