Linux Kernel ICMPv4
해당 포스트에서는 리눅스 커널의 ICMPv4 구현에 대해 설명합니다.
ICMPv4
ICMPv4 메시지는 오류 메시지와 정보 메시지로 나눌 수 있다.
ICMPv4 프로토콜을 사용하는 대표적인 유틸리티는 다음과 같다.
- ping : raw 소켓을 통해 ICMP_ECHO 메시지를 보내고, ICMP_REPLY 메시지를 회신
- traceroute : TTL 값이 0이 되면 포워딩 장비가 ICMP_TIME_EXCEED 메시지를 회신
- TTL 값이 1인 메시지를 전송하는 것으로 시작하여 응답으로 ICMP_TIME_EXCEED 코드가 지정된 ICMP_DEST_UNREACH를 받을 때마다 TTL을 1 증가시켜 같은 목적지로 재전송
- 반환된 Time Exceeded ICMP 메시지를 이용해 패킷이 이동한 라우터 목록을 만듦
- UDP 프로토콜을 사용
ICMPv4 헤더
- Type(8비트), Code(8비트), Checksum(16비트), Optional Data(32비트), Payload
- ICMPv4 오류 메시지의 데이터그램 길이는 576 바이트를 초과하지 않아야 함 (RFC791 -> RFC1812)
struct icmphdr {
__u8 type;
__u8 code;
__sum16 checksum;
union {
struct {
__be16 id;
__be16 sequence;
} echo;
__be32 gateway;
struct {
__be16 __unused;
__be16 mtu;
} frag;
__u8 reserved[4];
} un;
};
ICMPv4 초기화
ICMPv4 초기화는 부팅 시 inet_init() 함수에서 다음과 같이 수행한다.
static int __init inet_init(void)
{
...
if (inet_add_protocol(&icmp_protocol, IPPROTO_ICMP) < 0)
pr_crit("%s: Cannot add ICMP protocol\n", __func__);
...
ping_init();
/*
* Set the ICMP layer up
*/
if (icmp_init() < 0)
panic("Failed to create the ICMP control socket.\n");
...
}
inet_add_protocol() 함수는 커널에 특정 프로토콜에 대한 핸들러를 등록한다.
(net_protocol 타입의 전역 변수 배열 inet_protos에 인자로 전달받은 net_protocol 타입 객체를 등록한다.)
icmp_protocol 변수는 net_protocol 구조체의 인스턴스로 다음과 같다.
static const struct net_protocol icmp_protocol = {
.handler = icmp_rcv,
.err_handler = icmp_err,
.no_policy = 1,
.netns_ok = 1,
};
- icmp_rcv : IP 헤더의 프로토콜 필드가 IPPROTO_ICMP인 수신 패킷에 대해 icmp_rcv() 함수가 호출된다.
- no_policy : 해당 플래그가 1로 설정되면 IPsec policy 검사를 수행할 필요가 없음을 의미한다.
- netns_ok : 해당 플래그가 1로 설정되면, 이는 프로토콜이 네트워크 네임스페이스를 알고 있음을 나타낸다.
또한, icmp_init() 함수에서는 register_pernet_subsys() 함수로 init/exit operation을 등록하는데, 결국 초기화를 위하여 icmp_sk_init() 함수를 호출하도록 되어 있다.
static int __net_init icmp_sk_init(struct net *net)
{
int i, err;
net->ipv4.icmp_sk = alloc_percpu(struct sock *);
if (!net->ipv4.icmp_sk)
return -ENOMEM;
for_each_possible_cpu(i) {
struct sock *sk;
// RAW ICMP 소켓 생성
err = inet_ctl_sock_create(&sk, PF_INET,
SOCK_RAW, IPPROTO_ICMP, net);
if (err < 0)
goto fail;
// 각 CPU 마다 생성된 소켓 보관
*per_cpu_ptr(net->ipv4.icmp_sk, i) = sk;
/* Enough space for 2 64K ICMP packets, including
* sk_buff/skb_shared_info struct overhead.
*/
sk->sk_sndbuf = 2 * SKB_TRUESIZE(64 * 1024);
/*
* Speedup sock_wfree()
*/
sock_set_flag(sk, SOCK_USE_WRITE_QUEUE);
inet_sk(sk)->pmtudisc = IP_PMTUDISC_DONT;
}
/* Control parameters for ECHO replies. */
net->ipv4.sysctl_icmp_echo_ignore_all = 0;
net->ipv4.sysctl_icmp_echo_enable_probe = 0;
net->ipv4.sysctl_icmp_echo_ignore_broadcasts = 1;
/* Control parameter - ignore bogus broadcast responses? */
net->ipv4.sysctl_icmp_ignore_bogus_error_responses = 1;
/*
* Configurable global rate limit.
*
* ratelimit defines tokens/packet consumed for dst->rate_token
* bucket ratemask defines which icmp types are ratelimited by
* setting it's bit position.
*
* default:
* dest unreachable (3), source quench (4),
* time exceeded (11), parameter problem (12)
*/
net->ipv4.sysctl_icmp_ratelimit = 1 * HZ;
net->ipv4.sysctl_icmp_ratemask = 0x1818;
net->ipv4.sysctl_icmp_errors_use_inbound_ifaddr = 0;
return 0;
fail:
icmp_sk_exit(net);
return err;
}
위 코드의 주석부분에서 설명한 각 CPU마다 생성한 RAW ICMP 소켓은 icmp_push_reply() 함수에서 사용된다.
icmp_push_reply() 함수는 이 후 메시지 송신 파트에서 설명한다.
ICMPv4 메시지 수신
ICMPv4 메시지는 다음 링크와 같이 여러 형식이 존재한다.
각 메시지 타입들은 icmp_control 객체의 배열 icmp_pointers에 대해 다음과 같이 핸들러를 등록한다.
/*
* ICMP control array. This specifies what to do with each ICMP.
*/
struct icmp_control {
bool (*handler)(struct sk_buff *skb);
short error; /* This ICMP is classed as an error message */
};
/*
* This table is the definition of how we handle ICMP.
*/
static const struct icmp_control icmp_pointers[NR_ICMP_TYPES + 1] = {
[ICMP_ECHOREPLY] = {
.handler = ping_rcv,
},
[1] = {
.handler = icmp_discard,
.error = 1,
},
[2] = {
.handler = icmp_discard,
.error = 1,
},
[ICMP_DEST_UNREACH] = {
.handler = icmp_unreach,
.error = 1,
},
[ICMP_SOURCE_QUENCH] = {
.handler = icmp_unreach,
.error = 1,
},
[ICMP_REDIRECT] = {
.handler = icmp_redirect,
.error = 1,
},
[6] = {
.handler = icmp_discard,
.error = 1,
},
[7] = {
.handler = icmp_discard,
.error = 1,
},
[ICMP_ECHO] = {
.handler = icmp_echo,
},
[9] = {
.handler = icmp_discard,
.error = 1,
},
[10] = {
.handler = icmp_discard,
.error = 1,
},
[ICMP_TIME_EXCEEDED] = {
.handler = icmp_unreach,
.error = 1,
},
[ICMP_PARAMETERPROB] = {
.handler = icmp_unreach,
.error = 1,
},
[ICMP_TIMESTAMP] = {
.handler = icmp_timestamp,
},
[ICMP_TIMESTAMPREPLY] = {
.handler = icmp_discard,
},
[ICMP_INFO_REQUEST] = {
.handler = icmp_discard,
},
[ICMP_INFO_REPLY] = {
.handler = icmp_discard,
},
[ICMP_ADDRESS] = {
.handler = icmp_discard,
},
[ICMP_ADDRESSREPLY] = {
.handler = icmp_discard,
},
};
이렇게 등록된 핸들러는 icmp_rcv() 함수에서 다음과 같이 타입에 따라 호출된다.
int icmp_rcv(struct sk_buff *skb)
{
...
icmph = icmp_hdr(skb);
...
success = icmp_pointers[icmph->type].handler(skb);
...
}
즉, ICMP_ECHOREPLY 메시지를 수신하면 ping_rcv() 함수로 처리하고, ICMP_ECHO 메시지를 수신하면 icmp_echo() 함수로 처리하는 방식이다.
icmp_discard() 함수는 legacy 메시지를 처리하는 빈 함수이다.
icmp_redirect() 함수는 ICMP_REDIRECT 메시지를 처리하는 함수이다.
icmp_unreach() 함수는 ICMP_DEST_UNREACH, IMCP_TIME_EXCEEDED 등의 메시지를 처리하는데 사용된다.
ICMP_DEST_UNREACH 메시지는 다양한 조건에서 전송될 수 있다.
ICMP_TIME_EXCEEDED 메시지는 다음과 같은 경우에 전송된다.
- ip_forward() 함수에서 TTL이 0이 되면 다음과 같이 호출한다.
icmp_send(skb, ICMP_DEST_UNREACH, ICMP_SR_FAILED, 0);
- ip_expire() 함수에서 단편화된 패킷에서 타임아웃이 발생하면 다음과 같이 호출한다.
icmp_send(head, ICMP_TIME_EXCEEDED, ICMP_EXC_FRAGTIME, 0);
ICMPv4 메시지 송신
ICMP 메시지 송신은 다음 두 가지 함수에서 진행된다.
static void icmp_reply(struct icmp_bxm *icmp_param, struct sk_buff *skb)
: ICMP 요청인 ICMP_ECHO와 ICMP_TIMESTAMP에 대한 응답으로서 전송한다.void __icmp_send(struct sk_buff *skb_in, int type, int code, __be32 info, const struct ip_options *opt)
: 로컬 장비가 특정한 조건에서 ICMPv4 메시지를 전송한다.
ftrace를 이용하여 ping 메시지를 수신하고 응답하는 과정을 트레이싱 하면 다음과 같다.
# tracer: function
#
# entries-in-buffer/entries-written: 4/4 #P:4
#
# _-----=> irqs-off
# / _----=> need-resched
# | / _---=> hardirq/softirq
# || / _--=> preempt-depth
# ||| / delay
# TASK-PID CPU# |||| TIMESTAMP FUNCTION
# | | | |||| | |
<idle>-0 [003] ..s. 14084.047681: icmp_reply.constprop.0 <-icmp_echo.part.0
<idle>-0 [003] ..s. 14084.047683: <stack trace>
=> icmp_reply.constprop.0
=> icmp_echo.part.0
=> icmp_echo
=> icmp_rcv
=> ip_protocol_deliver_rcu
=> ip_local_deliver_finish
=> ip_local_deliver
=> ip_sublist_rcv_finish
=> ip_sublist_rcv
=> ip_list_rcv
=> __netif_receive_skb_list_core
=> netif_receive_skb_list_internal
=> gro_normal_list.part.0
=> napi_complete_done
=> e1000_clean
=> net_rx_action
=> __do_softirq
=> asm_call_irq_on_stack
=> do_softirq_own_stack
=> irq_exit_rcu
=> common_interrupt
=> asm_common_interrupt
=> native_safe_halt
=> acpi_idle_enter
=> cpuidle_enter_state
=> cpuidle_enter
=> call_cpuidle
=> do_idle
=> cpu_startup_entry
=> start_secondary
=> secondary_startup_64
=> 0xb50000e1ffffffff
=> 0xb4e01102ffffffff
=> 0xb423679dffffffff
=> 0xb42a8764ffffffff
=> 0xb4d7793dffffffff
=> 0xb4e00c1effffffff
=> 0x328c0ffffffff
=> 0x10000400000124
함수 호출 스택을 보면 icmp_rcv() 함수에서 ICMP_ECHO 메시지를 처리하기 위하여 icmp_echo() 핸들러를 호출하고, 해당 핸들러에서 icmp_reply() 함수를 호출하는 것을 볼 수 있다.
이 외에도 아래 ip 스택 함수들도 볼 수 있는데, 해당 내용은 추후 IPv4 챕터에서 설명하기로 한다.
icmp_send() 함수는 사실 __icmp_send() 함수의 래퍼이다.
__icmp_send() 함수에서는 패킷의 header를 변경시키면 안 되고, 멀티캐스트/브로드캐스트 주소에 reply 하면 안 되며, 단편화 패킷의 경우 첫 fragment 패킷에만 reply 해야 한다.
또한, ICMP 오류 메시지는 ICMP 오류 메시지를 수신한 결과로서 전송되면 안 된다.
이러한 온전성/제한 검사들을 통과하면, icmp_route_lookup() 함수로 라우터 테이블 lookup을 진행한 후, icmp_push_reply()함수로 실제 패킷을 전송한다.
icmp_send() 함수의 원형은 다음과 같다.
static inline void icmp_send(struct sk_buff *skb_in, int type, int code, __be32 info)
- skb_in : 해당 함수가 호출되게 한 SKB
- type : ICMPv4 메시지의 type
- code : ICMPv4 메시지의 code
- info : 추가적인 정보를 담고 있는 매개 변수로 다음과 같은 경우 사용된다.
- ICMP_PARAMETERPROB 메시지 형식의 경우 info는 구문 해석 문제가 발생한 IPv4 헤더의 오프셋이다.
- ICMP_FRAG_NEEDED 코드가 지정된 ICMP_DEST_UNREACH 메시지 형식의 경우 MTU이다.
- ICMP_REDIR_HOST 코드가 지정된 ICMP_REDIRECT 메시지 형식의 경우 수신한 SKB의 목적지 IP 주소이다. 아래에는 icmp_send() 함수가 호출되는 경우들을 설명한다.
ICMP_DEST_UNREACH
ICMP_DEST_UNREACH 메시지에는 아래와 같은 세부 코드들이 있다.
ICMP_PROT_UNREACH
IP 헤더의 프로토콜이 존재하지 않는 프로토콜이면 ICMP_DEST_UNREACH/ICMP_PROT_UNREACH 메시지가 회신된다.
존재하지 않는 프로토콜이 생기는 경우는 실제 오류이거나, 커널이 해당 프로토콜을 지원하지 않는 상태로 빌드된 경우이다.
해당 경우엔 패킷을 처리할 핸들러가 없기 때문에 “목적지에 연결할 수 없음”이라는 ICMP 메시지가 회신된다.
아래는 해당 부분을 처리하는 코드이다.
void ip_protocol_deliver_rcu(struct net *net, struct sk_buff *skb, int protocol)
{
const struct net_protocol *ipprot;
int raw, ret;
resubmit:
raw = raw_local_deliver(skb, protocol);
ipprot = rcu_dereference(inet_protos[protocol]);
if (ipprot) {
if (!ipprot->no_policy) {
...
} else {
if (!raw) {
if (xfrm4_policy_check(NULL, XFRM_POLICY_IN, skb)) {
__IP_INC_STATS(net, IPSTATS_MIB_INUNKNOWNPROTOS);
icmp_send(skb, ICMP_DEST_UNREACH,
ICMP_PROT_UNREACH, 0);
}
kfree_skb(skb);
} else {
__IP_INC_STATS(net, IPSTATS_MIB_INDELIVERS);
consume_skb(skb);
}
}
}
ICMP_PORT_UNREACH
UDPv4 패킷을 수신하면 그에 일치하는 UDP 소켓을 찾는다. 일치하는 소켓이 없는 경우 체크섬을 확인한다.
체크섬이 잘못된 경우에는 단순히 패킷을 drop하지만, 체크섬이 정확한 경우엔 통계를 업데이트하고 ICMP_DEST_UNREACH/ICMP_PORT_UNREACH 메시지를 회신한다.
int __udp4_lib_rcv(struct sk_buff *skb, struct udp_table *udptable,
int proto)
{
struct sock *sk;
...
sk = __udp4_lib_lookup_skb(skb, uh->source, uh->dest, udptable);
if (sk)
return udp_unicast_rcv_skb(sk, skb, uh);
if (!xfrm4_policy_check(NULL, XFRM_POLICY_IN, skb))
goto drop;
nf_reset_ct(skb);
/* No socket. Drop packet silently, if checksum is wrong */
if (udp_lib_checksum_complete(skb))
goto csum_error;
__UDP_INC_STATS(net, UDP_MIB_NOPORTS, proto == IPPROTO_UDPLITE);
icmp_send(skb, ICMP_DEST_UNREACH, ICMP_PORT_UNREACH, 0);
...
}
__udp4_lib_lookup_skb() 함수로 udp table lookup을 수행하고, 소켓이 없는 경우 csum_error 처리 루틴으로 가거나 “포트에 연결할 수 없음” 메시지를 회신한다.
ICMP_FRAG_NEEDED
MTU보다 길이가 긴 패킷을 포워딩할 경우 ICMP_DEST_UNREACH/ICMP_FRAG_NEEDED 메시지를 송신자에게 회신하고 패킷을 drop한다.
int ip_forward(struct sk_buff *skb)
{
u32 mtu;
struct iphdr *iph; /* Our header */
struct rtable *rt; /* Route we use */
struct ip_options *opt = &(IPCB(skb)->opt);
struct net *net;
...
mtu = ip_dst_mtu_maybe_forward(&rt->dst, true);
if (ip_exceeds_mtu(skb, mtu)) {
IP_INC_STATS(net, IPSTATS_MIB_FRAGFAILS);
icmp_send(skb, ICMP_DEST_UNREACH, ICMP_FRAG_NEEDED,
htonl(mtu));
goto drop;
}
...
}
static bool ip_exceeds_mtu(const struct sk_buff *skb, unsigned int mtu)
{
if (skb->len <= mtu)
return false;
if (unlikely((ip_hdr(skb)->frag_off & htons(IP_DF)) == 0))
return false;
/* original fragment exceeds mtu and DF is set */
if (unlikely(IPCB(skb)->frag_max_size > mtu))
return true;
if (skb->ignore_df)
return false;
if (skb_is_gso(skb) && skb_gso_validate_network_len(skb, mtu))
return false;
return true;
}
ICMP_SR_FAILED
패킷에 strict route 옵션이 설정되어 있는 경우엔 게이트웨이 사용이 제한된다. 따라서, strict route 옵션이 설정되어 있고 게이트웨이로 포워딩하는 경우 ICMP_DEST_UNREACH/ICMP_SR_FAILED 메시지를 회신하고 패킷은 drop된다.
int ip_forward(struct sk_buff *skb)
{
u32 mtu;
struct iphdr *iph; /* Our header */
struct rtable *rt; /* Route we use */
struct ip_options *opt = &(IPCB(skb)->opt);
struct net *net;
...
if (opt->is_strictroute && rt->rt_uses_gateway)
goto sr_failed;
sr_failed:
/*
* Strict routing permits no gatewaying
*/
icmp_send(skb, ICMP_DEST_UNREACH, ICMP_SR_FAILED, 0);
goto drop;
...
}
속도 제한
icmp_reply()와 icmp_send() 함수 모두 속도 제한을 지원한다. 두 함수는 icmpv4_xrlim_allow() 함수를 호출하고, 속도 제한 검사에서 패킷 전송을 허용하면 패킷을 전송한다. icmpv4_xrlim_allow() 함수는 다음과 같으며, true를 반환하면 패킷을 허용한다.
/*
* Send an ICMP frame.
*/
static bool icmpv4_xrlim_allow(struct net *net, struct rtable *rt,
struct flowi4 *fl4, int type, int code)
{
struct dst_entry *dst = &rt->dst;
struct inet_peer *peer;
bool rc = true;
int vif;
if (icmpv4_mask_allow(net, type, code))
goto out;
/* No rate limit on loopback */
if (dst->dev && (dst->dev->flags&IFF_LOOPBACK))
goto out;
vif = l3mdev_master_ifindex(dst->dev);
peer = inet_getpeer_v4(net->ipv4.peers, fl4->daddr, vif, 1);
rc = inet_peer_xrlim_allow(peer, net->ipv4.sysctl_icmp_ratelimit);
if (peer)
inet_putpeer(peer);
out:
return rc;
}
다음과 같은 경우 속도 제한 검사를 수행하지 않는다.
- 루프백 장치의 경우
- 장치의 icmp mask에 해당 type(ICMP_DEST_UNREACH)/code(ICMP_FRAG_NEEDED 등)이 설정되지 않은 경우
위의 경우가 아니라면 inet_peer_xrlim_allow() 함수를 통해 속도 제한이 수행된다.
icmp_push_reply
icmp_reply() 함수와 icmp_send() 함수 모두 결국 실제 패킷을 전송하기 위하여 icmp_push_reply()함수를 호출한다. 해당 함수의 정의는 다음과 같다.
static void icmp_push_reply(struct icmp_bxm *icmp_param,
struct flowi4 *fl4,
struct ipcm_cookie *ipc, struct rtable **rt)
{
struct sock *sk;
struct sk_buff *skb;
sk = icmp_sk(dev_net((*rt)->dst.dev)); // CPU에 할당된 RAW ICMP 소켓에 접근
if (ip_append_data(sk, fl4, icmp_glue_bits, icmp_param,
icmp_param->data_len+icmp_param->head_len,
icmp_param->head_len,
ipc, rt, MSG_DONTWAIT) < 0) {
__ICMP_INC_STATS(sock_net(sk), ICMP_MIB_OUTERRORS);
ip_flush_pending_frames(sk);
} else if ((skb = skb_peek(&sk->sk_write_queue)) != NULL) {
struct icmphdr *icmph = icmp_hdr(skb);
__wsum csum;
struct sk_buff *skb1;
csum = csum_partial_copy_nocheck((void *)&icmp_param->data,
(char *)icmph,
icmp_param->head_len);
skb_queue_walk(&sk->sk_write_queue, skb1) {
csum = csum_add(csum, skb1->csum);
}
icmph->checksum = csum_fold(csum);
skb->ip_summed = CHECKSUM_NONE;
ip_push_pending_frames(sk, fl4);
}
}
위 함수에서는 icmp_sk_init() 함수에서 생성한 소켓을 다음 코드로 접근한다.
sk = icmp_sk(dev_net((*rt)->dst.dev)); // CPU에 할당된 RAW ICMP 소켓에 접근
dev_net() 함수는 외부로 보낼 네트워크 장치의 네트워크 네임스페이스를 반환한다.
icmp_sk() 함수는 해당 네트워크 네임스페이스의 ICMPv4 소켓을 this_cpu_read() 함수로 가져온다.
그 후 ip_append_data() 함수를 호출하여 패킷을 IP 계층으로 옮긴다.
예제
linux/net/ipv4/ip_km.h
net/ipv4 디렉토리 아래에 작업용 헤더 파일을 하나 추가한다.
원래는 절대 소스 디렉토리에 헤더 파일을 추가하면 안 되고, 해당 파일은 include/net 디렉토리 아래에 위치해야 하지만, 편의를 위해 해당 디렉토리에 위치시켰다. (추후 변경될 수 있음)
#ifndef _IP_KM_H
#define _IP_KM_H
#include <linux/types.h>
#include <linux/kernel.h>
#include <linux/skbuff.h>
#define KM_DEBUG_IP_RCV 16
#define KM_DEBUG_ICMP_RCV 17
extern uint32_t km_debug_state;
static void print_skb(struct sk_buff *skb)
{
int i = 0, j = 0;
printk("%7s", "offset ");
for (i = 0; i < 16; i++) {
printk(KERN_CONT "%02x ", i);
if (!(i % 16 - 7))
printk(KERN_CONT "- ");
}
printk(KERN_CONT "\n");
for (i = 0; i < skb->len; i++) {
if (!(i % 16))
printk(KERN_CONT "0x%04x ", i);
printk(KERN_CONT "%02x ", skb->data[i]);
if (!(i % 16 - 7))
printk(KERN_CONT "- ");
if (!(i % 16 - 15)) {
printk(KERN_CONT "\t");
for (j = i - 15; j <= i; j++) {
printk(KERN_CONT "%c", skb->data[j] >= 0x20 && skb->data[j] < 0x80 ? skb->data[j] : '.');
}
printk(KERN_CONT "\n");
}
}
printk("\n");
}
#endif
이 전에 만들어둔 debug filesystem의 변수와 print_skb 함수를 선언한다.
linux/net/ipv4/af_inet.c
1757 라인에 다음 코드를 추가한다.
static const struct net_protocol icmp_protocol_km = {
.handler = icmp_rcv_km,
.err_handler = icmp_err,
.no_policy = 1,
.netns_ok = 1,
};
inet_init 함수의 IPPROTO_ICMP 등록부분을 다음과 같이 변경한다 (1982 라인)
// 기존
if (inet_add_protocol(&icmp_protocol, IPPROTO_ICMP) < 0)
pr_crit("%s: Cannot add ICMP protocol\n", __func__);
// 변경 후
if (inet_add_protocol(&icmp_protocol_km, IPPROTO_ICMP) < 0)
pr_crit("%s: Cannot add ICMP protocol\n", __func__);
위 상황에서는 IP 헤더의 ICMP 프로토콜 핸들러가 icmp_rcv 함수에서 icmp_rcv_km 함수로 변경되었다.
include/net/icmp.h
헤더 파일의 57 라인에 다음 내용을 추가한다.
...
int icmp_rcv(struct sk_buff *skb);
int icmp_rcv_km(struct sk_buff *skb); // 추가된 라인
int icmp_err(struct sk_buff *skb, u32 info);
linux/net/ipv4/icmp.c
96 라인에 다음과 같이 헤더 파일을 추가한다.
#include "ip_km.h"
1121 라인에 다음과 같이 icmp_rcv_km 함수를 정의한다.
int icmp_rcv_km(struct sk_buff *skb)
{
struct icmphdr *icmph;
struct rtable *rt = skb_rtable(skb);
struct net *net = dev_net(rt->dst.dev);
bool success;
// 기존 icmp_rcv 함수에서 변경된 부분
if (km_debug_state == KM_DEBUG_ICMP_RCV)
print_skb(skb);
if (!xfrm4_policy_check(NULL, XFRM_POLICY_IN, skb)) {
struct sec_path *sp = skb_sec_path(skb);
int nh;
if (!(sp && sp->xvec[sp->len - 1]->props.flags &
XFRM_STATE_ICMP))
goto drop;
if (!pskb_may_pull(skb, sizeof(*icmph) + sizeof(struct iphdr)))
goto drop;
nh = skb_network_offset(skb);
skb_set_network_header(skb, sizeof(*icmph));
if (!xfrm4_policy_check_reverse(NULL, XFRM_POLICY_IN, skb))
goto drop;
skb_set_network_header(skb, nh);
}
__ICMP_INC_STATS(net, ICMP_MIB_INMSGS);
if (skb_checksum_simple_validate(skb))
goto csum_error;
if (!pskb_pull(skb, sizeof(*icmph)))
goto error;
icmph = icmp_hdr(skb);
ICMPMSGIN_INC_STATS(net, icmph->type);
/*
* 18 is the highest 'known' ICMP type. Anything else is a mystery
*
* RFC 1122: 3.2.2 Unknown ICMP messages types MUST be silently
* discarded.
*/
if (icmph->type > NR_ICMP_TYPES)
goto error;
/*
* Parse the ICMP message
*/
if (rt->rt_flags & (RTCF_BROADCAST | RTCF_MULTICAST)) {
/*
* RFC 1122: 3.2.2.6 An ICMP_ECHO to broadcast MAY be
* silently ignored (we let user decide with a sysctl).
* RFC 1122: 3.2.2.8 An ICMP_TIMESTAMP MAY be silently
* discarded if to broadcast/multicast.
*/
if ((icmph->type == ICMP_ECHO ||
icmph->type == ICMP_TIMESTAMP) &&
net->ipv4.sysctl_icmp_echo_ignore_broadcasts) {
goto error;
}
if (icmph->type != ICMP_ECHO &&
icmph->type != ICMP_TIMESTAMP &&
icmph->type != ICMP_ADDRESS &&
icmph->type != ICMP_ADDRESSREPLY) {
goto error;
}
}
success = icmp_pointers[icmph->type].handler(skb);
if (success) {
consume_skb(skb);
return NET_RX_SUCCESS;
}
drop:
kfree_skb(skb);
return NET_RX_DROP;
csum_error:
__ICMP_INC_STATS(net, ICMP_MIB_CSUMERRORS);
error:
__ICMP_INC_STATS(net, ICMP_MIB_INERRORS);
goto drop;
}
위 함수는 기존 icmp_rcv() 함수를 그대로 복사한 후, 코드 상단에 print_skb 함수를 이용하여 SKB의 data를 출력한다.
빌드 및 결과
linux root 디렉토리에서 다음과 같이 빌드한다.
$ make -j8
$ sudo make install
재부팅 후 다음과 같이 km_debug_state 값을 17로 수정한다.
# root 계정으로 작업해야 함
$ echo 17 > /sys/kernel/debug/km_debug/val
이 후 해당 머신에 ping을 보내면 dmesg에 SKB 데이터가 출력되는 것을 볼 수 있다.
위 결과는 새로 정의한 함수(icmp_rcv_km)를 ICMP 프로토콜 핸들러로 등록한 결과이다.
이를 응용하면 새로운 프로토콜을 정의하거나, 기존 프로토콜에 대한 핸들러를 자유롭게 수정할 수 있다.