어제는 터널이 동작했는데 오늘 아침 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 diff와 sudoers diff를 변경 기록에 함께 남겨 다음 업그레이드가 분리된 경로를 조용히 되살리지 않게 하세요.

관련 글