ICMP 프로토콜 심화
Linux 커널 ICMP 프로토콜을 심층 분석합니다. Echo Request/Reply 처리, Destination Unreachable/Time Exceeded 같은 오류 메시지 생성 조건, ICMP rate limiting 정책, ping/traceroute가 커널에서 해석되는 방식, 네트워크 장애 시 ICMP를 이용한 경로 진단과 필터링 정책 설계까지 운영 실무 관점으로 정리합니다.
핵심 요약
- 패킷 수명주기 — ingress, 처리, egress 경로를 연결합니다.
- 큐/버퍼 모델 — sk_buff와 큐 지점의 역할을 분리합니다.
- 정책/데이터 분리 — 제어 평면과 데이터 평면을 구분합니다.
- 성능 지표 — PPS, 지연, 드롭 원인을 함께 분석합니다.
- 오프로딩 경계 — NIC/XDP/DPDK 경계를 명확히 유지합니다.
단계별 이해
- 경로 고정
문제가 발생한 ingress/egress 지점을 먼저 특정합니다. - 큐 관찰
백로그와 드롭 위치를 계측합니다. - 정책 반영 확인
라우팅/필터 변경이 데이터 경로에 반영됐는지 봅니다. - 부하 검증
실제 트래픽 패턴에서 재현성을 확인합니다.
ICMP 심화
ICMP(Internet Control Message Protocol, IP 프로토콜 1)는 IP 네트워크의 제어 평면 프로토콜입니다. 패킷 전달 실패 보고, 경로 변경 알림, 연결 진단(ping/traceroute) 등 네트워크 운영의 핵심 기능을 담당합니다. RFC 792(IPv4 ICMP)와 RFC 4443(ICMPv6)에 정의되어 있으며, 커널의 net/ipv4/icmp.c와 net/ipv6/icmp.c에 구현되어 있습니다.
- 프로토콜 번호: IPv4 = 1 (
IPPROTO_ICMP), IPv6 = 58 (IPPROTO_ICMPV6) - 전송 계층이 아님: IP 위에 직접 동작하지만, 포트 개념 없음 — 라우터와 호스트 모두 처리
- 커널 헤더:
<linux/icmp.h>,<uapi/linux/icmp.h>,<net/icmp.h>
ICMP 헤더 구조와 커널 구조체
/* include/uapi/linux/icmp.h — ICMP 헤더 (고정 8바이트) */
struct icmphdr {
__u8 type; /* 메시지 타입 (0-255) */
__u8 code; /* 타입별 세부 코드 */
__sum16 checksum; /* ICMP 헤더 + 데이터 전체의 체크섬 */
union {
struct {
__be16 id; /* Echo: 식별자 (프로세스 구분) */
__be16 sequence; /* Echo: 시퀀스 번호 */
} echo;
__be32 gateway; /* Redirect: 게이트웨이 주소 */
struct {
__be16 __unused;
__be16 mtu; /* Frag Needed: 다음 홉 MTU */
} frag;
__u8 reserved[4]; /* 기타 타입에서 사용 */
} un;
};
/*
* ICMP 패킷 구조 (에러 메시지의 경우):
*
* ┌──────────────────────────────────┐
* │ IP Header (20+ bytes) │ ← 외부 IP 헤더
* ├──────────────────────────────────┤
* │ ICMP Header (8 bytes) │ ← type, code, checksum, un
* ├──────────────────────────────────┤
* │ Original IP Header (20+ bytes) │ ← 에러 유발 패킷의 IP 헤더
* │ + Original L4 Header (8+ bytes) │ ← 원본 TCP/UDP 헤더 (포트 포함)
* └──────────────────────────────────┘
*
* → 에러 ICMP는 원본 패킷 헤더를 포함해 어떤 연결에서 발생한 에러인지 식별 가능
* → RFC 1122: 최소 원본 IP 헤더 + 8바이트 포함 필수
*/
ICMP 메시지 타입/코드 종합
| 타입 | 이름 | 주요 코드 | 용도 | 커널 처리 함수 |
|---|---|---|---|---|
| 0 | Echo Reply | 0 | ping 응답 | ping_rcv() |
| 3 | Destination Unreachable | 0: Net 1: Host 2: Protocol 3: Port 4: Frag Needed (DF set) 13: Filtered |
패킷 전달 불가 보고 | icmp_unreach() |
| 4 | Source Quench | 0 | (폐기) 혼잡 알림 | 무시 (RFC 6633) |
| 5 | Redirect | 0: Network 1: Host 2: TOS+Net 3: TOS+Host |
더 나은 경로 알림 | icmp_redirect() |
| 8 | Echo Request | 0 | ping 요청 | icmp_echo() |
| 11 | Time Exceeded | 0: TTL expired 1: Frag reassembly timeout |
TTL 만료 / 재조합 실패 | icmp_unreach() |
| 12 | Parameter Problem | 0: Pointer 1: Missing option 2: Bad length |
헤더 오류 보고 | icmp_unreach() |
| 13/14 | Timestamp / Reply | 0 | 시간 동기화 (거의 미사용) | icmp_timestamp() |
에러 vs 조회 메시지: ICMP 메시지는 두 범주로 나뉩니다. 에러 메시지(Type 3, 4, 5, 11, 12)는 다른 패킷의 처리 실패를 보고하며, 원본 패킷의 IP 헤더+8바이트를 페이로드에 포함합니다. 조회 메시지(Type 0/8, 13/14)는 요청-응답 쌍으로 네트워크 진단에 사용됩니다. 에러 메시지에 대해 ICMP 에러를 생성하지 않는 것이 핵심 규칙입니다 (무한 루프 방지).
ICMP 수신 경로 (icmp_rcv)
/* net/ipv4/icmp.c — ICMP 수신 진입점 */
int icmp_rcv(struct sk_buff *skb)
{
struct icmphdr *icmph;
struct net *net = dev_net(skb->dev);
/* 1. 체크섬 검증 */
if (skb_checksum_simple_validate(skb))
goto csum_error;
icmph = icmp_hdr(skb);
/* 2. 브로드캐스트/멀티캐스트 ICMP 처리 */
if (skb->pkt_type != PACKET_HOST) {
/* Echo Request to broadcast: icmp_echo_ignore_broadcasts 확인 */
if (icmph->type == ICMP_ECHO &&
net->ipv4.sysctl_icmp_echo_ignore_broadcasts)
goto drop;
}
/* 3. icmp_pointers[] 디스패치 테이블로 타입별 핸들러 호출 */
if (icmph->type < NR_ICMP_TYPES) {
int ret = icmp_pointers[icmph->type].handler(skb);
return ret;
}
goto drop;
}
/* icmp_pointers[] — 타입별 핸들러 디스패치 테이블 */
static const struct icmp_control icmp_pointers[NR_ICMP_TYPES + 1] = {
[ICMP_ECHOREPLY] = { .handler = ping_rcv, },
[ICMP_DEST_UNREACH] = { .handler = icmp_unreach, .error = 1, },
[ICMP_SOURCE_QUENCH] = { .handler = icmp_unreach, .error = 1, },
[ICMP_REDIRECT] = { .handler = icmp_redirect,.error = 1, },
[ICMP_ECHO] = { .handler = icmp_echo, },
[ICMP_TIME_EXCEEDED] = { .handler = icmp_unreach, .error = 1, },
[ICMP_PARAMETERPROB] = { .handler = icmp_unreach, .error = 1, },
[ICMP_TIMESTAMP] = { .handler = icmp_timestamp, },
[ICMP_TIMESTAMPREPLY] = { .handler = ping_rcv, },
/* ... 나머지는 icmp_discard()로 무시 */
};
/* icmp_control.error = 1인 타입은 ICMP 에러 메시지
* → icmp_unreach()가 원본 패킷 정보를 추출해
* 상위 프로토콜(TCP/UDP)의 에러 핸들러에 전달
*/
icmp_unreach() — 에러 메시지 처리
/* net/ipv4/icmp.c — Destination Unreachable / Time Exceeded 처리 */
static bool icmp_unreach(struct sk_buff *skb)
{
struct icmphdr *icmph = icmp_hdr(skb);
struct iphdr *iph; /* 에러 유발 원본 패킷의 IP 헤더 */
/* 1. ICMP 페이로드에서 원본 IP 헤더 추출 */
iph = (struct iphdr *)skb->data;
/* 2. Fragmentation Needed (Type 3, Code 4) → PMTUD 처리 */
if (icmph->type == ICMP_DEST_UNREACH &&
icmph->code == ICMP_FRAG_NEEDED) {
/* Path MTU 업데이트:
* icmph->un.frag.mtu에 다음 홉 MTU가 포함됨
* → ip_rt_frag_needed()로 라우팅 캐시 MTU 갱신 */
ipv4_update_pmtu(skb, net, ntohs(icmph->un.frag.mtu),
iph->daddr);
}
/* 3. 원본 IP 헤더의 프로토콜 번호로 상위 에러 핸들러 호출 */
protocol = iph->protocol;
ipprot = rcu_dereference(inet_protos[protocol]);
if (ipprot && ipprot->err_handler)
ipprot->err_handler(skb, /* info */);
/* TCP → tcp_v4_err(): 연결 RST, 재전송 등 처리
* UDP → udp_err(): 소켓에 에러 전파
* SCTP → sctp_v4_err(): association 에러 처리 */
/* 4. SNMP 카운터 갱신 */
__ICMP_INC_STATS(net, ICMP_MIB_INDESTUNREACHS);
}
/* TCP가 ICMP Destination Unreachable를 받았을 때:
* - Code 0,1 (Net/Host Unreachable):
* → soft error 기록 (즉시 종료하지 않음)
* → 재전송 타이머 만료 시 EHOSTUNREACH 반환
* - Code 2 (Protocol Unreachable):
* → 연결 RST (상대방에 TCP 스택 없음)
* - Code 3 (Port Unreachable):
* → TCP에서는 일반적으로 무시 (TCP는 RST 사용)
* - Code 4 (Frag Needed):
* → MSS 조정 후 재전송 (Path MTU Discovery)
* - Code 13 (Admin Filtered):
* → soft error (방화벽 차단)
*/
ICMP 전송 메커니즘 (icmp_send)
/* net/ipv4/icmp.c — ICMP 에러 메시지 전송 */
void icmp_send(struct sk_buff *skb_in,
int type, int code, __be32 info)
{
struct iphdr *iph = ip_hdr(skb_in);
/* ===== RFC 1122 규칙: ICMP 에러를 보내지 않는 경우 ===== */
/* 규칙 1: ICMP 에러 메시지에 대해 ICMP 에러를 보내지 않음
* → 무한 루프 방지 */
if (icmp_is_err_type(type) &&
iph->protocol == IPPROTO_ICMP) {
struct icmphdr *inner = icmp_hdr(skb_in);
if (icmp_pointers[inner->type].error)
return; /* 원본이 ICMP 에러 → 이중 에러 금지 */
}
/* 규칙 2: 첫 번째 단편이 아닌 패킷에 대해 보내지 않음 */
if (ntohs(iph->frag_off) & IP_OFFSET)
return;
/* 규칙 3: 브로드캐스트/멀티캐스트 목적지에 대해 보내지 않음 */
if (skb_in->pkt_type != PACKET_HOST &&
skb_in->pkt_type != PACKET_OUTGOING)
return;
/* 규칙 4: 소스 주소가 0.0.0.0이면 보내지 않음 */
if (!iph->saddr)
return;
/* ===== Rate Limiting 확인 ===== */
if (!icmpv4_global_allow(net, type, code))
return;
if (!icmpv4_xrlim_allow(net, type, code, skb_in))
return;
/* ===== ICMP 패킷 구성 및 전송 ===== */
/* per-CPU icmp_sk 소켓 사용 (락 경쟁 최소화) */
sk = icmp_sk(net);
/* 에러 유발 패킷의 IP 헤더 + 8바이트를 페이로드에 복사 */
room = dst_mtu(dst) - sizeof(struct iphdr)
- sizeof(struct icmphdr);
/* RFC 4884: 가능하면 더 많은 원본 데이터 포함 */
icmp_push_reply(sk, &icmp_param, &fl4, &ipc);
}
Echo Request/Reply 구현 (ping)
/* net/ipv4/icmp.c — Echo Request 처리 */
static bool icmp_echo(struct sk_buff *skb)
{
struct net *net = dev_net(skb->dev);
struct icmphdr *icmph = icmp_hdr(skb);
/* sysctl로 Echo 응답 비활성화 가능 */
if (net->ipv4.sysctl_icmp_echo_ignore_all)
return true;
/* Echo Reply 구성: type=0, id/seq 그대로 복사, 데이터 복사 */
icmp_param.data.icmph = *icmph;
icmp_param.data.icmph.type = ICMP_ECHOREPLY;
icmp_param.skb = skb;
/* icmp_reply()로 응답 전송 (소스 주소 = 수신 주소) */
icmp_reply(&icmp_param, skb);
return true;
}
/* ===== ping 소켓 (IPPROTO_ICMP) ===== */
/* 커널 3.0+: 비특권 사용자도 ping 가능
*
* 기존: raw socket(SOCK_RAW) 필요 → CAP_NET_RAW 권한 필수
* 현재: SOCK_DGRAM + IPPROTO_ICMP → "ping socket" 자동 생성
*
* net.ipv4.ping_group_range = "0 2147483647"
* → 모든 GID의 사용자가 ping 가능
* → setuid 없이 /bin/ping 실행
*
* 커널 처리:
* - id 필드를 소켓 포트처럼 사용 (소켓 demux)
* - Echo Reply를 해당 소켓으로 직접 전달 (ping_rcv)
*/
int fd = socket(AF_INET, SOCK_DGRAM, IPPROTO_ICMP);
/* → 커널이 자동으로 id 할당, Echo Request 전송 시:
* sendto(fd, payload, len, 0, &dst, sizeof(dst));
* → 커널이 ICMP 헤더 구성 (type=8, code=0, id=소켓 id)
* recvfrom(fd, buf, sizeof(buf), 0, ...);
* → Echo Reply 수신 (type=0 응답만 필터링됨) */
ICMP Rate Limiting 메커니즘
/* net/ipv4/icmp.c — 전역 Rate Limiting */
static bool icmpv4_global_allow(struct net *net, int type, int code)
{
/* Token Bucket 알고리즘 기반
*
* net.ipv4.icmp_msgs_per_sec (기본: 1000)
* → 초당 최대 ICMP 메시지 전송 수
* → 토큰이 이 속도로 리필됨
*
* net.ipv4.icmp_msgs_burst (기본: 50)
* → 버스트 허용 크기 (토큰 버킷 최대 토큰 수)
*
* 동작: 토큰이 있으면 전송 허용 + 토큰 1개 소비
* 토큰이 없으면 ICMP 전송 억제
*/
if (icmp_global_allow())
return true;
__ICMP_INC_STATS(net, ICMP_MIB_RATELIMITGLOBAL);
return false;
}
/* 목적지별 Rate Limiting */
static bool icmpv4_xrlim_allow(struct net *net, int type, int code,
struct sk_buff *skb)
{
struct dst_entry *dst = skb_dst(skb);
/* net.ipv4.icmp_ratelimit (기본: 1000 ms)
* → 동일 목적지에 대한 ICMP 에러 최소 간격
*
* net.ipv4.icmp_ratemask (기본: 6168 = 0x1818)
* → rate limit 적용 대상 ICMP 타입 비트마스크
* → 비트가 설정된 타입만 rate limiting 적용
* → 기본값: Type 3 (Dest Unreach), 11 (Time Exceeded), 12 (Param Problem)
* → Type 0 (Echo Reply), 8 (Echo)은 기본적으로 rate limit 미적용
*/
/* Destination Unreachable(Type 3)은 항상 rate limit */
if (type == ICMP_DEST_UNREACH)
return dst_output_okfn(...); /* per-route rate check */
/* Echo Reply는 rate limit 미적용 (별도 제어) */
if (type == ICMP_ECHOREPLY)
return true;
return inet_peer_xrlim_allow(dst, net->ipv4.sysctl_icmp_ratelimit);
}
Path MTU Discovery (PMTUD)
/* PMTUD: IP 경로의 최소 MTU를 동적으로 탐지
*
* 동작 원리:
* 1. 송신자가 DF(Don't Fragment) 비트를 설정해 패킷 전송
* 2. 경로상 라우터가 패킷 > 자신의 MTU이면:
* → ICMP Type 3 Code 4 (Fragmentation Needed) + next-hop MTU 반환
* 3. 송신자가 Path MTU를 줄이고 재전송
* 4. 종단까지 도달할 때까지 반복
*
* ┌──────┐ MTU 1500 ┌──────┐ MTU 1400 ┌──────┐
* │ Host ├────────────────▶│Router├──────────────▶│ Host │
* │ A │ │ R1 │ ▲ │ B │
* └──────┘ └──────┘ │ └──────┘
* ▲ │
* │ ICMP Frag Needed (MTU=1400) │
* └─────────────────────────────────┘
*/
/* net/ipv4/route.c — PMTUD: MTU 갱신 */
void ipv4_update_pmtu(struct sk_buff *skb, struct net *net,
u32 mtu, __be32 daddr)
{
struct rtable *rt;
/* mtu 유효성 검사: 최소 68바이트 (RFC 791) */
if (mtu < 68)
return;
/* 라우팅 캐시에서 해당 목적지의 PMTU 갱신 */
rt = ip_route_output(net, daddr, ...);
if (rt) {
rt_update_pmtu(rt, mtu);
/* → dst_entry->metrics[RTAX_MTU]를 mtu로 설정
* → PMTU 만료 타이머 시작 (기본 10분)
* → 만료 시 원래 interface MTU로 복귀 (경로 변경 감지) */
}
}
/* TCP에서의 PMTUD 연동 */
/* tcp_v4_err()가 Frag Needed 수신 시:
* 1. tp->mtu_info = mtu (새 Path MTU 저장)
* 2. tcp_sync_mss(sk, mtu) 호출
* → MSS = mtu - IP header - TCP header
* 3. 현재 전송 큐의 세그먼트를 새 MSS로 재분할
* 4. 재전송 트리거 (cwnd는 유지)
*/
/* PMTUD 문제와 대안 */
/* 문제: ICMP Frag Needed가 방화벽에서 차단되면 PMTUD 실패
* → "PMTUD Black Hole": 패킷이 무한 드롭
*
* 대안 1: TCP MSS Clamping
* iptables -A FORWARD -p tcp --tcp-flags SYN,RST SYN \
* -j TCPMSS --clamp-mss-to-pmtu
* → SYN의 MSS 옵션을 경로 MTU에 맞춤 (ICMP 불필요)
*
* 대안 2: PLPMTUD (RFC 8899, Packetization Layer PMTUD)
* → ICMP에 의존하지 않고 프로브 패킷으로 MTU 탐색
* → TCP: net.ipv4.tcp_mtu_probing = 1
* → SCTP, QUIC도 지원
*
* 대안 3: net.ipv4.ip_no_pmtu_disc = 1
* → PMTUD 비활성화 (DF 비트 미설정 → IP 단편화 허용)
* → 성능 저하 주의
*/
PMTUD Black Hole 감지: net.ipv4.tcp_mtu_probing = 1을 설정하면, TCP 재전송이 반복될 때 커널이 자동으로 MSS를 줄여가며 프로브합니다 (tcp_base_mss부터 시작). = 2이면 초기 연결부터 프로빙을 시작합니다. 이는 ICMP가 차단된 환경(많은 클라우드/기업 네트워크)에서 강력히 권장되는 설정입니다.
ICMP Redirect와 라우팅 캐시
/* ICMP Redirect (Type 5):
* 라우터가 "너에게 더 좋은 next-hop이 있다"고 알리는 메시지
*
* 발생 조건: 라우터가 패킷을 수신한 인터페이스와 동일한 인터페이스로
* 포워딩해야 할 때 (더 직접적인 경로가 존재)
*
* ┌──────┐ default gw=R1 ┌──────┐
* │ Host ├───────────────────▶│ R1 │
* │ A │◀─── Redirect ─────┤ │
* └──┬───┘ (use R2 for dst) └──────┘
* │ │
* │ direct route to dst │
* │ ┌──────┐ │
* └────────▶│ R2 │◀─────────┘
* └──────┘
*/
/* net/ipv4/icmp.c — ICMP Redirect 수신 */
static bool icmp_redirect(struct sk_buff *skb)
{
struct icmphdr *icmph = icmp_hdr(skb);
__be32 new_gw = icmph->un.gateway; /* 새 게이트웨이 */
/* 보안 검증:
* 1. Redirect 소스가 현재 게이트웨이인지 확인
* 2. 새 게이트웨이가 같은 서브넷에 있는지 확인
* 3. 새 게이트웨이가 멀티캐스트/브로드캐스트가 아닌지 확인
*/
if (!ip_route_input(skb, iph->daddr, iph->saddr, ...))
goto reject;
/* 라우팅 테이블에 redirect 경로 추가 */
ip_rt_redirect(new_gw, iph->daddr, iph->saddr, skb->dev);
return true;
}
/* ICMP Redirect 관련 sysctl */
/*
* net.ipv4.conf.{iface}.accept_redirects
* = 1: Redirect 수신 허용 (호스트 기본값)
* = 0: 무시 (라우터/보안 환경 권장)
*
* net.ipv4.conf.{iface}.secure_redirects
* = 1: 기본 게이트웨이에서 온 Redirect만 수락 (기본)
* = 0: 모든 게이트웨이의 Redirect 수락 (위험)
*
* net.ipv4.conf.{iface}.send_redirects
* = 1: 포워딩 시 Redirect 전송 (라우터 기본값)
* = 0: Redirect 전송 안 함
*
* 보안 경고: ICMP Redirect는 MITM 공격에 악용 가능
* → 서버/라우터에서는 반드시 accept_redirects=0 설정
* → IPv6: net.ipv6.conf.{iface}.accept_redirects = 0
*/
커널 ICMP 소켓과 per-CPU 구조
/* net/ipv4/icmp.c — per-CPU ICMP 소켓 */
/* 커널은 ICMP 전송을 위해 네트워크 네임스페이스 + CPU별 전용 소켓을 유지
*
* 이유: icmp_send()는 softirq 컨텍스트에서 호출될 수 있으므로
* 소켓 할당/해제 오버헤드를 줄이고 락 경쟁을 방지
*
* 초기화: icmp_init() → icmp_sk_init() (네임스페이스별)
*/
struct icmp_bxm { /* ICMP 빌드 + 전송 매개변수 */
struct sk_buff *skb; /* 에러 유발 원본 패킷 */
int offset; /* 데이터 오프셋 */
int data_len; /* 복사할 원본 데이터 길이 */
struct {
struct icmphdr icmph; /* 전송할 ICMP 헤더 */
__be32 times[3]; /* timestamp용 */
} data;
int head_len; /* 헤더 길이 */
struct ip_options_data replyopts; /* IP 옵션 복사 */
};
/* icmp_reply() vs icmp_send():
* - icmp_reply(): 수신된 ICMP에 대한 응답 (Echo Reply 등)
* → 소스 주소 = 수신 패킷의 목적지 주소
* → 목적지 = 수신 패킷의 소스 주소
*
* - icmp_send(): 에러 ICMP 생성 (Dest Unreach 등)
* → 소스 주소 = 에러를 감지한 인터페이스의 주소
* → 목적지 = 에러 유발 패킷의 소스 주소
* → 원본 패킷의 IP 헤더 + 8바이트를 페이로드에 포함
*/
ICMPv6 심화
/* include/uapi/linux/icmpv6.h — ICMPv6 헤더 */
struct icmp6hdr {
__u8 icmp6_type; /* 메시지 타입 */
__u8 icmp6_code; /* 타입별 코드 */
__sum16 icmp6_cksum; /* 체크섬 (IPv6 pseudo-header 포함!) */
union {
__be32 un_data32[1];
__be16 un_data16[2];
__u8 un_data8[4];
struct icmpv6_echo u_echo; /* id + seq */
struct icmpv6_nd_advt u_nd_advt; /* NDP 광고 플래그 */
struct icmpv6_nd_ra u_nd_ra; /* Router Advert */
} icmp6_dataun;
};
/* ICMPv6 vs ICMPv4 주요 차이:
* 1. 체크섬에 IPv6 pseudo-header 포함 (ICMPv4는 ICMP 자체만)
* 2. 에러 메시지: 타입 0-127, 정보 메시지: 타입 128-255
* 3. ICMPv6가 ARP/IGMP 역할 흡수 (NDP, MLD)
* 4. Path MTU Discovery: Type 2 (Packet Too Big)
*/
| 타입 | 이름 | ICMPv4 대응 | 용도 |
|---|---|---|---|
| 1 | Destination Unreachable | Type 3 | 전달 불가 (no route, admin prohibited, port unreach 등) |
| 2 | Packet Too Big | Type 3 Code 4 | PMTUD — IPv6에서는 라우터가 단편화하지 않으므로 필수 |
| 3 | Time Exceeded | Type 11 | Hop Limit 만료 / 재조합 타임아웃 |
| 4 | Parameter Problem | Type 12 | 헤더 필드 오류 / 인식 불가 Next Header |
| 128/129 | Echo Request/Reply | Type 8/0 | ping6 |
| 130-132 | MLD (v1) | IGMP | 멀티캐스트 리스너 관리 |
| 133-137 | NDP (RS/RA/NS/NA/Redirect) | ARP + ICMP Redirect | 주소 해석, 라우터 발견, DAD, SLAAC |
| 143 | MLDv2 | IGMPv3 | 소스 특정 멀티캐스트 그룹 관리 |
/* net/ipv6/icmp.c — ICMPv6 수신 */
int icmpv6_rcv(struct sk_buff *skb)
{
struct icmp6hdr *hdr = icmp6_hdr(skb);
/* ICMPv6 체크섬 검증 (pseudo-header 포함) */
if (skb_checksum_validate(skb, IPPROTO_ICMPV6, ...))
goto csum_error;
switch (hdr->icmp6_type) {
case ICMPV6_ECHO_REQUEST:
if (net->ipv6.sysctl.icmpv6_echo_ignore_all)
break;
icmpv6_echo_reply(skb);
break;
case ICMPV6_PKT_TOOBIG:
/* IPv6 PMTUD: Packet Too Big → Path MTU 갱신 */
icmpv6_notify(skb, hdr->icmp6_type, hdr->icmp6_code,
hdr->icmp6_mtu);
break;
case NDISC_ROUTER_SOLICITATION:
case NDISC_ROUTER_ADVERTISEMENT:
case NDISC_NEIGHBOUR_SOLICITATION:
case NDISC_NEIGHBOUR_ADVERTISEMENT:
case NDISC_REDIRECT:
ndisc_rcv(skb); /* NDP 서브시스템으로 전달 */
break;
case ICMPV6_MGM_QUERY:
case ICMPV6_MGM_REPORT:
case ICMPV6_MGM_REDUCTION:
case ICMPV6_MLD2_REPORT:
igmp6_event_query(skb); /* MLD → 멀티캐스트 그룹 관리 */
break;
case ICMPV6_DEST_UNREACH:
case ICMPV6_TIME_EXCEED:
case ICMPV6_PARAMPROB:
icmpv6_notify(skb, ...); /* 상위 프로토콜에 에러 전파 */
break;
}
}
traceroute와 ICMP
/* traceroute 동작 원리:
*
* 방법 1: UDP 기반 (전통적 Unix traceroute)
* → 높은 포트 번호(33434+)로 UDP 패킷 전송, TTL을 1부터 증가
* → 각 홉에서 TTL 만료 시 ICMP Time Exceeded (Type 11, Code 0) 반환
* → 최종 목적지는 ICMP Port Unreachable (Type 3, Code 3) 반환
*
* 방법 2: ICMP 기반 (Windows tracert, traceroute -I)
* → ICMP Echo Request(Type 8) 전송, TTL을 1부터 증가
* → 각 홉에서 TTL 만료 시 ICMP Time Exceeded 반환
* → 최종 목적지는 ICMP Echo Reply (Type 0) 반환
*
* 방법 3: TCP SYN 기반 (traceroute -T, tcptraceroute)
* → TCP SYN 패킷 전송 (포트 80 등), TTL을 1부터 증가
* → 방화벽을 더 잘 통과함
*
* 커널에서 TTL 만료 처리:
*/
/* net/ipv4/ip_forward.c — TTL 감소와 만료 처리 */
int ip_forward(struct sk_buff *skb)
{
struct iphdr *iph = ip_hdr(skb);
/* TTL 검사 */
if (iph->ttl <= 1) {
/* TTL 만료 → ICMP Time Exceeded 전송 */
icmp_send(skb, ICMP_TIME_EXCEEDED, ICMP_EXC_TTL, 0);
__IP_INC_STATS(net, IPSTATS_MIB_INHDRERRORS);
kfree_skb(skb);
return NET_RX_DROP;
}
/* TTL 감소 */
ip_decrease_ttl(iph);
/* ... 라우팅 후 전송 */
}
/* ICMP Time Exceeded 응답에 포함되는 정보:
* - 원본 IP 헤더 전체 (소스/목적지 주소, 프로토콜 등)
* - 원본 L4 헤더 8바이트 (UDP: src/dst 포트, TCP: src/dst 포트 + seq)
* → traceroute가 어떤 프로브에 대한 응답인지 매칭 가능
*
* traceroute의 RTT 계산:
* - 프로브 전송 시각과 ICMP 응답 수신 시각의 차이
* - 보통 홉당 3개 프로브 전송 → 3개 RTT 표시
* - * 표시: 해당 홉에서 ICMP 응답 없음 (방화벽 차단 또는 rate limit)
*/
Raw Socket과 ICMP 프로그래밍
/* ICMP 패킷 직접 제어: Raw Socket 사용 */
/* 방법 1: Raw Socket (CAP_NET_RAW 필요) */
int sockfd = socket(AF_INET, SOCK_RAW, IPPROTO_ICMP);
/* → 모든 ICMP 타입을 수신/전송 가능
* → ICMP 헤더를 직접 구성해야 함
* → 체크섬은 커널이 자동 계산 (IP_HDRINCL이 아닌 경우)
*/
/* ICMP Echo Request 전송 예시 */
struct icmphdr hdr = {
.type = ICMP_ECHO, /* 8 */
.code = 0,
.checksum = 0, /* 커널이 계산 */
.un.echo.id = htons(getpid() & 0xFFFF),
.un.echo.sequence = htons(seq++),
};
sendto(sockfd, &hdr, sizeof(hdr), 0,
(struct sockaddr *)&dst, sizeof(dst));
/* ICMP 수신 — 모든 ICMP 메시지가 raw socket에 복사됨 */
struct sockaddr_in src;
socklen_t slen = sizeof(src);
char buf[1500];
ssize_t n = recvfrom(sockfd, buf, sizeof(buf), 0,
(struct sockaddr *)&src, &slen);
/* buf에 IP 헤더 + ICMP 메시지가 포함됨
* → ip_hdr를 파싱해 ICMP 오프셋 계산 필요 */
/* 방법 2: Ping Socket (비특권, 커널 3.0+) */
int pingfd = socket(AF_INET, SOCK_DGRAM, IPPROTO_ICMP);
/* → Echo Request/Reply만 가능 (타 타입 접근 불가)
* → IP 헤더 없이 ICMP 페이로드만 송수신
* → 커널이 id 필드를 소켓 포트처럼 관리
* → CAP_NET_RAW 불필요 (ping_group_range 범위 내) */
/* ICMP 소켓 필터: 특정 타입만 수신 */
struct icmp_filter filt;
filt.data = ~(1 << ICMP_ECHOREPLY); /* Echo Reply만 허용 */
setsockopt(sockfd, SOL_RAW, ICMP_FILTER, &filt, sizeof(filt));
ICMP 관련 sysctl 종합
| sysctl 경로 | 기본값 | 설명 |
|---|---|---|
net.ipv4.icmp_echo_ignore_all |
0 | 1이면 모든 Echo Request 무시 (ping 차단) |
net.ipv4.icmp_echo_ignore_broadcasts |
1 | 브로드캐스트/멀티캐스트 Echo 무시 (Smurf 방어) |
net.ipv4.icmp_echo_enable_probe |
0 | 1이면 RFC 8335 Extended Echo (Probe) 지원 (커널 5.7+) |
net.ipv4.icmp_msgs_per_sec |
1000 | 초당 ICMP 전송 최대 수 (전역 token bucket) |
net.ipv4.icmp_msgs_burst |
50 | ICMP 버스트 허용량 (token bucket 용량) |
net.ipv4.icmp_ratelimit |
1000 | 동일 목적지 ICMP 에러 최소 간격 (ms) |
net.ipv4.icmp_ratemask |
6168 | rate limit 적용 ICMP 타입 마스크 (비트 필드) |
net.ipv4.icmp_ignore_bogus_error_responses |
1 | 비정상 ICMP 에러 무시 (로그 오염 방지) |
net.ipv4.icmp_errors_use_inbound_ifaddr |
0 | 1이면 ICMP 에러의 소스 주소를 수신 인터페이스 주소로 설정 |
net.ipv4.conf.*.accept_redirects |
호스트:1 라우터:0 |
ICMP Redirect 수락 여부 |
net.ipv4.conf.*.secure_redirects |
1 | 기본 게이트웨이의 Redirect만 수락 |
net.ipv4.conf.*.send_redirects |
1 | 포워딩 시 ICMP Redirect 전송 여부 |
net.ipv4.ping_group_range |
"1 0" | ping 소켓 허용 GID 범위 (0 2147483647 = 모두 허용) |
net.ipv4.ip_no_pmtu_disc |
0 | 1이면 PMTUD 비활성화 (DF 비트 미설정) |
net.ipv4.tcp_mtu_probing |
0 | 1: PMTUD 블랙홀 시 프로빙, 2: 항상 프로빙 |
net.ipv6.icmp.ratelimit |
1000 | ICMPv6 에러 rate limit (ms) |
net.ipv6.icmp.echo_ignore_all |
0 | ICMPv6 Echo 무시 여부 |
ICMP와 Connection Tracking (conntrack)
/* Netfilter conntrack에서의 ICMP 처리
*
* 핵심 개념: ICMP 에러 메시지는 독립 연결이 아닌
* 기존 연결과 "RELATED" 관계로 추적됨
*
* 예시: Host A → Host B (TCP SYN)
* → 중간 라우터가 ICMP Dest Unreachable 반환
* → conntrack이 이 ICMP를 원본 TCP 연결과 연결(RELATED)
*/
/* net/netfilter/nf_conntrack_proto_icmp.c */
static int icmp_error_message(struct nf_conn *tmpl,
struct sk_buff *skb, ...)
{
/* ICMP 에러 페이로드에서 원본 패킷 헤더 추출 */
struct nf_conntrack_tuple innertuple;
/* 내부 패킷(원본)으로 conntrack 엔트리 검색 */
h = nf_conntrack_find_get(net, zone, &innertuple);
if (h) {
/* 기존 연결 발견 → ICMP를 RELATED로 분류
* → "iptables -A INPUT -m state --state RELATED -j ACCEPT"
* 규칙에 의해 허용됨 */
nf_ct_set(skb, nf_ct_tuplehash_to_ctrack(h), IP_CT_RELATED);
}
}
/* ICMP Echo의 conntrack:
* - Echo Request/Reply는 별도의 ICMP 연결로 추적
* - tuple: (src_ip, dst_ip, type, code, id)
* → id 필드가 TCP/UDP의 포트 역할
* - timeout: net.netfilter.nf_conntrack_icmp_timeout (기본 30초)
*
* conntrack 확인:
* conntrack -L -p icmp
* → icmp 1 29s src=10.0.0.1 dst=10.0.0.2 type=8 code=0 id=1234
* src=10.0.0.2 dst=10.0.0.1 type=0 code=0 id=1234
*/
ICMP 디버깅과 모니터링
# 1. ICMP SNMP 통계 확인
cat /proc/net/snmp | grep Icmp
# InMsgs OutMsgs InErrors OutErrors InDestUnreachs OutDestUnreachs
# InTimeExcds OutTimeExcds InEchos OutEchos InEchoReps OutEchoReps ...
# 상세 ICMP 통계 (타입별 카운터)
cat /proc/net/snmp | grep IcmpMsg
# InType0 (Echo Reply 수신)
# InType3 (Dest Unreachable 수신)
# InType8 (Echo Request 수신)
# OutType0 (Echo Reply 전송)
# OutType3 (Dest Unreachable 전송)
# OutType11 (Time Exceeded 전송) 등
# 2. nstat으로 ICMP 카운터 모니터링 (증분 확인)
nstat -s | grep -i icmp
# IcmpInMsgs, IcmpOutMsgs, IcmpInDestUnreachs, ...
# IcmpOutRateLimitGlobal ← rate limit으로 억제된 ICMP 수!
nstat -z | grep -i icmp # 0인 카운터도 표시
# 3. tcpdump로 ICMP 패킷 캡처
tcpdump -ni eth0 icmp
# 특정 타입만: ICMP Dest Unreachable
tcpdump -ni eth0 'icmp[icmptype] == 3'
# ICMP Frag Needed (PMTUD)
tcpdump -ni eth0 'icmp[icmptype] == 3 and icmp[icmpcode] == 4'
# ICMPv6 전체
tcpdump -ni eth0 icmp6
# 4. ftrace로 커널 ICMP 함수 추적
echo icmp_rcv > /sys/kernel/tracing/set_ftrace_filter
echo icmp_send >> /sys/kernel/tracing/set_ftrace_filter
echo icmp_echo >> /sys/kernel/tracing/set_ftrace_filter
echo function > /sys/kernel/tracing/current_tracer
echo 1 > /sys/kernel/tracing/tracing_on
# → /sys/kernel/tracing/trace 에서 호출 확인
# 5. perf로 ICMP 관련 커널 이벤트
perf trace -e 'net:*icmp*' -- ping -c 3 10.0.0.1
# 6. dropwatch로 ICMP 드롭 위치 추적
dropwatch -l kas
# → icmp_rcv+0x... 에서 드롭 발생 시 원인 파악 가능
# 7. 현재 ICMP sysctl 설정 일괄 확인
sysctl -a 2>/dev/null | grep icmp
sysctl -a 2>/dev/null | grep pmtu
ICMP 보안 모범 사례: 서버에서는 accept_redirects=0과 send_redirects=0을 설정하고, icmp_echo_ignore_broadcasts=1을 유지합니다. ping을 완전 차단(icmp_echo_ignore_all=1)하면 네트워크 진단이 불가능해지므로, 대신 nftables rate limit으로 적절히 제어하는 것이 권장됩니다. ICMP Destination Unreachable은 절대 차단하지 마세요 — PMTUD와 TCP 연결 관리에 필수적입니다.
관련 문서
ICMP와 관련된 다른 주제를 더 깊이 이해하고 싶다면 다음 문서를 참고하세요.