CloudStack Virtual Router에서 트래픽이 어디서 막히는지 추적하는 방법

CloudStack 운영 시절의 트래픽 추적 메모를 공개 가능한 형태로 정리했습니다. tcpdump, iptables, conntrack, ips.json을 함께 보면서 Public IP, VR, DNAT, VM 중 어디서 막히는지 좁히는 절차입니다.

예전 CloudStack 운영 메모를 정리하다 보면, “고객은 방화벽에서 통과했다고 하고, 포털에서는 포트포워딩이 열려 있고, 그런데 특정 출발지에서만 접속이 안 된다”는 유형이 반복해서 나옵니다. 이런 장애는 말로만 확인하면 계속 공방이 됩니다. 어느 지점에서 패킷이 사라지는지 같은 시각에 찍어야 합니다.

이 글은 원본 메모에서 고객명, 내부 도메인, 실제 IP, 티켓 번호, 담당자명은 모두 제거하고 절차만 남긴 것입니다. 구조는 CloudStack 계열 Virtual Router를 기준으로 잡았지만, NAT gateway나 방화벽 VM을 쓰는 환경에서도 같은 방식으로 응용할 수 있습니다.

1. 먼저 5-tuple과 경로를 고정한다

트래픽 추적은 “접속이 안 된다”로 시작하면 안 됩니다. 출발지, 목적지 공인 IP, 목적지 포트, DNAT 후 VM 사설 IP, VM 포트, VR의 외부/내부 인터페이스를 먼저 고정합니다.

출발지      : 198.51.100.50
목적 공인IP : 203.0.113.10
목적 포트   : tcp/3389
VM 사설IP   : 10.10.0.25
VM 포트     : tcp/3389
VR external : eth2
VR internal : eth0
VR 상태     : MASTER
증상        : 특정 출발지에서만 timeout 또는 RST, 다른 출발지는 정상

운영 중에는 여기에 테스트 시간, 테스트한 출발지, 성공한 출발지, 실패한 출발지를 같이 적습니다. 특정 출발지만 실패하는지, 모든 출발지가 실패하는지에 따라 의심 지점이 완전히 달라집니다.

# CloudStack/VR 계층에서 먼저 확인할 값
cloudmonkey list publicipaddresses ipaddress=203.0.113.10
cloudmonkey list portforwardingrules ipaddressid=<public-ip-id>
cloudmonkey list virtualmachines id=<vm-id>

# VR 안에서 현재 인터페이스와 라우팅 확인
ip -br addr
ip route
cat /proc/net/nf_conntrack 2>/dev/null | head || true
conntrack -S 2>/dev/null || true

2. tcpdump는 VR 양쪽 인터페이스에 동시에 건다

VR external에서만 보면 “들어왔다”까지만 알 수 있고, internal에서만 보면 “VM에 전달됐다”만 알 수 있습니다. 둘을 같은 시간대에 같이 잡아야 NAT 전후가 이어집니다.

CLIENT=198.51.100.50
PUBLIC_IP=203.0.113.10
PRIVATE_IP=10.10.0.25
PORT=3389
STAMP=$(date +%Y%m%d_%H%M%S)

# VR external interface: 고객/인터넷 쪽에서 패킷이 들어오는지 확인
timeout 60 tcpdump -nn -s0 -B 8192 -i eth2   "host ${CLIENT} and host ${PUBLIC_IP} and tcp port ${PORT}"   -w /root/vr-ext-${STAMP}.pcap &

# VR internal interface: DNAT 이후 VM 사설 IP로 넘어가는지 확인
timeout 60 tcpdump -nn -s0 -B 8192 -i eth0   "host ${CLIENT} and host ${PRIVATE_IP} and tcp port ${PORT}"   -w /root/vr-int-${STAMP}.pcap &

wait

tcpdump -nn -r /root/vr-ext-${STAMP}.pcap | head -n 40
tcpdump -nn -r /root/vr-int-${STAMP}.pcap | head -n 40

파일로 저장하는 이유는 단순합니다. 장애 보고나 벤더 분석에서는 화면에 보이는 몇 줄보다 원본 pcap이 훨씬 강한 증거입니다. 짧게 잡더라도 -nn, -s0, -B, -w를 붙여 재분석 가능하게 남깁니다.

3. 패킷 모양으로 구간을 판정한다

같은 포트라도 SYN, SYN-ACK, ACK, RST, FIN이 어디서 보이는지에 따라 원인이 달라집니다. 특히 RST가 VR에서 만들어진 것처럼 보이는지, DNAT 후 VM에서 돌아온 것인지 구분해야 합니다.

external에서 SYN 보임, internal에서 SYN 안 보임
  -> VR 내부의 firewall, DNAT, interface mapping, policy route, conntrack 문제를 우선 본다.

external과 internal 모두 SYN 보임, VM이 RST 응답
  -> 네트워크 경로는 VM까지 도달했다. VM OS 방화벽, 서비스 listen, 애플리케이션 상태를 본다.

external과 internal 모두 SYN/SYN-ACK/ACK 보임, 이후 payload 또는 FIN/RST에서 끊김
  -> L4 연결은 만들어졌다. 서비스 프로토콜, 애플리케이션, 세션 idle timeout, 중간 보안장비를 본다.

external에서 대상 출발지 패킷이 아예 안 보임
  -> VR 이전 구간이다. 상단 방화벽, IPS/WAF, 라우팅, 고객 측 NAT, 회선 구간을 본다.

특정 출발지만 실패하고 다른 출발지는 정상
  -> 전체 NAT/포트포워딩 장애보다 출발지 기반 차단, 비대칭 경로, conntrack stale entry를 먼저 의심한다.

원본 메모 중에는 외부 인터페이스와 내부 인터페이스 양쪽에서 같은 SYN과 RST가 보인 케이스가 있었습니다. 그 경우 “VR이 막았다”가 아니라 패킷이 VM까지 들어갔고 VM 또는 VM 내부 서비스가 RST를 돌려준 것으로 봐야 합니다.

4. iptables는 존재 여부보다 counter를 본다

포트포워딩 rule이 있다는 사실만으로는 부족합니다. 실제 테스트 순간에 해당 rule의 packet/byte counter가 증가하는지 봐야 합니다. counter가 오르지 않으면 패킷이 그 rule을 안 타는 겁니다.

PUBLIC_IP=203.0.113.10
PRIVATE_IP=10.10.0.25
PORT=3389

iptables-save -t nat | grep -E "${PUBLIC_IP}|${PRIVATE_IP}|dport ${PORT}"
iptables-save -t filter | grep -E "${PUBLIC_IP}|${PRIVATE_IP}|dport ${PORT}|FIREWALL"

iptables -t nat -L PREROUTING -nv --line-numbers | grep -E "${PUBLIC_IP}|${PORT}"
iptables -L -nv --line-numbers | grep -E "${PRIVATE_IP}|${PORT}"

# 테스트 전후로 packet/byte counter가 증가하는지 비교한다.
iptables -t nat -L PREROUTING -nv --line-numbers > /root/nat-before.txt
sleep 10
iptables -t nat -L PREROUTING -nv --line-numbers > /root/nat-after.txt
diff -u /root/nat-before.txt /root/nat-after.txt | sed -n '1,120p'

CloudStack VR에서는 DNAT, MARK, CONNMARK, filter accept chain이 같이 움직입니다. 공인 IP 기준 chain은 있는데 DNAT rule의 -i가 현재 active public interface와 다르면, rule이 존재해도 트래픽이 안 탈 수 있습니다.

5. conntrack은 “세션이 만들어졌는지”를 보여준다

정상적으로 3-way handshake가 만들어지면 conntrack에는 원래 방향과 reply 방향이 같이 남습니다. [ASSURED]가 붙은 entry가 있으면 적어도 L4 세션은 양방향으로 본 적이 있다는 뜻입니다.

CLIENT=198.51.100.50
PUBLIC_IP=203.0.113.10
PRIVATE_IP=10.10.0.25
PORT=3389

conntrack -L -p tcp 2>/dev/null | grep -E "${CLIENT}|${PUBLIC_IP}|${PRIVATE_IP}|dport=${PORT}|sport=${PORT}" | head -n 80

cat /proc/sys/net/netfilter/nf_conntrack_count 2>/dev/null
cat /proc/sys/net/netfilter/nf_conntrack_max 2>/dev/null
conntrack -S 2>/dev/null

특정 출발지는 실패하고 다른 출발지는 정상인데, 정상 출발지의 conntrack만 보인다면 NAT rule 전체보다는 출발지 기반 차단이나 upstream 경로를 의심합니다. 반대로 conntrack table 사용률이 높고 VR packet drop 경보가 같이 있으면 conntrack 고갈이나 과다 트래픽을 먼저 봐야 합니다.

6. ips.json과 active interface 불일치를 확인한다

운영 메모에서 꽤 중요한 케이스가 있었습니다. VR의 public subnet은 현재 active interface에 붙어 있는데, /etc/cloudstack/ips.json에 과거 non-active interface 정보가 남아 있고, rule 생성 로직이 그쪽을 먼저 읽어 잘못된 interface로 DNAT/MARK rule을 만든 경우입니다.

PUBLIC_IP=203.0.113.10

ip -br addr
iptables-save -t nat | grep "${PUBLIC_IP}"

# ips.json 안에서 같은 public subnet이 active/add:true 인터페이스에 남아 있는지 확인한다.
jq '.[]? // empty' /etc/cloudstack/ips.json >/dev/null 2>&1 && jq --arg ip "${PUBLIC_IP}" '
  to_entries[]
  | {device: .key, values: .value}
  | .values[]
  | select((.public_ip // .cidr // "") | tostring | contains($ip))
  | {device, add: (.ADD // .add), cidr, network, public_ip, source_nat}
' /etc/cloudstack/ips.json

# jq가 없으면 최소한 public IP와 주변 라인을 본다.
grep -n -C 6 "${PUBLIC_IP}" /etc/cloudstack/ips.json
정상이어야 하는 모양
  - public subnet은 eth2에 있고 eth2 항목이 add:true
  - DNAT/MARK/CONNMARK rule도 -i eth2 기준으로 생성

문제였던 모양
  - public subnet은 eth2에서 쓰는데 ips.json의 과거 eth3 항목도 같은 subnet으로 남아 있음
  - 과거 eth3 항목은 add:false인데 rule 생성 로직이 먼저 읽음
  - DNAT/MARK rule이 non-active interface 기준으로 생성
  - external tcpdump와 iptables counter가 맞지 않거나 특정 포트포워딩만 실패

판정
  - 현재 ip a의 active interface와 iptables-save의 -i interface가 다르면 VR 재생성/절체 또는 버전 패치 여부를 본다.

7. VR 상태와 과다 트래픽은 같이 본다

VR redundant state, keepalived 상태, eth fault, conntrack 사용량, cnode 성능을 같이 봐야 합니다. 특히 대량 트래픽이 유입 중일 때는 VR 재기동이나 절체를 먼저 하면 더 큰 장애로 번질 수 있습니다. 트래픽이 자연 해소됐거나 상단에서 차단됐는지 확인한 뒤 조치해야 합니다.

grep -i 'Entering .* STATE' /var/log/messages | tail -n 40
systemctl status keepalived --no-pager 2>/dev/null || true
ip addr show | grep -E 'state|inet '

cat /proc/sys/net/netfilter/nf_conntrack_count 2>/dev/null
cat /proc/sys/net/netfilter/nf_conntrack_max 2>/dev/null
dmesg -T | grep -Ei 'vif|rx stalled|disabled state|conntrack|martian|drop' | tail -n 80

원본 메모에도 conntrack 과다, packet drop, VR eth daemon, redundant state 경보가 같은 묶음으로 등장합니다. 이 경보들은 “NAT rule이 틀렸다”와 다른 계층입니다. 포트포워딩 장애처럼 보여도 실제로는 VR이 과다 세션을 처리하느라 지연되거나 drop하는 상황일 수 있습니다.

8. zone 간 CIP 통신은 게스트 인터페이스까지 본다

공인 IP 포트포워딩이 아니라 사설망/CIP 간 통신 불가라면 VR NAT보다 VM 내부 인터페이스, host VIF, hypervisor 로그를 같이 봐야 합니다. 메모 중에는 특정 시간대 이후 VM의 인터페이스 down/up으로 통신이 회복된 케이스도 있었습니다. 이때는 고객 OS 이벤트, sosreport, host의 VIF 로그를 함께 묶어야 원인 분석이 됩니다.

  • VM 안에서 ip addr, ip route, ss -lntp, OS firewall 상태 확인
  • hypervisor/cnode에서 VIF disabled, guest rx stalled, link state 변경 로그 확인
  • VM 내부 tcpdump와 host/VR tcpdump를 같은 시각에 비교
  • 인터페이스 down/up으로 회복됐다면 회복 시간 전후 로그를 따로 보존

9. 장애 보고는 구간 판정으로 끝내야 한다

좋은 보고는 “확인했습니다”가 아니라 “여기까지는 통과했고 여기서 끊겼다”로 끝납니다. 아래 형식이면 고객, 보안장비 담당, 플랫폼 담당이 같은 증거를 보고 다음 액션을 정할 수 있습니다.

장애 증상
  - 198.51.100.50 -> 203.0.113.10:3389 접속 실패
  - 다른 출발지에서는 정상 또는 일부 포트만 정상

동시 캡처 결과
  - VR external: SYN 관측 여부
  - VR internal: DNAT 후 10.10.0.25:3389 전달 여부
  - VM 응답: SYN-ACK / RST / 무응답

NAT/FILTER 확인
  - DNAT rule 존재 여부
  - filter accept/drop rule 존재 여부
  - 테스트 전후 counter 증가 여부
  - rule interface와 active interface 일치 여부

conntrack 확인
  - ESTABLISHED / SYN_SENT / TIME_WAIT / ASSURED 상태
  - mark/zone 값
  - nf_conntrack_count 대비 max 사용률

판정
  - VR 이전 구간 / VR NAT-filter 구간 / VM OS 구간 / 애플리케이션 구간 중 어디인지
  - 임시 조치와 영구 조치
  - 재현 테스트 결과

정리

CloudStack Virtual Router 네트워크 장애는 포털 설정만 보고 판단하면 늦습니다. 먼저 5-tuple을 고정하고, VR external/internal에서 동시에 tcpdump를 잡고, iptables counter와 conntrack을 같은 시간대에 대조합니다. 패킷이 VR에 안 들어오면 상단 구간, external에는 있는데 internal로 안 가면 VR NAT/filter/interface mapping, VM까지 갔는데 RST가 오면 VM OS나 서비스, 세션은 생기는데 지연되면 애플리케이션 또는 과다 트래픽/conntrack 구간으로 좁히면 됩니다.

BGM EVER