昨日はトンネルが動いていたのに、今朝は unit が死んでいて、journal に Failed at step EXEC spawning /usr/local/bin/vpnclient と出ています。認証情報やルート、アカウントはこの一文には一切関係しません。systemd は ExecStart をパスに解決し、カーネルに実行を求め、カーネルが拒否しました。多くの場合、そのパスに何も存在しないか、ファイルが実行可能ではないからです。解析済みのコマンドがそうでないと証明するまで、この障害はファイルと argv の問題として扱ってください。

認証が始まる前に EXEC が失敗する理由

systemd は ExecStart をシェル経由では実行しません。fork し、行から引数ベクトルを組み立て、最初の要素に対して execve を呼びます。この呼び出しが失敗すると、サービスは VPN クライアント自身のコードに到達しないため、プロファイルの編集も認証情報の確認も役に立ちません。

ステータスコードがこの種の障害を通常のクラッシュから切り分けます。status=203/EXEC で終了した unit はプロセスを一度も起動していません。起動してから status=1 で終了した unit は、認証、DNS、ルーティングのいずれかで失敗した可能性があります。クライアントのログより先にコードを読みましょう。前者の場合、クライアントのログは空になります。

journal に出ているパスは systemd が使ったパスであり、あなたが編集したパスとは限りません。drop-in やジェネレーターの出力はベンダーのファイルを上書きでき、古い daemon-reload は古い行をメモリに残します。だから最初の診断は、覚えているファイルを開くことではなく、systemd にコマンドを何だと考えているかを尋ねることです。

バイナリのパスが書かれる三つの場所

ディストリビューションのパッケージ、ベンダーのスクリプト、リリースの tarball からインストールされた VPN クライアントは、三つの異なるプレフィックスに置かれることがあります。unit、権限ルール、インストーラーがそれぞれ一つのプレフィックスを記録しており、三つのうち二つが食い違ったときに障害が現れます。

RecordCommon valueHow to read it
Unit or drop-in/usr/local/bin/vpnclientsystemctl cat vpnclient.service
Installed binary/usr/bin/vpnclientcommand -v vpnclient, then ls -l the result
Sudoers grant/opt/vpnclient/bin/vpnclientsudo grep -r vpnclient /etc/sudoers.d/

具体的な不一致はこうです。ベンダーのスクリプトは /usr/local/bin にインストールし、tarball は /opt の下に展開され、unit は以前のパッケージの古い /usr/bin パスをまだ指しています。存在するのはそのうち一つだけで、ExecStart にあるのはまれです。

どちらのファイルを変更する前に、どちらのパスが正典かを決めてください。バイナリがディストリビューションのパッケージに属するなら、パッケージのパスが正典であり、unit はそれに従うべきです。ベンダーのスクリプトに属するなら、同じ規則がベンダーのプレフィックスに当てはまります。プレフィックス間でバイナリをコピーすると、次のアップグレードで食い違う二つの複製ができます。

引用符付きのコマンドがファイル名になるとき

二つ目の失敗の型は、同じ journal の行の背後に隠れています。誰かがこれを unit に書き、起動中にクライアントが接続すると期待します。

ExecStart=/usr/bin/"vpnclient connect office"

systemd は値を単語に分割するので、最初の単語は、引用符がグループ化の構文として取り除かれた /usr/bin/vpnclient connect office になります。次にカーネルは名前に空白を含むファイルを探しますが、そんなファイルはありません。バイナリがインストールされていても、エラーはバイナリが無い場合と同一です。

サポートされる形式は、実際の実行ファイルの後に個別の引数が続く形です。たとえば ExecStart=/usr/bin/vpnclient connect office です。ワークフローが本当にパイプラインや連鎖を必要とするなら、それをスクリプトに入れてスクリプトを呼んでください。ExecStart には && も、パイプも、glob 展開もないからです。

ファイルシステムではなく unit を直す

誘惑される修復は、期待されるパスから実際のバイナリへのシンボリックリンクです。パッケージが更新されてリンクが削除済みのファイルを指すまで、あるいは SELinux ラベルが対象と一致しなくなるまでは機能します。ベンダーのファイルをそのままにし、アップグレードを予測可能に保つため、drop-in で unit を編集するほうがよいでしょう。

権限ルールは同じパスと同じサブコマンドに一致しなければなりません。/usr/local/bin/vpnclient に対する sudoers の項目は /usr/bin/vpnclient を許可しませんし、あらゆる引数を許す項目は、任意のローカルプロセスにトンネルの再設定や停止を許します。unit が必要とする二つの動詞だけを与え、サービスの停止がすでにインターフェースを閉じるなら解体作業は外に出してください。

ここで比較が効いてきます。unit、パッケージデータベース、sudoers のすべてが一つのパスを示すべきです。そうなれば、再起動も、三つ目の場所へのバイナリの複製もなしに EXEC のエラーは消えます。

再起動の前に解析済みのコマンドを証明する

sudo systemctl daemon-reload
sudo systemctl cat vpnclient.service
systemctl show -p ExecStart vpnclient.service
systemd-analyze verify /etc/systemd/system/vpnclient.service
sudo systemctl restart vpnclient.service
systemctl status vpnclient.service --no-pager

systemctl show が決定的な出力です。systemd が実際に解析した argv を、drop-in も含めて表示するからです。systemd-analyze verify は再起動の前に、存在しない、あるいは実行できないパスを捕まえます。再起動後の status コマンドは、プロセスが今あるかどうかを教えてくれます。

unit が起動してから失敗するなら、EXEC の領域を離れてクライアント自身のエラー処理に入っています。journal で探すべきはクライアントのメッセージであり、別の execve の失敗ではありません。unit の差分と sudoers の差分を変更記録にまとめて残し、次のアップグレードが分割されたパスを黙って戻さないようにしてください。

関連記事