지식

[Network] IP를 옮겼는데 원격에서만 안 될 때 — 라우터의 스테일 ARP

서버실의 IP 체계를 정리하면서 — 노드들 주소를 연번으로 재배정하는 작업이었다 — 같은 원리의 네트워크 유령을 두 번 만났다. 증상은 매번 사람을 홀리는 모양새였다: 같은 스위치에 물린 이웃 장비에서는 멀쩡히 통신되는데, 라우터 건너 원격에서만 접속이 안 된다. 서버는 분명 살아 있고, 설정도 맞고, 방화벽도 문제없다. 범인은 서버가 아니라 상단 라우터의 ARP 캐시였다. (아래 주소는 전부 문서화용 예시 대역 192.0.2.0/24로 치환했다.)

사례 1 — 같은 IP를 두 장비가 들고 있었다

백업 서버(192.0.2.49)가 갑자기 통째로 접속 불가가 됐다. 그런데 증상이 층층이 어긋나 있었다:

$ ping 192.0.2.49          → 0% 유실, 정상
$ ssh -p 2222 192.0.2.49   → Connection refused   (timeout이 아니라 즉시 거부)
$ nc -z 192.0.2.49 22      → 열려 있음(?)
$ ssh 192.0.2.49           → WARNING: REMOTE HOST IDENTIFICATION HAS CHANGED!

배너를 직접 읽어보니 결정타가 나왔다:

$ bash -c 'exec 3<>/dev/tcp/192.0.2.49/22; head -1 <&3'
SSH-2.0-OpenSSH_7.2 FreeBSD-20160310

우리 서버는 Ubuntu인데 FreeBSD 배너가 온다. 즉 "주소는 살아 있는데 다른 기계가 대답하는" 상태 — 창고에 있던 스토리지 어플라이언스가 부팅하면서 같은 주소를 들고 올라와 ARP 경쟁에서 이긴 것이었다. ping이 정상이었던 건 그 다른 기계가 대답하고 있었기 때문이다. "ping 되니까 서버는 살아 있네"라는 판단이 정확히 반대 결론으로 이끈 셈이다.

사례 2 — 주소를 이사시켰더니 원격에서만 죽었다

몇 달 뒤, 재배정 작업으로 서버 하나를 192.0.2.45에서 192.0.2.37로 이사시켰다. 이사는 깔끔했다 — 같은 L2의 이웃 노드에서 ping·ARP 모두 정상. 그런데 라우터 건너에서 들어오는 원격 접속만 timeout이 났다.

핵심은 .37이 몇 분 전까지 다른 서버의 주소였다는 것. 상단 라우터의 ARP 캐시에는 ".37 = 옛 주인의 MAC"이 남아 있었고, 라우터는 원격에서 온 패킷을 계속 옛 MAC으로 배달했다. 옛 주인의 NIC은 프레임을 받고도 "내 IP 아닌데" 하고 조용히 버린다 — 에러도 응답도 없는 블랙홀이다.

같은 L2의 이웃들이 멀쩡했던 이유: 걔네는 자기 캐시가 비면 그 자리에서 ARP를 새로 물어보고, 새 주인이 대답하니 바로 맞는 MAC을 배운다. 라우터를 거치는 트래픽만 라우터의 낡은 표를 탄다. 그래서 "안에서는 되는데 밖에서만 안 되는" 반쪽 증상이 나온다.

공통 원리 — ARP 캐시는 스스로 안 고쳐진다

두 사례 모두 본질은 하나다: IP↔MAC 대응표(ARP 캐시)는 한 번 배우면 TTL이 만료되거나 다시 물어볼 이유가 생기기 전까지 그냥 믿는다. 장비에 따라 TTL이 몇 시간(일부 라우터는 기본 4시간)이라, 주소의 주인이 바뀌어도 라우터는 한나절 동안 옛 주인에게 배달을 계속할 수 있다.

판별법은 간단하다 — 문제의 서버에서 게이트웨이로 ping을 쏴 본다:

ping -c 3 192.0.2.1     # 게이트웨이
# → 100% 유실이면: 라우터가 내 응답을 엉뚱한 MAC으로 보내고 있다 (스테일 ARP 확정)

해법 — 라우터에게 먼저 인사한다

새 주인이 자기 존재를 라우터에게 알리면 즉시 풀린다. 정석은 gratuitous ARP:

sudo arping -c 4 -A -I eth0 192.0.2.37

arping이 없는 최소 설치 환경이라면 파이썬 raw socket으로도 쏠 수 있다:

import socket, struct
iface, ip = 'eth0', '192.0.2.37'
mac = open(f'/sys/class/net/{iface}/address').read().strip()
m = bytes.fromhex(mac.replace(':','')); i = socket.inet_aton(ip)
eth = b'\xff'*6 + m + b'\x08\x06'
arp = struct.pack('!HHBBH',1,0x0800,6,4,1) + m + i + b'\x00'*6 + i
s = socket.socket(socket.AF_PACKET, socket.SOCK_RAW); s.bind((iface,0))
for _ in range(5): s.send(eth+arp)

그리고 더 쉬운 우회가 하나 있다 — 그냥 게이트웨이로 ping을 보내는 것. ping을 보내려면 서버가 먼저 게이트웨이의 MAC을 ARP로 물어야 하는데, 그 ARP 요청 패킷 안에 발신자의 IP+MAC이 실려 있다. 대부분의 장비는 요청을 받는 것만으로 캐시를 갱신한다. 사례 2는 이 방법으로 몇 초 만에 복구됐다.

재발 방지 — 규칙 세 개

  1. 주소를 새로 넣기 전에, 그 주소가 정말 비었는지 MAC까지 확인한다. 같은 L2에서 ping + ip neigh show <addr> — ping 무응답만으로는 부족하고, ARP 응답의 MAC이 없어야 빈 주소다.
  2. 주소 이동의 마지막 스텝은 항상 새 주소에서 ping -c 3 <게이트웨이>. 남이 쓰던 주소를 물려받는 모든 재배정에서 이 유령은 구조적으로 재발한다. 공짜 한 줄로 예방된다.
  3. "원격만 안 되고 로컬은 된다"는 증상을 보면 서버를 뒤지기 전에 라우터 ARP부터 의심한다. 판별은 게이트웨이 ping 유실률 하나면 끝난다.
조회 1댓글 0

댓글

아직 댓글이 없습니다. 첫 댓글을 남겨보세요.