nftables cgroup 우회에는 소스 주소 수정도 필요하다
cgroup 하나를 표시하면 다른 테이블로 보내지지만 소켓은 이미 갖고 있던 주소를 그대로 유지한다. 분류, 경로 선택, 변환을 세 개의 별도 단계로 다뤄야 한다.
cgroup 하나에서 나오는 트래픽을 표시하는 것은 쉬운 부분이다. 애플리케이션 하나가 물리 인터페이스로 나가고 나머지는 모두 터널에 남으려면 세 가지 별개의 결정이 맞아떨어져야 한다. 어느 소켓이 예외에 속하는지, 그 표시가 어떤 경로 조회를 일으키는지, 그리고 패킷이 나갈 때 어떤 소스 주소를 싣는지다. 실패한 시도의 대부분은 첫 번째를 맞히고, 두 번째를 절반만 맞히며, 세 번째는 아예 보지 않는다.
세 가지 결정이지 하나가 아니다
분류는 누가 예외인지에 답한다. 경로 선택은 표시된 패킷이 대신 어디로 가는지에 답한다. 소스 선택은 반대편이 무엇을 보게 되고 응답이 어디로 돌아가야 하는지에 답한다.
이들은 각각 독립적으로 실패한다. 패킷은 분류되고 물리 인터페이스로 다시 라우팅된 뒤에도 상위 라우터가 돌려보낼 수 없는 사설 주소를 계속 실을 수 있으며, 그러면 방화벽 차단과 똑같이 보이는 타임아웃이 생긴다. 분류 단계에서는 그것을 경고하지 않는다. 경로 단계에서도 마찬가지다.
순서도 중요하다. 분류는 패킷이 생성될 때 일어나고, 경로 선택은 그 뒤에, 변환은 나가는 지점에서 일어난다.
세 단계 중 어느 것이 고장 났는지 말할 수 없다면, 그것은 디버깅이 아니라 규칙을 재배열하며 기대하는 것이다.
내일도 존재할 cgroup에 맞춰라
nftables는 소켓의 cgroup v2 소속을 검사할 수 있는데, 목적지가 아니라 프로세스를 따라가므로 정말 유용하다. 함정은 식별자다. 메뉴에서 실행한 데스크톱 애플리케이션은 다음 실행 때 이름이 바뀌는 일시적 scope에 사는 경우가 많다.
nft list ruleset
systemctl status session.slice 2>/dev/null
cat /proc/self/cgroup한 세션에서 scope 이름을 복사해 하드코딩하면 오늘은 동작하고 내일은 조용히 일치하지 않게 되는 규칙이 만들어진다. 대신 애플리케이션을 직접 통제하는 전용 scope나 unit처럼 의도적이고 안정적인 실행 문맥에 두고 그것에 맞춰라. 그러면 규칙이 재부팅과 패키지 업데이트를 견딘다. 동료가 당신의 세션 기록 없이도 이해할 수 있는 것이 되기도 한다.
어떤 메커니즘을 쓰는지도 정확히 하라. cgroup v1의 net_cls 분류와 cgroup v2의 소켓 일치는 다른 것이며, 문서에 비슷한 이름의 필드가 있다고 해서 서로 바꿔 쓸 수 있는 것은 아니다.
두 번째 조회를 일으키는 것은 route 체인이다
로컬에서 생성되는 트래픽에서는 체인 유형이 표시가 설정된 뒤 무언가 일어날지를 결정한다. filter 유형의 output 체인은 패킷을 표시할 수 있지만 커널은 이미 선택한 경로를 계속 쓴다. route 유형의 체인이 새 조회를 요청하는 메커니즘이다.
table inet bypass {
chain mark {
type route hook output priority mangle; policy accept;
socket cgroupv2 level 2 "app.slice" meta mark set 0x1 comment "app bypass"
}
}
ip rule add fwmark 0x1 lookup 200
ip route add default via 203.0.113.1 dev wlan0 table 200그다음 표시에는 그것을 테이블로 보내는 정책 규칙이 필요하고, 그 테이블은 완전해야 한다. 기본 경로, 애플리케이션이 여전히 요구하는 로컬 목적지, 그리고 실제로 응답하는 상위 게이트웨이다.
표시 값은 의도적으로 고르라. WireGuard는 외부 전송 트래픽에 자체 fwmark를 쓰므로, 우연히 같은 번호에 닿으면 터널 자체의 경로 예외를 진단하기 어려운 방식으로 방해할 수 있다.
소스 주소는 경로를 따라오지 않는다
터널 경로를 위해 이미 소스 주소를 선택한 소켓은 다시 라우팅된 뒤에도 그 주소를 유지한다. 경로 변경에서 그것을 다시 쓰는 것은 없으며, 그래서 패킷은 상위 네트워크가 돌려보낼 수 없는 사설 소스로 물리 인터페이스를 나간다.
눈에 보이는 증상은 데이터를 보내는 것처럼 보이지만 응답을 전혀 받지 못하는 애플리케이션과, 패킷이 돌아오지 않아 아무것도 보여주지 않는 방화벽 로그다.
좁은 수정은 그 애플리케이션의 표시와 실제 출력 인터페이스로 범위를 한정한 소스 변환 규칙이며, 기존 NAT를 대체하지 않고 통합하는 것이다.
table inet bypass {
chain postrouting {
type nat hook postrouting priority srcnat; policy accept;
meta mark 0x1 oifname "wlan0" masquerade
}
}예외만 변환하고 그 밖의 것은 건드리지 말라. 전면적인 masquerade 규칙은 증상을 고치고 어떤 트래픽이 어느 경로를 탔는지 구분하는 능력을 파괴한다. 표시, 실제 출력 인터페이스, 가능하면 애플리케이션 자체의 주소에 맞춰라. 넓은 규칙은 쓰기 쉽고 나중에 감사하기는 거의 불가능하다.
그래도 예외를 벗어나는 것들
이름 확인은 다른 cgroup의 별도 리졸버 프로세스에서 올 수 있으므로 브라우저의 소켓을 분류해도 그 조회는 분류되지 않는다. 리졸버의 질의가 터널에 남아 있는데 연결이 그 밖으로 나가면, 스플릿 호라이즌 응답이 경로와 어긋날 수 있다. 그 불일치는 DNS 버그처럼 보이지만 그렇지 않은 장애를 만든다.
대부분의 구성에서 자식 프로세스는 cgroup을 상속하지만, 스스로를 전용 scope로 옮기는 헬퍼는 상속하지 않는다. 가정하지 말고 확인하라. 특히 스스로를 다시 실행하는 모든 것에 대해서.
마지막으로, 터널이 내려갔을 때 무슨 일이 일어나야 하는지 정하라. 조용히 완전한 우회가 되는 예외는 실패 시 닫히는 예외와 다른 보안 태세이며, 둘 중 하나만이 당신이 의도한 것이다.