VPN サービスが EXEC で失敗する: unit、バイナリ、sudoers を比較する
EXEC ステップでの失敗は、systemd がバイナリを起動できなかったことを意味します。何かを編集する前に、unit のパス、インストール済みのパス、sudoers の許可を比較してください。
昨日はトンネルが動いていたのに、今朝は 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、権限ルール、インストーラーがそれぞれ一つのプレフィックスを記録しており、三つのうち二つが食い違ったときに障害が現れます。
| Record | Common value | How to read it |
|---|---|---|
| Unit or drop-in | /usr/local/bin/vpnclient | systemctl cat vpnclient.service |
| Installed binary | /usr/bin/vpnclient | command -v vpnclient, then ls -l the result |
| Sudoers grant | /opt/vpnclient/bin/vpnclient | sudo 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-pagersystemctl show が決定的な出力です。systemd が実際に解析した argv を、drop-in も含めて表示するからです。systemd-analyze verify は再起動の前に、存在しない、あるいは実行できないパスを捕まえます。再起動後の status コマンドは、プロセスが今あるかどうかを教えてくれます。
unit が起動してから失敗するなら、EXEC の領域を離れてクライアント自身のエラー処理に入っています。journal で探すべきはクライアントのメッセージであり、別の execve の失敗ではありません。unit の差分と sudoers の差分を変更記録にまとめて残し、次のアップグレードが分割されたパスを黙って戻さないようにしてください。