ICMP 프로토콜 심화

Linux 커널 ICMP 프로토콜을 심층 분석합니다. Echo Request/Reply 처리, Destination Unreachable/Time Exceeded 같은 오류 메시지 생성 조건, ICMP rate limiting 정책, ping/traceroute가 커널에서 해석되는 방식, 네트워크 장애 시 ICMP를 이용한 경로 진단과 필터링 정책 설계까지 운영 실무 관점으로 정리합니다.

전제 조건: 네트워크 스택라우팅 문서를 먼저 읽으세요. 제어 평면과 데이터 평면이 분리되어 동작하므로, 규칙 갱신 시점과 실제 적용 지연을 함께 확인해야 합니다.
일상 비유: 이 주제는 도로 표지판 갱신과 교통 흐름 제어와 비슷합니다. 표지판을 바꿔도 차량 흐름 반영에는 시간이 필요하듯이, 경로/정책 갱신과 패킷 처리 시점은 분리해서 봐야 합니다.

핵심 요약

  • 패킷 수명주기 — ingress, 처리, egress 경로를 연결합니다.
  • 큐/버퍼 모델 — sk_buff와 큐 지점의 역할을 분리합니다.
  • 정책/데이터 분리 — 제어 평면과 데이터 평면을 구분합니다.
  • 성능 지표 — PPS, 지연, 드롭 원인을 함께 분석합니다.
  • 오프로딩 경계 — NIC/XDP/DPDK 경계를 명확히 유지합니다.

단계별 이해

  1. 경로 고정
    문제가 발생한 ingress/egress 지점을 먼저 특정합니다.
  2. 큐 관찰
    백로그와 드롭 위치를 계측합니다.
  3. 정책 반영 확인
    라우팅/필터 변경이 데이터 경로에 반영됐는지 봅니다.
  4. 부하 검증
    실제 트래픽 패턴에서 재현성을 확인합니다.
관련 표준: RFC 792 (ICMP), RFC 4443 (ICMPv6) — 인터넷 제어 메시지 프로토콜 표준입니다. 종합 목록은 참고자료 — 표준 & 규격 섹션을 참고하세요.

ICMP 심화

ICMP(Internet Control Message Protocol, IP 프로토콜 1)는 IP 네트워크의 제어 평면 프로토콜입니다. 패킷 전달 실패 보고, 경로 변경 알림, 연결 진단(ping/traceroute) 등 네트워크 운영의 핵심 기능을 담당합니다. RFC 792(IPv4 ICMP)와 RFC 4443(ICMPv6)에 정의되어 있으며, 커널의 net/ipv4/icmp.cnet/ipv6/icmp.c에 구현되어 있습니다.

ICMP 헤더 구조와 커널 구조체

IP Header (20+ bytes) - 외부 IP 헤더 ICMP Header (8 bytes) - type / code / checksum / union Original IP Header (20+ bytes) + Original L4 Header (8+ bytes) RFC 1122: 원본 IP 헤더 + 최소 8바이트 포함 필수
/* 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)

Host A Router R1 Host B DF=1, MTU 1500 패킷 다음 홉 MTU 1400 전달 실패 ICMP Type 3 / Code 4 (MTU=1400) Host A: Path MTU를 1400으로 갱신 MSS/세그먼트 크기 줄여 재전송 커널 경로: ipv4_update_pmtu() - rt_update_pmtu() dst_entry RTAX_MTU 갱신 + PMTU 타이머 시작
/* 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=0send_redirects=0을 설정하고, icmp_echo_ignore_broadcasts=1을 유지합니다. ping을 완전 차단(icmp_echo_ignore_all=1)하면 네트워크 진단이 불가능해지므로, 대신 nftables rate limit으로 적절히 제어하는 것이 권장됩니다. ICMP Destination Unreachable은 절대 차단하지 마세요 — PMTUD와 TCP 연결 관리에 필수적입니다.

ICMP와 관련된 다른 주제를 더 깊이 이해하고 싶다면 다음 문서를 참고하세요.