ICMP 프로토콜 심화
Linux 커널 ICMP 프로토콜을 심층 분석합니다. Echo Request/Reply 처리, Destination Unreachable/Time Exceeded 같은 오류 메시지 생성 조건, ICMP rate limiting 정책, ping/traceroute가 커널에서 해석되는 방식, NDP·MLD 등 ICMPv6 확장, NAT/conntrack 상호작용, BPF/XDP를 이용한 고속 ICMP 처리, 네트워크 공격 방어, iptables/nftables 규칙 설계까지 운영 실무 관점으로 정리합니다.
핵심 요약
- 제어 프로토콜 — ICMP는 데이터 전송이 아닌, IP 네트워크의 상태 보고와 진단을 담당합니다.
- 에러 vs 조회 — 에러 메시지(Type 3,5,11,12)와 조회 메시지(Type 0/8,13/14)로 나뉩니다.
- Rate Limiting — 커널은 Token Bucket으로 ICMP 전송량을 제한해 DoS 방지합니다.
- PMTUD — Path MTU Discovery는 ICMP Frag Needed에 의존합니다.
- ICMPv6 = ICMP + ARP + IGMP — IPv6에서는 NDP와 MLD가 ICMPv6로 통합됩니다.
단계별 이해
- 헤더 구조 파악
8바이트 고정 헤더(type/code/checksum/union)와 에러 메시지의 원본 패킷 포함 규칙을 이해합니다. - 수신/전송 경로 추적
icmp_rcv()→ 디스패치 테이블 → 핸들러,icmp_send()의 RFC 규칙과 rate limiting을 확인합니다. - PMTUD 연동 이해
Frag Needed →ipv4_update_pmtu()→ TCP MSS 조정 경로를 따릅니다. - 보안/운영 적용
sysctl 튜닝, nftables 규칙, 공격 방어, 모니터링 도구를 실무에 적용합니다.
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> - 핵심 소스:
net/ipv4/icmp.c(~1600줄),net/ipv6/icmp.c(~1000줄),net/ipv4/ping.c
ICMP 패킷 수명주기
ICMP 패킷이 NIC에 도착해서 최종 처리되기까지의 전체 경로를 보여줍니다. 수신과 송신 양방향 경로를 한눈에 파악할 수 있습니다.
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 체크섬 계산 메커니즘
ICMP 체크섬은 ICMP 헤더 + 데이터 전체에 대해 계산됩니다. IPv4 ICMP는 IP pseudo-header를 포함하지 않지만, ICMPv6는 IPv6 pseudo-header를 포함합니다. 이 차이는 중요한 설계 결정입니다.
/* ICMP 체크섬 계산 원리
*
* IPv4 ICMP:
* checksum = one's complement of sum(ICMP header + ICMP data)
* → IP pseudo-header 미포함 (IPv4 IP 헤더에 자체 체크섬 있음)
*
* ICMPv6:
* checksum = one's complement of sum(IPv6 pseudo-header + ICMPv6)
* → IPv6에는 IP 헤더 체크섬이 없으므로 pseudo-header 포함 필수
* → pseudo-header = src_ip(16B) + dst_ip(16B) + length(4B) + next_header(4B)
*/
/* net/ipv4/icmp.c — ICMP 체크섬 검증 (수신 시) */
static bool icmp_checksum_validate(struct sk_buff *skb)
{
/* HW 체크섬 오프로드가 이미 검증했는지 확인 */
if (skb->ip_summed == CHECKSUM_UNNECESSARY)
return true; /* NIC가 이미 검증 완료 */
/* SW 체크섬 검증 */
if (skb_checksum_simple_validate(skb))
return false; /* 체크섬 오류 → 드롭 */
return true;
}
/* ICMP 체크섬 계산 (전송 시) */
/* icmp_push_reply() 내에서:
* 1. ICMP 헤더의 checksum 필드를 0으로 설정
* 2. ICMP 헤더 + 전체 페이로드에 대해 16비트 워드 합산
* 3. one's complement 적용
* 4. 결과를 checksum 필드에 기록
*
* 성능 최적화:
* - NIC TX checksum offload 지원 시 → skb->ip_summed = CHECKSUM_PARTIAL
* → 커널은 pseudo-header 부분 합만 계산, NIC가 나머지 완성
* - 미지원 시 → csum_partial() + csum_fold()로 SW 계산
*/
/* ICMPv6 체크섬: pseudo-header 포함 */
/* net/ipv6/icmp.c — ICMPv6 체크섬 */
__wsum csum = csum_partial(icmp6h, len, 0);
icmp6h->icmp6_cksum = csum_ipv6_magic(
&saddr, &daddr, /* IPv6 pseudo-header src/dst */
len, /* ICMPv6 전체 길이 */
IPPROTO_ICMPV6, /* next header = 58 */
csum /* 부분 체크섬 */
);
/* 주의: NAT 환경에서 ICMP 체크섬
* - ICMP 에러 메시지는 원본 패킷을 페이로드에 포함
* - NAT가 원본 패킷의 IP/포트를 변환하면 → ICMP 체크섬도 재계산 필요
* - conntrack의 nf_nat_icmp_reply_translation()이 처리
*/
ICMP 메시지 타입/코드 종합
| 타입 | 이름 | 주요 코드 | 용도 | 커널 처리 함수 |
|---|---|---|---|---|
| 0 | Echo Reply | 0 | ping 응답 | ping_rcv() |
| 3 | Destination Unreachable | 0: Net Unreachable 1: Host Unreachable 2: Protocol Unreachable 3: Port Unreachable 4: Frag Needed (DF set) 5: Source Route Failed 6: Dest Network Unknown 7: Dest Host Unknown 9: Net Admin Prohibited 10: Host Admin Prohibited 11: Net Unreach for TOS 12: Host Unreach for TOS 13: Admin Filtered 14: Host Precedence Violation 15: Precedence Cutoff |
패킷 전달 불가 보고 | 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() |
| 9 | Router Advertisement | 0: Normal 16: Not default route |
라우터 광고 (IRDP, RFC 1256) | icmp_discard() |
| 10 | Router Solicitation | 0 | 라우터 탐색 요청 (IRDP) | icmp_discard() |
| 11 | Time Exceeded | 0: TTL expired in transit 1: Frag reassembly timeout |
TTL 만료 / 재조합 실패 | icmp_unreach() |
| 12 | Parameter Problem | 0: Pointer indicates error 1: Missing required option 2: Bad length |
헤더 오류 보고 | icmp_unreach() |
| 13/14 | Timestamp / Reply | 0 | 시간 동기화 (거의 미사용, NTP로 대체) | icmp_timestamp() |
| 17/18 | Address Mask / Reply | 0 | 서브넷 마스크 조회 (폐기, DHCP로 대체) | icmp_discard() |
| 42/43 | Extended Echo / Reply | RFC 8335 | 인터페이스/주소 기반 확장 Probe (커널 5.7+) | icmp_echo() |
에러 vs 조회 메시지: ICMP 메시지는 두 범주로 나뉩니다. 에러 메시지(Type 3, 4, 5, 11, 12)는 다른 패킷의 처리 실패를 보고하며, 원본 패킷의 IP 헤더+8바이트를 페이로드에 포함합니다. 조회 메시지(Type 0/8, 13/14, 42/43)는 요청-응답 쌍으로 네트워크 진단에 사용됩니다. 에러 메시지에 대해 ICMP 에러를 생성하지 않는 것이 핵심 규칙입니다 (무한 루프 방지).
ICMP 에러 생성 조건 플로우차트
커널이 ICMP 에러 메시지를 생성할지 결정하는 전체 조건 분기입니다. RFC 1122, RFC 1812의 규칙이 icmp_send()에 구현되어 있습니다.
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_EXT_ECHO] = { .handler = icmp_echo, }, /* RFC 8335, 커널 5.7+ */
[ICMP_EXT_ECHOREPLY] = { .handler = ping_rcv, },
/* ... 나머지는 icmp_discard()로 무시 */
};
/* icmp_control 구조체:
* - .handler: 해당 타입의 처리 함수 포인터
* - .error: 1이면 에러 메시지 → icmp_unreach()가 원본 패킷 정보를
* 추출해 상위 프로토콜(TCP/UDP)의 에러 핸들러에 전달
*
* 디스패치 흐름:
* icmp_rcv() → icmp_pointers[type].handler(skb)
* ├── icmp_echo() : Echo Reply 생성 후 전송
* ├── ping_rcv() : ping 소켓에 전달
* ├── icmp_unreach() : 원본 패킷 추출 → 상위 프로토콜 err_handler
* ├── icmp_redirect() : 라우팅 테이블 갱신
* ├── icmp_timestamp() : Timestamp Reply 생성
* └── icmp_discard() : 무시 (SNMP 카운터만 갱신)
*/
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 (방화벽 차단)
*
* UDP가 ICMP Dest Unreachable를 받았을 때:
* - Code 3 (Port Unreachable):
* → connected UDP 소켓: ECONNREFUSED 반환
* → unconnected UDP 소켓: 다음 recvfrom()에서 에러
* - Code 4 (Frag Needed):
* → cork/fastopen 등에서 세그먼트 크기 조정
*/
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;
/* 규칙 5: 루프백 소스에 대해서는 보내지 않음 (추가 검증) */
if (ipv4_is_loopback(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);
}
/* icmp_send()가 호출되는 주요 지점들:
*
* 1. ip_forward() - TTL 만료 (Type 11, Code 0)
* 2. ip_forward() - MTU 초과 + DF=1 (Type 3, Code 4)
* 3. ip_error() - 라우팅 실패 (Type 3, Code 0/1)
* 4. ip_local_deliver() - 알 수 없는 프로토콜 (Type 3, Code 2)
* 5. udp_queue_rcv_skb() - 포트 미사용 (Type 3, Code 3)
* 6. ip_options_compile() - 잘못된 IP 옵션 (Type 12, Code 0)
* 7. ip_rt_send_redirect() - 라우팅 리다이렉트 (Type 5)
* 8. ip_frag_reasm_expire() - 재조합 타임아웃 (Type 11, Code 1)
*/
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 응답만 필터링됨) */
ping 유틸리티의 커널 경로
사용자가 ping 10.0.0.1을 실행했을 때, 커널 내부에서 Echo Request가 어떻게 생성되고 Echo Reply가 어떻게 수신되는지의 전체 경로입니다.
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. 종단까지 도달할 때까지 반복
*/
/* 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가 차단된 환경(많은 클라우드/기업 네트워크)에서 강력히 권장되는 설정입니다.
PMTUD Black Hole 진단
ICMP Frag Needed가 차단되면 PMTUD가 실패합니다. 이 시나리오를 시각적으로 보여주고, 진단 및 해결 방법을 정리합니다.
ICMP Redirect와 라우팅 캐시
/* 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() (네임스페이스별)
*
* per-CPU 소켓 구조:
* net->ipv4.icmp_sk[cpu] — 각 CPU마다 독립 소켓
* → 동시에 여러 CPU에서 ICMP를 보내도 락 없이 동작
* → preempt_disable() / preempt_enable()로 보호
*/
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바이트를 페이로드에 포함
*
* icmp_errors_use_inbound_ifaddr 사용 시:
* → 소스 주소를 수신 인터페이스의 주소로 설정
* → 다중 인터페이스 환경에서 응답 경로 일관성 유지
*/
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) — 별도 타입
* 5. IPv6에서는 라우터가 단편화하지 않으므로 PMTUD가 더 중요
* 6. ICMPv6 에러 메시지 최소 크기: IPv6 최소 MTU (1280) 이내
*/
| 타입 | 이름 | 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;
}
}
NDP (Neighbor Discovery Protocol) 심화
NDP는 IPv6에서 ARP를 대체하는 프로토콜로, ICMPv6 (Type 133-137) 위에서 동작합니다. 주소 해석, 라우터 발견, 주소 중복 감지(DAD), 자동 주소 설정(SLAAC), 리다이렉트를 모두 담당합니다.
traceroute와 ICMP
/* 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)
*/
/* 고급 traceroute 기법:
*
* Paris traceroute:
* → 기존 traceroute는 프로브마다 다른 src port 사용
* → ECMP 라우터가 다른 경로를 선택 → 경로 일관성 깨짐
* → Paris traceroute: flow hash를 고정해 동일 경로 유지
* → UDP: 같은 src/dst port 사용, checksum으로 seq 구분
*
* Dublin traceroute:
* → ECMP 경로를 의도적으로 탐색
* → 다른 flow hash를 가진 프로브를 병렬 전송
* → 로드 밸런서 뒤의 다중 경로 가시화
*
* mtr (My Traceroute):
* → traceroute + ping 결합
* → 연속 측정으로 패킷 손실률과 RTT 지터 확인
* → 네트워크 품질 실시간 모니터링에 유용
*/
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.ipv4.tcp_base_mss |
1024 | MTU probing 시작 MSS (tcp_mtu_probing 활성 시) |
net.ipv4.tcp_mtu_probe_floor |
48 | MTU probing 최소 MSS (커널 5.10+) |
net.ipv6.icmp.ratelimit |
1000 | ICMPv6 에러 rate limit (ms) |
net.ipv6.icmp.echo_ignore_all |
0 | ICMPv6 Echo 무시 여부 |
net.ipv6.icmp.echo_ignore_multicast |
0 | 멀티캐스트 ICMPv6 Echo 무시 여부 |
net.netfilter.nf_conntrack_icmp_timeout |
30 | ICMP conntrack 엔트리 타임아웃 (초) |
net.netfilter.nf_conntrack_icmpv6_timeout |
30 | ICMPv6 conntrack 엔트리 타임아웃 (초) |
ICMP와 NAT/NAPT 상호작용
NAT 환경에서 ICMP 처리는 특별한 주의가 필요합니다. ICMP에는 포트 번호가 없으므로 일반적인 NAPT(Network Address Port Translation)와 다른 메커니즘이 사용됩니다.
/* ICMP NAT 처리의 핵심 문제와 해결
*
* 1. Echo Request/Reply NAT:
* - ICMP에는 포트가 없으므로 id 필드를 "포트"처럼 사용
* - NAT가 id를 변환해 내부 호스트를 구분
* - conntrack tuple: (src_ip, dst_ip, type, code, id)
*
* 내부 호스트 A (10.0.0.1, id=1234)
* ↓ NAT (203.0.113.1, id=5678로 변환)
* 외부 서버 (8.8.8.8)
* ↓ Echo Reply (id=5678)
* NAT이 id=5678 → 10.0.0.1:1234로 역변환
*
* 2. ICMP 에러 메시지 NAT (가장 복잡):
* - ICMP 에러는 원본 패킷 헤더를 페이로드에 포함
* - NAT가 3중 변환 필요:
* a. 외부 ICMP 패킷의 dst IP 변환
* b. 내부 원본 IP 헤더의 src IP 역변환
* c. 내부 원본 L4 헤더의 src port 역변환
* - ICMP 체크섬도 재계산 필요
*/
/* net/netfilter/nf_nat_proto.c — ICMP NAT */
static bool icmp_manip_pkt(struct sk_buff *skb,
const struct nf_conntrack_tuple *tuple,
enum nf_nat_manip_type maniptype)
{
struct icmphdr *hdr = icmp_hdr(skb);
/* Echo: id 변환 */
inet_proto_csum_replace2(&hdr->checksum, skb,
hdr->un.echo.id,
tuple->src.u.icmp.id, false);
hdr->un.echo.id = tuple->src.u.icmp.id;
return true;
}
/* ICMP 에러 NAT — nf_nat_icmp_reply_translation() */
/*
* 외부에서 받은 ICMP 에러:
* IP(src=router, dst=NAT_public)
* ICMP(Type=3, Code=3)
* Original_IP(src=NAT_public, dst=server) ← 변환 필요!
* Original_UDP(src=NAT_port, dst=80) ← 변환 필요!
*
* NAT 처리 후:
* IP(src=router, dst=internal_host) ← dst 변환
* ICMP(Type=3, Code=3, checksum 재계산)
* Original_IP(src=internal_ip, dst=server) ← src 역변환
* Original_UDP(src=original_port, dst=80) ← src port 역변환
*
* 이 3중 변환이 실패하면 내부 호스트가 ICMP 에러를
* 자신의 연결과 매칭할 수 없음 → PMTUD 실패 등 문제
*/
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
*
* conntrack 상태 모델 (ICMP):
* NEW: 첫 Echo Request
* ESTABLISHED: Echo Reply 수신 후 (양방향 확인)
* RELATED: ICMP 에러가 기존 연결에 매핑된 경우
*/
ICMP 기반 네트워크 공격과 방어
ICMP는 네트워크 진단의 핵심이지만, 악용될 수 있는 여러 공격 벡터가 존재합니다. 각 공격 유형과 리눅스 커널의 방어 메커니즘을 정리합니다.
| 공격 유형 | 메커니즘 | 커널 방어 | 설정/대응 |
|---|---|---|---|
| Smurf Attack | 브로드캐스트 주소로 Echo Request (spoofed src) → 모든 호스트가 피해자에게 Echo Reply | 브로드캐스트 Echo 무시 | icmp_echo_ignore_broadcasts=1 (기본) |
| Ping of Death | 비정상적으로 큰 ICMP 패킷 (>65535바이트) → 버퍼 오버플로 | IP 재조합 크기 검증 | 현대 커널에서 자동 방어 |
| ICMP Flood | 대량 Echo Request → CPU/대역폭 소모 | Rate limiting | icmp_msgs_per_sec + nftables rate limit |
| ICMP Redirect 공격 | 위조 Redirect → 트래픽을 공격자 경유 (MITM) | 소스/게이트웨이 검증 | accept_redirects=0, secure_redirects=1 |
| ICMP 터널링 | ICMP Echo 데이터에 임의 페이로드 삽입 → 방화벽 우회 | DPI / 페이로드 검사 | ICMP 페이로드 크기 제한, IDS/IPS |
| PMTUD 공격 | 위조 Frag Needed (작은 MTU) → 성능 저하 | 최소 MTU 검증 (68B) | tcp_mtu_probing, MSS Clamping |
| ICMP 정보 수집 | Timestamp/Address Mask/Echo → OS 핑거프린팅, 호스트 발견 | 레거시 타입 무시 | icmp_echo_ignore_all=1 (극단), nftables 규칙 |
| Inverse Mapping | 존재하지 않는 호스트에 패킷 전송 → ICMP 에러로 네트워크 매핑 | Rate limiting | icmp_ratelimit으로 에러 응답 제한 |
# ICMP 보안 강화 sysctl 설정 (서버/라우터 권장)
# 1. Redirect 비활성화 (MITM 방지)
sysctl -w net.ipv4.conf.all.accept_redirects=0
sysctl -w net.ipv4.conf.all.send_redirects=0
sysctl -w net.ipv6.conf.all.accept_redirects=0
# 2. 브로드캐스트 Echo 무시 (Smurf 방지, 기본값)
sysctl -w net.ipv4.icmp_echo_ignore_broadcasts=1
# 3. Bogus 에러 응답 무시
sysctl -w net.ipv4.icmp_ignore_bogus_error_responses=1
# 4. ICMP rate limit 조정 (DDoS 시)
sysctl -w net.ipv4.icmp_msgs_per_sec=100
sysctl -w net.ipv4.icmp_msgs_burst=20
# 5. Source Route 거부
sysctl -w net.ipv4.conf.all.accept_source_route=0
# ⚠️ 주의: ping을 완전 차단하지 마세요!
# icmp_echo_ignore_all=1 설정 시:
# - 네트워크 진단 불가
# - 모니터링 시스템 오탐
# - SLA 모니터링 불가
# → 대신 nftables rate limit 사용
ICMP와 BPF/XDP 프로그래밍
XDP(eXpress Data Path)와 TC BPF를 사용하면 커널 네트워크 스택 진입 전에 ICMP 패킷을 처리할 수 있습니다. 초고속 ping 응답, 선택적 ICMP 필터링, 커스텀 rate limiting 등에 활용됩니다.
/* XDP에서 ICMP Echo Request를 직접 응답하는 예제
* → 커널 네트워크 스택을 우회해 나노초 수준 응답 가능
*/
/* SPDX-License-Identifier: GPL-2.0 */
#include <linux/bpf.h>
#include <linux/if_ether.h>
#include <linux/ip.h>
#include <linux/icmp.h>
#include <bpf/bpf_helpers.h>
#include <bpf/bpf_endian.h>
/* 체크섬 증분 갱신 헬퍼 */
static __always_inline void update_icmp_csum(
struct icmphdr *icmph, __u8 old_type, __u8 new_type)
{
__u32 csum = (~bpf_ntohs(icmph->checksum)) & 0xFFFF;
csum += (~old_type) & 0xFF;
csum += new_type;
csum = (csum & 0xFFFF) + (csum >> 16);
csum = (csum & 0xFFFF) + (csum >> 16);
icmph->checksum = bpf_htons(~csum);
}
SEC("xdp")
int xdp_icmp_echo(struct xdp_md *ctx)
{
void *data = (void *)(long)ctx->data;
void *data_end = (void *)(long)ctx->data_end;
/* 1. Ethernet 헤더 파싱 */
struct ethhdr *eth = data;
if ((void *)(eth + 1) > data_end)
return XDP_PASS;
if (eth->h_proto != bpf_htons(ETH_P_IP))
return XDP_PASS;
/* 2. IP 헤더 파싱 */
struct iphdr *iph = (struct iphdr *)(eth + 1);
if ((void *)(iph + 1) > data_end)
return XDP_PASS;
if (iph->protocol != IPPROTO_ICMP)
return XDP_PASS;
/* 3. ICMP 헤더 파싱 */
struct icmphdr *icmph = (struct icmphdr *)(iph + 1);
if ((void *)(icmph + 1) > data_end)
return XDP_PASS;
/* Echo Request가 아니면 통과 */
if (icmph->type != ICMP_ECHO)
return XDP_PASS;
/* 4. Echo Reply로 변환 */
/* MAC 주소 스왑 */
__u8 tmp_mac[ETH_ALEN];
__builtin_memcpy(tmp_mac, eth->h_dest, ETH_ALEN);
__builtin_memcpy(eth->h_dest, eth->h_source, ETH_ALEN);
__builtin_memcpy(eth->h_source, tmp_mac, ETH_ALEN);
/* IP 주소 스왑 */
__be32 tmp_ip = iph->saddr;
iph->saddr = iph->daddr;
iph->daddr = tmp_ip;
/* ICMP type 변경 + 체크섬 증분 갱신 */
update_icmp_csum(icmph, ICMP_ECHO, ICMP_ECHOREPLY);
icmph->type = ICMP_ECHOREPLY;
/* 5. XDP_TX로 즉시 응답 (커널 스택 미경유!) */
return XDP_TX;
}
/* 성능 비교:
* - 일반 커널 경로: ~30-50μs RTT (softirq 경유)
* - XDP Echo Reply: ~1-5μs RTT (드라이버 수준 처리)
* - 10Gbps NIC에서 14Mpps 이상 처리 가능
*
* 로드 방법:
* ip link set dev eth0 xdpdrv obj xdp_icmp.o sec xdp
* → "xdpdrv": 네이티브 XDP (최고 성능)
* → "xdpgeneric": 제네릭 XDP (모든 드라이버, 성능 열세)
*
* TC BPF 활용:
* - ingress/egress에서 ICMP 필터링
* - 커스텀 rate limiting (BPF map 기반)
* - ICMP 로깅/모니터링
*/
iptables/nftables ICMP 규칙 설계
ICMP를 적절히 허용하면서 보안을 유지하는 방화벽 규칙 모범 사례입니다.
# ===== iptables 규칙 =====
# 필수 허용: ICMP 에러 메시지 (차단 금지!)
iptables -A INPUT -p icmp --icmp-type destination-unreachable -j ACCEPT
iptables -A INPUT -p icmp --icmp-type time-exceeded -j ACCEPT
iptables -A INPUT -p icmp --icmp-type parameter-problem -j ACCEPT
# PMTUD 필수: Frag Needed (이것을 차단하면 TCP 연결 끊김)
iptables -A INPUT -p icmp --icmp-type fragmentation-needed -j ACCEPT
# ping 허용 (rate limit 적용)
iptables -A INPUT -p icmp --icmp-type echo-request \
-m limit --limit 10/s --limit-burst 20 -j ACCEPT
iptables -A INPUT -p icmp --icmp-type echo-request -j DROP
# Echo Reply 허용 (나가는 ping의 응답)
iptables -A INPUT -p icmp --icmp-type echo-reply -j ACCEPT
# RELATED ICMP (conntrack 연동)
iptables -A INPUT -m state --state RELATED -p icmp -j ACCEPT
# 나머지 ICMP 드롭 (Redirect, Timestamp 등)
iptables -A INPUT -p icmp -j DROP
# ===== nftables 규칙 (권장) =====
nft add table inet filter
nft add chain inet filter input '{ type filter hook input priority 0; policy drop; }'
# ICMP 에러: 무조건 허용
nft add rule inet filter input \
ip protocol icmp icmp type { \
destination-unreachable, \
time-exceeded, \
parameter-problem \
} accept
# ICMPv6 필수 허용 (NDP, MLD 포함!)
nft add rule inet filter input \
ip6 nexthdr icmpv6 icmpv6 type { \
destination-unreachable, \
packet-too-big, \
time-exceeded, \
parameter-problem, \
echo-request, \
echo-reply, \
nd-router-solicit, \
nd-router-advert, \
nd-neighbor-solicit, \
nd-neighbor-advert, \
mld-listener-query, \
mld-listener-report, \
mld2-listener-report \
} accept
# ping rate limit (nftables meter)
nft add rule inet filter input \
ip protocol icmp icmp type echo-request \
meter icmp-ratelimit '{ ip saddr limit rate 10/second burst 5 packets }' \
accept
# RELATED ICMP
nft add rule inet filter input \
ct state related accept
ICMPv6 차단 금지! IPv6에서는 NDP(주소 해석, 라우터 발견)와 MLD(멀티캐스트)가 ICMPv6로 동작합니다. ICMPv6 Type 133-137(NDP)과 130-132,143(MLD)을 차단하면 IPv6 통신 자체가 불가능해집니다. IPv6 환경에서 ICMPv6를 무차별 차단하는 것은 IPv4에서 ARP를 차단하는 것과 같습니다.
Extended ICMP (RFC 4884 / RFC 8335)
기존 ICMP의 한계를 극복하기 위한 확장 규격들입니다.
/* RFC 4884: Multi-Part ICMP Messages
*
* 기존 ICMP 에러 메시지의 한계:
* → 원본 패킷의 IP 헤더 + 8바이트만 포함
* → MPLS 라벨, 인터페이스 정보 등 추가 정보 전달 불가
*
* RFC 4884 확장:
* → ICMP 메시지 뒤에 Extension Header + Object 추가
* → Length 필드로 원본 데이터와 확장 데이터 구분
*
* 구조:
* [ICMP Header (8B)]
* [Original Datagram (≥128B)]
* [Extension Header (4B): version(4b) + reserved(12b) + checksum(16b)]
* [Extension Object 1: length(16b) + class-num(8b) + c-type(8b) + data]
* [Extension Object 2: ...]
*
* 주요 Extension Object:
* Class 1: MPLS Label Stack (RFC 4950)
* → traceroute에서 MPLS 경로 가시화 가능
* Class 2: Interface Information (RFC 5837)
* → 에러 발생 인터페이스의 이름, IP, ifIndex
* Class 3: Interface Identification (RFC 8335)
* → Extended Echo에서 인터페이스 식별
*/
/* RFC 8335: Extended Echo (Type 42/43)
*
* 목적: 원격 호스트의 특정 인터페이스 상태를 Probe
* → 기존 ping은 IP 주소로만 대상 지정
* → Extended Echo는 인터페이스 이름/주소/ifIndex로 대상 지정 가능
*
* 사용 사례:
* - 멀티홈 호스트의 특정 인터페이스 상태 확인
* - L2 연결 확인 (인터페이스 이름 기반)
* - 네트워크 장비의 특정 포트 모니터링
*
* 커널 지원: 5.7+ (net.ipv4.icmp_echo_enable_probe=1 필요)
*
* 사용법:
* ping -I eth0 -e ifname:eth1 10.0.0.1
* → "10.0.0.1의 eth1 인터페이스가 살아있는지 확인"
*
* 응답 코드:
* 0: No Error (active)
* 1: Malformed Query
* 2: No Such Interface
* 3: No Such Table Entry
* 4: Multiple Interfaces Satisfy Query
*/
실전 트러블슈팅 시나리오
| 증상 | 원인 | 진단 | 해결 |
|---|---|---|---|
| ping은 되는데 TCP 연결이 안 됨 | 방화벽에서 TCP 차단, 포트 미오픈 | telnet host port, ss -tlnp |
방화벽 규칙 확인, 서비스 바인딩 확인 |
| TCP 핸드셰이크 후 데이터 전송 중단 | PMTUD Black Hole (ICMP Frag Needed 차단) | tcpdump -ni eth0 'icmp[icmptype]==3 and icmp[icmpcode]==4'ip route get dst (pmtu 확인) |
tcp_mtu_probing=1 또는 MSS Clamping |
| traceroute에서 * * * 연속 | ICMP Time Exceeded가 차단됨 또는 rate limited | traceroute -T(TCP 방식) 시도 |
방화벽에서 ICMP Type 11 허용 |
| ping RTT가 비정상적으로 높음 | CPU softirq 처리 지연, 큐잉 지연, 경로 문제 | ping -D(타임스탬프), mtr host |
softirq 튜닝, 라우팅 최적화 |
| ICMP Redirect 메시지 수신 | 비효율적 라우팅 (같은 인터페이스로 포워딩) | ip route show, tcpdump icmp |
라우팅 테이블 수정, accept_redirects=0 |
| IcmpOutRateLimitGlobal 증가 | ICMP rate limit으로 에러 메시지 억제됨 | nstat -s | grep RateLimit |
icmp_msgs_per_sec 조정 또는 원인 트래픽 제거 |
| IPv6 통신 불가 (ping6 실패) | ICMPv6 NDP 차단 (방화벽 오설정) | ip -6 neigh show, tcpdump icmp6 |
ICMPv6 Type 133-137 허용 |
| NAT 뒤 호스트 PMTUD 실패 | NAT의 ICMP 에러 역변환 실패 | conntrack -L -p icmp, NAT 규칙 확인 |
conntrack 모듈 확인, MSS Clamping |
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
# ICMPv6 NDP만
tcpdump -ni eth0 'icmp6 and (ip6[40] >= 133 and ip6[40] <= 137)'
# 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
# 8. PMTU 캐시 확인
ip route show cache | grep mtu
# → 특정 목적지에 대한 PMTU 값 확인 가능
# 9. conntrack ICMP 엔트리 확인
conntrack -L -p icmp 2>/dev/null
conntrack -L -p icmpv6 2>/dev/null
# 10. BPF tracepoint로 ICMP 추적 (bpftrace)
bpftrace -e 'tracepoint:icmp:icmp_send { printf("ICMP send type=%d code=%d\n", args->type, args->code); }'
# 11. ss로 ping 소켓 확인
ss -w # RAW 소켓 (ICMP 포함)
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를 차단 | PMTUD 실패, TCP 연결 중단, traceroute 불가 | Type 3(Dest Unreach), 11(Time Exceeded), 12(Param Problem)은 반드시 허용 |
| ICMPv6를 IPv4 ICMP처럼 차단 | IPv6 통신 자체 불가 (NDP 실패 → 주소 해석 불가) | ICMPv6 Type 133-137(NDP), 130-132,143(MLD) 필수 허용 |
icmp_echo_ignore_all=1 설정 |
모니터링 시스템 알람, 네트워크 진단 불가 | nftables rate limit으로 제어 (limit rate 10/second) |
| ICMP rate limit을 너무 낮게 설정 | 정상 PMTUD/Time Exceeded까지 억제됨 | icmp_msgs_per_sec=100 이상 권장, nstat으로 모니터링 |
| Raw socket에서 체크섬을 직접 계산 | 불필요 — IP_HDRINCL 미사용 시 커널이 자동 계산 | checksum=0으로 설정하면 커널이 처리 |
| ICMP 에러에 대해 ICMP 에러 전송 시도 | 커널이 자동 차단하므로 전송 안 됨 | RFC 1122 규칙 — 이중 에러 금지는 커널이 보장 |
ping이 안 될 때 icmp_echo_ignore_all만 확인 |
실제 원인이 라우팅, 방화벽, ARP 문제일 수 있음 | tcpdump으로 Echo Request 도착 여부부터 확인 |
| NAT 환경에서 ICMP 에러를 별도 처리 | conntrack RELATED 매핑 실패 → 내부 호스트 에러 미수신 | -m state --state RELATED -j ACCEPT 규칙 필수 |
관련 문서
ICMP와 관련된 다른 주제를 더 깊이 이해하고 싶다면 다음 문서를 참고하세요.