멀티캐스트는 "보이기만 하면 되는" 캡처와 "받아서 처리하는" 수신이 다르게 동작한다.
캡처(tcpdump · scapy)는 NIC 에 도착한 프레임을 들여다볼 뿐이므로 그룹에 가입하지 않는다. 스위치가 IGMP 스누핑을 켜 두었다면 가입하지 않은 포트로는 트래픽을 내려보내지 않으므로, 캡처를 걸어도 아무것도 잡히지 않는 상황이 생긴다.
수신 프로그램은 IP_ADD_MEMBERSHIP 으로 그룹에 가입하고, 그 결과 IGMP 보고가 나가면서 스위치가 해당 포트로 트래픽을 흘려보낸다. 따라서 "캡처가 안 잡힌다"는 상황에서는 수신 프로그램을 띄워 놓고 캡처하는 것이 올바른 순서다.
tcpdump -i eno2 -nn 'ip multicast and udp'
tcpdump -i eno2 -nn 'host 239.1.1.10 and port 5000' -vv
IGMP 가 오가는지도 함께 본다. 그룹 가입이 실제로 나가고 있는지 알 수 있다.
tcpdump -i eno2 -nn igmp
ip maddr show dev eno2
netstat -gn
그룹 가입 구조체는 그룹 주소와 로컬 인터페이스 주소를 각각 4바이트로 담는다. struct.pack("4sl", ...) 처럼 두 번째 항목을 정수형으로 잡아 놓고 주소를 넣는 예제가 돌아다니는데, 형식과 값이 어긋나 동작하지 않는다. 4s4s 를 쓴다.
import socket
import struct
MCAST_GRP = '239.1.1.10'
MCAST_PORT = 5000
IFACE_IP = '192.0.2.20' # 수신할 NIC 의 IP
sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM, socket.IPPROTO_UDP)
sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
sock.bind(('', MCAST_PORT))
mreq = struct.pack('4s4s', socket.inet_aton(MCAST_GRP), socket.inet_aton(IFACE_IP))
sock.setsockopt(socket.IPPROTO_IP, socket.IP_ADD_MEMBERSHIP, mreq)
try:
while True:
data, addr = sock.recvfrom(65535)
print('%s:%d %d bytes' % (addr[0], addr[1], len(data)))
except KeyboardInterrupt:
pass
finally:
sock.setsockopt(socket.IPPROTO_IP, socket.IP_DROP_MEMBERSHIP, mreq)
sock.close()
세부 사항 몇 가지가 결과를 가른다.
bind 는 그룹 주소가 아니라 빈 문자열(모든 주소)로 하는 편이 이식성이 좋다. 리눅스는 그룹 주소로 bind 하는 것도 허용하지만 다른 OS 에서는 동작이 다르다.
같은 포트를 여러 프로세스가 듣는다면 SO_REUSEADDR 만으로는 부족할 수 있으므로 SO_REUSEPORT 도 함께 켠다.
NIC 가 여러 개면 IFACE_IP 를 반드시 지정한다. 0.0.0.0 으로 두면 라우팅 테이블이 고른 인터페이스로 가입하므로 엉뚱한 NIC 에 붙는다.
리버스 경로 필터가 멀티캐스트 패킷을 버리는 경우가 있다. 송신지가 수신 NIC 의 경로와 다르면 걸린다.
sysctl net.ipv4.conf.eno2.rp_filter
sysctl -w net.ipv4.conf.eno2.rp_filter=2
방화벽이 UDP 포트와 IGMP 를 막고 있는지 확인한다.
firewall-cmd --list-all
라우터를 건너야 한다면 송신 측 TTL 이 1 보다 커야 한다. 기본값이 1 이라 같은 세그먼트를 벗어나지 못하는 경우가 많다.