systemd での OpenVPN のランダム選択:ExecStart が失敗する理由
systemd はシェルではないため、ExecStart の中で混ぜたコピーは死ぬ。選択を ExecStartPre に移すか、OpenVPN 自身にリモートを選ばせよう。
ある国の .ovpn ファイルがプロバイダーから四十個届き、ランダムな一つを所定の位置にコピーしてからトンネルを起動するという計画は明白に見える。これを ExecStart の中でパイプラインとして書くと、その計画は起動時に、何も説明しないステータスコードを残して死ぬ。ユニットファイルはスクリプトではなく、修正はシェルを明示的に持ち込むか、ファイルの選択そのものをやめるかのどちらかだ。
症状:起動してすぐに死ぬユニット
ステータスはサービスが一秒の何分の一かで失敗したことを示し、ジャーナルの行はどのシェルも受け取らなかったはずのコマンドを名指しする。トンネルについては何も現れない、トンネルがそもそも始まっていないからだ。
ジャーナルから正確なコマンドラインを読むのが、これを確認する最短の方法だ。systemd は実行した内容をそのまま返すので、文字列にはパイプラインの記号が演算子ではなく文字通りの引数として現れる。
ExecStart がパイプラインを実行できない理由
systemd はコマンドラインを自分で分割し、独自の引用符処理と変数展開を適用してから、結果を直接実行する。パイプ、リダイレクト、シェルによるグロブ展開、コマンド置換は、そもそもこの文法の一部ではない。これは省略ではなく設計上の選択であり、ユニットは何を実行するかを宣言的に述べ、シェルのロジックはユニットが呼ぶスクリプトに属する。どちらの修正もそこから導かれ、どちらもユニットに追加の権限を与える必要はない。
これは誤解を招く終了コードも説明する。最初の引数がバイナリとして扱われるため、不正な行はあなたのロジックが動く前に失敗する。
選択は ExecStartPre で行う
直接の修正は、シェルを明示的に呼ぶ準備ステップであり、選択と起動を同じユニットに置くことで、片方の失敗がもう片方を単独で動かし続けることを防ぐ。
[Service]
Type=notify
ExecStartPre=/bin/bash -c "ls /etc/openvpn/profiles/au* | shuf -n 1 | xargs -r -I{} cp -f {} /etc/openvpn/%i.conf"
ExecStart=/usr/sbin/openvpn --config /etc/openvpn/%i.conf
三つの点が、これが実機との接触に耐えるかどうかを決める。第一の罠は命名で、systemd はユニットの解析中、bash が動くよりずっと早い段階で %i を置換するため、指定子は一重引用符の外に置かなければならない。xargs -r は、グロブが何にも一致しないときに cp が引数なしで実行されるのを防ぐ。そして準備ステップが失敗すればユニットは停止するが、それが正しい結果であり、昨日の設定で起動するより起動しないほうがましである。常に同じファイルを選ぶランダム選択は、たいてい運が悪いのではない。コピーしたファイルがパッケージ更新で上書きされていないか、グロブが古いエントリ一つにしか一致していないかを確認しよう。
あるいは OpenVPN にエンドポイントを選ばせる
OpenVPN はすでに一つのプロファイルに複数のエンドポイントを保持し、その中からランダムに選べるため、シェルもコピーも書き込み権限も起動経路から完全に外れる。
client
remote-random
<connection>
remote au1.example.net 1194 udp
</connection>
<connection>
remote au2.example.net 1194 udp
</connection>
この形式はサーバーの選択をプロバイダーが書いたファイルの中に保つので、ファイル名を変えるローテーションでもユニットは壊れない。候補の一覧が監査可能になる点も、ディレクトリに対するグロブには決してない利点だ。プロファイルがエンドポイント以外の点でも異なる場合、証明書が違い、ルーティングの指示が違い、暗号ポリシーが違うだけに、シャッフルはなお価値がある。その場合は構成全体の間の選択になり、シェルのステップがそれを正直に表現する方法だ。
使われたインスタンスとファイルを読み返す
内容をデバッグする前に、実際にどのユニットが動いているかを確認する。パッケージのメタサービスとテンプレートはインフラであり、あなたのファイルに対応するユニットは、その名を冠したインスタンスだ。
systemctl list-units 'openvpn*'
systemctl cat [email protected]
journalctl -u openvpn@au -n 30 --no-pager
誰も有効にした覚えのないインスタンスは、たいていパッケージディレクトリに紛れた設定ファイルに行き着く。そうしないと次のパッケージ更新で戻ってくるため、テンプレートではなく、その名前のインスタンスを無効にしよう。変更後はユニットを起動し、ジャーナルが報告するコマンドラインを読み、実際に開いた設定パスを確認する。その一つの確認が、機能するランダム選択と、先月のファイルを静かに使い回しているサービスとを分ける。