UID 규칙으로 Linux 앱 하나를 VPN에 고정하는 방법
UID 규칙은 로컬 소켓만 보며, 흔한 loopback 예외가 터널을 죽인다. 먼저 계정에 경로를 주고, 그다음 각 단계를 걸러 검증한다.
Linux에서 프로그램 하나를 VPN에 고정하는 것은 주로 라우팅 문제다, 그리고 방화벽 규칙은 경로가 선택된 뒤에 무슨 일이 일어나는지만 결정한다. 터널이 기본 경로를 가지기 전에 OUTPUT에서 프로그램의 UID를 일치시키면, 규칙은 보호하려던 트래픽을 그대로 버린다. 같은 규칙은 프로그램의 DNS 질의에 응답하는 리졸버 프로세스도 보지 못한다. 순서를 바로잡으면 권한 없는 계정 하나가 신뢰할 수 있는 분할 터널 선택자가 된다.
판정 전에 계정에 경로를 주라
소유자 일치는 한 가지 질문에 답한다. 이 UID에 속한 로컬 소켓이 패킷을 만들었는가. 인터페이스를 고르지도 않고, 메인 테이블이 이미 물리 NIC로 내보내는 트래픽을 옮길 수도 없다. 그래서 일치하는 경로가 없는 UID 규칙은 프로그램을 돌려보내는 대신 차단한다.
계정을 먼저 자기 테이블로 통과시키고, 그다음 방화벽이 결과를 확인하게 하라. 이것은 iproute2가 uidrange를 이해하는 호스트에서 동작한다. ip rule help 가 이를 한 줄로 보고한다. 테이블 100은 터널을 통한 기본 경로를 가지며, 규칙은 UID 1000만 그 테이블로 보낸다. 나머지는 모두 메인 테이블에 남아 물리 링크를 계속 사용한다.
ip rule add uidrange 1000-1000 table 100 ip route add default dev tun0 table 100
다른 터널이 필요한 두 번째 계정은 테이블 101과 자기 규칙을 받는다. 두 프로필이 같은 테이블에서 기본 경로를 함께 가질 수 없기 때문이다. NetworkManager 프로필은 시스템 전역이므로 이 부분은 수동으로 남고, 재부팅 시 무언가 복원하지 않으면 규칙이 사라진다. 터널 정의 옆의 부팅 스크립트에 테이블을 적어 두어 재연결이 계정 하나를 잘못된 경로에 남기지 않게 하라.
loopback 예외가 터널을 죽이는 이유
복사된 규칙 대부분은 loopback으로 나가지 않는 모든 패킷을 버린다. 터널은 loopback이 아니므로 그 형태는 터널 트래픽도 버리고, 결과는 방화벽 실수가 아니라 죽은 VPN처럼 보인다.
OUTPUT이 실행될 때 커널은 이미 출구 인터페이스를 골랐으므로 -o 패킷 헤더가 아니라 경로의 응답과 비교된다. 소유자 일치 는 OUTPUT과 POSTROUTING에서만 조회되며, 전달된 패킷에는 그것이 읽을 소켓이 없다. 이 조합은 예외가 loopback을 부정하는 대신 터널 인터페이스를 지목하고 그 일치를 부정해야 함을 뜻한다.
loopback은 여전히 DROP 위에 자기 ACCEPT를 받을 자격이 있다. 프로그램의 첫 DNS 패킷이 lo 를 거쳐 로컬 스텁으로 가기 때문이다. 두 규칙을 이 순서로 두면 계정은 스텁에 닿고, 다음 터널에 닿으며, 다른 곳에는 닿지 않는다.
iptables -I OUTPUT 1 -o lo -m owner --uid-owner vpnapp -j ACCEPT iptables -A OUTPUT -m owner --uid-owner vpnapp ! -o tun0 -j DROP
IPv6는 별도의 규칙 집합이고, v4만을 위한 쌍은 ip6tables 를 건드리지 않는다. AAAA 레코드를 선호하는 프로그램은 그러면 DROP에 한 번도 닿지 않고 물리 링크로 나갈 것이다. 두 규칙을 모두 ip6tables 에 반영하거나, 터널이 v6 주소를 나르지 않을 때 호스트에서 IPv6를 끈다. 손으로 추가한 규칙도 재부팅 시 사라지므로 netfilter-persistent save 로 저장하거나 같은 논리를 부팅 유닛이 읽는 nftables 테이블로 다시 만든다.
DNS 응답은 다른 프로세스에서 온다
프로그램의 127.0.0.53 로의 질의는 loopback ACCEPT를 통과하고, 바로 거기서 UID 일치가 멈춘다. 스텁 리졸버는 자기 서비스 계정에서 위로 전달하므로 vpnapp 에 묶인 규칙은 두 번째 질의를 결코 보지 못한다. 리졸버가 링크용으로 LAN 서버를 고르면 응답은 물리 NIC로 돌아오고 프로그램 자체 패킷은 터널에 남는다. 눈에 보이는 결과는 이름이 깨진 채 동작하는 터널이거나, 선택하지 않은 서버가 답하는 이름이다.
이름 해석은 애플리케이션이 아니라 터널 링크에서 고쳐라. 터널 프로필에 DNS 서버와 라우팅 도메인을 주고, 그다음 resolvectl query 과 resolvectl status tun0 로 리졸버가 중요한 이름에 그것을 쓰는지 확인한다. 네트워크 이름 공간 안에서는 문제가 사라진다. 이름 공간 리졸버는 터널만 알기 때문이다.
계정 자체에서 규칙을 확인하라
커널에 UID가 어떤 경로를 받는지 묻고, 그 계정으로 요청을 보내 출구 주소를 자기 계정의 주소와 비교하라. 둘 다 같은 주소를 보고하면 규칙은 시험한 트래픽이 아닌 다른 것과 일치하고 있다. 아래 첫 명령은 패킷을 보내지 않고 경로 조회를 보여 주므로 라우팅 실수와 방화벽 실수를 구분한다.
ip route get 1.1.1.1 uid 1000 sudo -u vpnapp curl -s https://ifconfig.co
카운터는 규칙이 실제로 발동했는지 보여 준다. iptables -vnL OUTPUT 를 실행하고 각 규칙 옆의 패킷 수를 읽으며, 실제 트래픽 뒤 DROP 옆의 영을 일치 오류로 취급하라.
전달되는 트래픽은 이 설계 밖에 있다. 브리지 뒤의 컨테이너나 VM에는 vpnapp 가 소유한 로컬 소켓이 없으므로 소유자 일치는 결코 발동하지 않고 그 패킷은 대신 FORWARD 정책을 따른다. 그 워크로드를 덮어야 한다면 따로 시험하고 브리지나 컨테이너 주소에 묶인 정책을 써라. 설계는 테이블, 인터페이스 예외, 리졸버, v6 규칙이 모두 같은 방향을 가리킬 때 성립한다. 하나라도 빼면 계정은 새거나 연결을 잃는다.