중첩 WireGuard: 중간 홉은 자기 포트에 응답할 수 없다
중간 홉이 모든 것을 하나의 터널로 보내면, 들어오는 서비스에 대한 응답이 잘못된 경로로 나간다. 인터페이스의 원본 포트 규칙이 이를 고친다.
세 개의 WireGuard 홉이 공인 주소가 없는 네트워크에 도달하고 나면, 중간 장비가 수신해야 할 포트에서 응답을 멈춘다. 앞쪽 터널은 여전히 동작하고, 원격 호스트도 여전히 응답하며, 외부에서 조용해진 것은 중간 서버 자신뿐이다. 이 조합이 헷갈리는 이유는 바로 자초한 것이기 때문이다 — 중간 홉은 자기 라우팅 테이블을 따르고 있고, 그 테이블이 응답을 엉뚱한 방향으로 보낸다.
중간 상자와 이야기하기 전까지 동작하는 체인
예시 체인을 보자 — 클라이언트 A는 UDP 500으로 서버 B에 도달하고, B는 UDP 500으로 서버 C로 이어진다. C는 자신의 유일한 피어 주소에서 오는 트래픽만 받아들이므로 리프가 되어 동작이 단순하게 유지된다. B는 리프가 아니며, 이 고장의 흥미로운 점은 모두 그곳에서 일어난다.
이 구분이 중요한 이유는 리프와 중간 역할이 서로 반대 방향으로 당기기 때문이다. 리프는 자신에게 의존하는 것이 없으므로 모든 것을 하나의 터널로 보내도 문제가 없다. 중간 홉은 클라이언트에게는 종단점이면서 동시에 원격 쪽으로는 클라이언트이며, 두 역할은 서로 다른 라우팅을 원한다.
응답이 터널로 나가는 이유
B에서 C를 향한 인터페이스는 대응하는 기본 경로와 함께 AllowedIPs = 0.0.0.0/0을 지닌다. 따라서 B가 생성하는 모든 패킷은 더 구체적인 것이 우선하지 않는 한 그 터널로 들어간다.
A에서 B로 향한 패킷이 도착해 수락되는데, B가 목적지이고 로컬 전달이 먼저 일어나기 때문이다. 응답은 A의 공인 주소로 향하는 완전히 새로 로컬에서 생성된 패킷이며, 그 목적지는 터널로 가는 기본 경로와 일치한다. C는 이를 받지만 그 주소를 소유하지 않으므로 버린다.
증상은 방화벽 문제나 키 불일치처럼 읽히지만 여기서 둘 다 사실이 아니다. C로 향하는 터널에서 캡처하면 응답이 잘못된 방향으로 가는 것이 보인다. 그 한 번의 관찰이 아무리 설정을 읽는 것보다 시간을 아껴 주므로, 키를 바꾸기 전에 적어 두자. 캡처를 보지 못한 사람은 터널 자체가 고장 났다고 생각할 것이다.
좁은 수리: 원본 포트와 일치하는 정책 규칙
B의 수신 소켓에서 나가는 응답은 원본 포트 500을 지니므로, 정책 규칙으로 이를 일치시켜 해당 패킷을 일반 테이블로 되돌릴 수 있다.
ip rule add sport 500 table main priority 100
ip rule add sport 8080 table main priority 101
정책 규칙은 우선순위 순서로 조회되고 main 테이블은 32766에 있으므로, 100의 규칙은 터널 기본 경로보다 먼저 질의된다. 외부에서 계속 도달 가능해야 하는 포트마다 항목을 하나만 추가하고 그 이상은 두지 않아야, 전달되는 트래픽이 원래 목적한 터널을 계속 사용한다.
이 요령의 한계를 알자. sport 선택자는 로컬에서 생성된 패킷과 일치하며, 이는 응답의 경우를 정확히 덮고 B가 남을 위해 단순히 전달하는 트래픽은 의도적으로 덮지 않는다. B 뒤에서 호스팅되는 서비스에는 여전히 자체 응답이 필요하며, 그 반대로 가장하면 첫 번째 서비스에는 동작하고 두 번째에는 실패하는 수리가 된다.
규칙에 주인을 주자
프롬프트에 입력한 규칙은 다음에 인터페이스가 다시 만들어질 때 사라지므로, WireGuard 설정에서는 인터페이스가 생성되고 제거되는 곳에 두자.
PostUp = ip rule add sport 500 table main priority 100
PostDown = ip rule del sport 500 table main priority 100
인터페이스를 NetworkManager, 배포판 훅 또는 컨테이너 진입점이 소유한다면, 그 쌍은 손으로 고친 파일이 아니라 그 주인에게 속한다. 첫 시작 후에만이 아니라 재연결 후에 유효 규칙을 확인하자 — 첫 시작은 항상 잘 되는 경우이기 때문이다.
대안을 정직하게 비교하자
원본 포트 규칙은 네 가지 출구 중 하나이며 그중 가장 좁다. 터널 정책을 전혀 건드리지 않는다는 장점이 있어 이후 변경에서 살아남는 편이다. 좁은 수리는 적용되지 않게 되면 크게 실패하지만, 넓은 예외는 그 이유가 사라진 뒤에도 오래 조용히 동작한다. 이 차이는 이 년 뒤 다른 누군가가 이 상자를 소유할 때 우아함보다 더 중요하다.
| Approach | What it costs | Where it breaks |
|---|---|---|
| Source-port policy rule | One rule per exposed port | Anything B forwards rather than originates |
| Destination exception route | A route pinned to specific client addresses | Clients whose address changes |
| Stop full-tunnelling on B | Loses the catch-all path toward C | Designs that depend on it |
| Split into two tunnels | More interfaces and more policy | Nothing, at the cost of complexity |
고른 뒤 양쪽 끝에서 검증하자. A에서 B의 포트로 연결을 시작하고 잠시 바쁘게 유지한다. B에서는 ip route get <address-of-A> sport 500 같은 것으로 커널이 응답 경로를 정당화하게 하고, 터널이 아니라 WAN 인터페이스를 지목하는지 확인한다. 그다음 C 뒤의 무언가가 체인을 통해 여전히 응답하는지 확인하는데, 이는 예외가 우회로가 되지 않고 좁게 유지되었음을 증명한다. 마지막으로 포트를 설계 노트에 기록하지 않으면 다음 운영자가 다른 서비스에서 이것을 다시 발견할 것이다.