Difference between r1.54 and the current
@@ -528,6 +528,9 @@
=== 참고사항 ===
* [wiki:OSI_7LayerModel OSI 7 계층모델]
* [wiki:TCP TCP(Transmission Control Protocol)]
* [^https://tools.ietf.org/html/rfc1071 RFC1071 - Computing the Internet Checksum]
* O'REILLY사의 "Understanding LINUX NETWORK INTERNALS" 서적
* [wiki:OSI_7LayerModel OSI 7 계층모델]
* [wiki:TCP TCP(Transmission Control Protocol)]
* [wiki:skbuff Linux Kernel의 skbuff(Socket buffer descriptors)에 대하여]
* [wiki:XDP XDP(eXpress Data Path)]
* [wiki:DPDK DPDK(Data Plane Development Kit)]
* [^https://tools.ietf.org/html/rfc768 RFC768 User Datagram Protocol]* [^https://tools.ietf.org/html/rfc1071 RFC1071 - Computing the Internet Checksum]
* O'REILLY사의 "Understanding LINUX NETWORK INTERNALS" 서적
@@ -536,5 +539,4 @@
|컴퓨터네트워크 제12강 UDP와Checksum| 참고 영상 ||
|| [[Play(https://youtu.be/JUqhA1eC-io)]] ||
Computing the Internet Checksum (RFC1071)
1.1. 시작하기전에
"RFC1071 - Computing the Internet Checksum" 에서 제시하는 checksum 알고리즘은 IP, UDP, TCP protocol 에서 packet의 오류 정정 확인을 위한 용도로 사용됩니다. (IP 뿐만 아니라 Transport layer 에서 많은 protocol 이 같은 알고리즘을 사용합니다.)
checksum은 주어진 DATA를 16-bits 씩 합하고 1의 보수 (1's complement)로 하는 구현을 의미합니다.
checksum은 주어진 DATA를 16-bits 씩 합하고 1의 보수 (1's complement)로 하는 구현을 의미합니다.
- (덧셈 과정) INPUT DATA를 16-bits (2bytes) 단위로 더합니다.
- 만약 남은 odd 1byte가 있는 경우는 그 뒤 1byte를 0으로 간주하는 2bytes를 더합니다.
- 덧셈 결과 값이 16-bits (2bytes)를 넘어서 올림 수 (Carry bit, 부호 없는 덧셈에서 그 자리를 넘어서는 값이 되는 경우 1이 되는 bit)가 발생하는 경우 이 올림 수 (Carry bit)를 덧셈 결과에 다시 더합니다.
- (1의 보수 과정) 덧셈 결과에 1의 보수 (1's complement)로 하는 값이 최종 checksum 값이 됩니다.
- (검증 과정) 위 1 과정에서 덧셈 결과 값에 2 과정에서 1의 보수로 도출된 checksum 을 다시 더하면 항상 그 값은 FFFFH 가 됩니다.
- 여기에 1의 보수를 취하면 항상 0000H 가 됩니다.
- 이를 통해서 checksum 이 올바르게 계산되었는지 검증하는 기능을 구현합니다.
=> do checksum [과정] [L00] 1011 0110 1100 1011 B6CBH /* INPUT DATA A */ [L01] + 0100 1001 0011 0110 + 4936H /* INPUT DATA B */ [L02] --------------------- ------- [L03] Carry 1 0000 0000 0000 0001 1 0001H /* sum = A + B , carry = (sum < (A + B)) ? 1 : 0 */ [L04] + Carry 1 + 1H /* sum += carry */ [L05] --------------------- ------- [L06] 0000 0000 0000 0010 0002H [L07] ~ ~ /* 1's complement */ [L08] --------------------- ------- [L09] 1111 1111 1111 1101 FFFDH /* checksum = ~sum */ => checksum verify function [L06] 0000 0000 0000 0010 0002H /* sum = A + B , sum += carry */ [L09] + 1111 1111 1111 1101 + FFFDH /* sum += checksum */ --------------------- ------- Carry 0 1111 1111 1111 1111 FFFFH /* checksum 이 올바르게 계산된 값이라면, 이를 검증하는 방법으로 항상 어떤 INPUT DATA 에 대하여도 INPUT DATA의 합 sum과 sum을 1의 보수로 하는 결과 값인 checksum 을 더하면 그 결과 값은 항상 모든 bit가 1이 되는 특성을 갖습니다. (즉, L06과 L09 값을 더하는 것에서는 FFFFH 로 동일한 값이 나옵니다.) 여기서 checksum 값이 0000H인 경우는 +0 그리고 FFFFH인 경우는 -0 을 의미하기 때문에 정확히 따지면 다른 값으로 취급되어야 하겠지만 checksum verify 과정에서는 차이가 없습니다. Carry bit는 부호 없는 정수의 덧셈에서 그 정수 범위를 넘어서면 Carry bit가 set(1)됩니다. */
1.2. 소스
- RFC1071 에서 C언어로의 구현 예시
{ /* Compute Internet Checksum for "count" bytes * beginning at location "addr". */ register long sum = 0; while( count > 1 ) { /* This is the inner loop */ sum += * (unsigned short) addr++; count -= 2; } /* Add left-over byte, if any */ if( count > 0 ) sum += * (unsigned char *) addr; /* Fold 32-bit sum to 16 bits */ while (sum>>16) sum = (sum & 0xffff) + (sum >> 16); checksum = ~sum; }
- 필자가 수정하여 제시하는 예제 소스
unsigned int hwport_rfc1071_checksum(const void *s_data, size_t s_size) { /* hwport_internet_checksum (RFC1071) */ register uint32_t s_result = (uint32_t)0u; while(s_size > ((size_t)1u)) { s_result += (uint32_t)(*((const uint16_t *)s_data)); s_data = ((const uint16_t *)s_data) + ((size_t)1u); s_size -= (size_t)2u; } if(s_size > ((size_t)0u)) { s_result += (uint32_t)(*((const uint8_t *)s_data)); } /* Fold 32-bit sum to 16 bits */ s_result = (s_result >> 16) + (s_result & ((uint32_t)0xffffu)); s_result += s_result >> 16; return((unsigned int)((~s_result) & ((uint32_t)0xffffu))); }
- 이미 계산된 checksum 에서 국소 데이터 변경 시 checksum 갱신 함수
static inline uint16_t csum16_add(uint16_t csum, /* BE16 */ uint16_t addend) { uint16_t res = (uint16_t)csum; res += (uint16_t)addend; return (uint16_t)(res + (res < (uint16_t)addend)); } static inline uint16_t csum16_sub(uint16_t csum, /* BE16 */ uint16_t addend) { return csum16_add(csum, ~addend); } static inline void csum_replace2(uint16_t *sum, /* BE16 */ uint16_t old, /* BE16 */ uint16_t new) { *sum = ~csum16_add(csum16_sub(~(*sum), old), new); }
- 예를 들어 어떤 데이터의 checksum이 계산된 상태에서 특정 위치의 byte 값이 A 에서 B로 변경되었다면 다음과 같이 기존 checksum 에서 변화한 부분만 갱신하는데 사용할 수 있습니다. (데이터 일부가 변경되었음에도 전체를 다시 checksum 계산하는 것을 최소화 하는 효율적 구현의 필요성에 따른...)
csum_replace2(&checksum, A, B);
- 예를 들어 어떤 데이터의 checksum이 계산된 상태에서 특정 위치의 byte 값이 A 에서 B로 변경되었다면 다음과 같이 기존 checksum 에서 변화한 부분만 갱신하는데 사용할 수 있습니다. (데이터 일부가 변경되었음에도 전체를 다시 checksum 계산하는 것을 최소화 하는 효율적 구현의 필요성에 따른...)
- 가장 성능 좋은 구현 예제는 Linux kernel 에서 csum_partial() 함수와 csum_fold() 함수입니다.
1.3. Checksum (IP, TCP, UDP, ICMP, ...)의 업데이트가 필요한 경우
- TTL 감소
- ip_forward 에서 ip_decrease_ttl 함수를 통해서 checksum의 update 구현으로 처리
- 패킷 난도질(mangling 및 Transform 그리고 NAT 포함)
- NAT 에 의해서 IP header의 출발지, 목적지 등이 변경될 때 필요
- XFRM 등의 Transform module(IPSec VPN 등) 에 의해서 변화직전에 반영할 때
- IP옵션의 수정시
- 옵션의 변경이 발생하면 재계산 필요
- 단편화
- 단편화 되면 서로 다른 Header를 갖고 이에 따른 각각의 체크섬이 재계산 되어야 합니다.
1.4. 수신된 프레임이 하드웨어에서 계산된 L4 checksum이 무효화 되는 일상적인 경우
- 입력 L2 프레임이 최소 프레임 크기를 맞추려고 padding을 포함하고, NIC가 checksum을 계산할 때 padding을 뺄 수 있을 만큼 똑똑하지 못한 경우 (=> 이 경우 ip_rcv함수는 항상 checksum을 무효화 하게 됩니다. bridge에서 이와 비슷한 상황)
- 입력 IP 단편이 이전에 받은 단편과 겹쳐지는 경우
- 입력 IP 패킷이 IPSec protocol 처리에 관여되는 경우
- NAT되어 checksum이 재계산 되어야 하거나 IP계층에서 비슷한 개입이 발생하는 경우
- Linux kernel의 ip_nat_fn 함수를 참고
1.5. 리눅스 커널(Linux kernel) 에서의 checksum 관점 정리
리눅스의 경우 unsigned long으로 합만 하는 과정인 nofold(또는 partial과 일부 의미 비슷)과 이를 최종 상위 값을 16bit로 합산하는 fold 과정을 나누어 처리함으로써 매번할 필요는 없는 1의 보수 합을 fold로 분리하여 다루게 됩니다. (이를 구현한 대표적인 함수는 ip_fast_csum 이 있으며 그 밖에도 많은 함수가 나뉘어 있음)
- 대부분의 아키텍쳐에서 어셈블리로 구현된 함수 : "static inline unsigned short ip_fast_csum(unsigned char *iph, unsigned int ihl);"
- 송신부에서는 호출전에 iph의 checksum 을 0으로 초기화 후에 호출하여 사용합니다.
- 포워딩이나 수신단계에서는 iph의 checksum값이 있는 상태로 이 함수를 호출하여 반환값 0이 반환되면 그 checksum이 맞다는 것을 확인하도록 사용한다는 점에 흥미로운 사용법이 존재합니다.
- 결국 매번 fold 하는 작업을 줄임으로써 성능에 이점을 얻으려는 아이디어에서 만들어진 것이 nofold와 fold를 분리한다는 것!
- skb->ip_summed 는 IP checksum만의 이야기가 아니고 L4 checksum과 관계가 있습니다.
- 무언가 checksum 갱신이 될만한 사항은 이 값이 그에 맞는 값으로 상태값을 가져야 합니다.
- CHECKSUM_NONE 또는 CHECKSUM_COMPLETE
- 수신 패킷 (인입시 장치로부터 설정되는 skb->ip_summed)
- skb->csum 값이 유효하지 않음을 의미
- 장치가 하드웨어 checksum을 지원하지 못하는 경우
- 충돌이 있는 프레임인 경우 (이 경우 하드웨어는 패킷을 버리지 않고 수신검증과정에서 이를 확인하여 검증하고 유무를 판단하는게 의도)
- skb->csum 값이 유효하지 않음을 의미
- 송출 패킷 (TX 시에 skb->ip_summed)
- checksum을 완료했으므로 장치에서 checksum을 더 할게 없음을 표현
- 수신 패킷 (인입시 장치로부터 설정되는 skb->ip_summed)
- CHECKSUM_UNNECESSARY
- 송출/수신 패킷 (인입시 장치로부터 설정되거나 TX시에 skb->ip_summed)
- NIC는 L4헤더의 체크섬과 의사 헤더의 체크섬(선택적)을 계산하고 검증함. 더 이상의 L4 checksum 검증 할 필요 없는 만큼 신뢰되는 패킷임을 의미
- 오류가능성이 매우 낮은 경우 설정됨
- 보통은 loopback 인 경우 구간신뢰가 가능하기 때문에 설정됨
- 송출/수신 패킷 (인입시 장치로부터 설정되거나 TX시에 skb->ip_summed)
- CHECKSUM_PARTIAL
- 송출 패킷 (TX 시에 skb->ip_summed)
- 특정 부분까지 checksum을 수행(pseudo checksum)하였으나 나머지 L4 모든 부분에 대한 checksum이 수행되지 않은 상태 (보통 pseudo checksum은 계산하였으나 L4 checksum이 미완료인 상태)
- 송출 직전까지는 checksum이 더 계산되어 완료되어야 하는 상태 !
- IP header 가 변경될 때 L4 checksum은 다시 계산되어야 하는 경우가 빈번할 수 있으며 이것을 IP header 가 변경될 때는 pseudo checksum만 재계산하고 나머지 부분의 L4 checksum부분을 최종 전송 시점으로 미루어 불필요하게 다시 L4 계산하는 것을 줄일 수 있도록 한 것이 PARTIAL 상태 유지 관점의 구현입니다.
- skb가 GSO/LRO 수신 구조의 L4 세그먼트를 IP 분할해야 하고 어차피 이 때 IP 헤더가 추가로 만들어지기 때문에 L4 checksum을 전송하기 이전에 완료하는 것이 의미가 없으므로 이 때 partial 상태를 유지하도록 하는 것이 유리할겁니다.
- skb->csum_start 는 skb->head 로부터 아직 계산되지 않은 offset 을 나타냅니다.
- skb->csum_offset 은 skb->csum_start 로부터 실제 checksum이 기록되어야 할 위치에 대한 offset을 나타냅니다.
- skb->csum_not_inet 은 partial csum 값이 IP checksum 값이 아닌 CRC32c 등의 계산을 의미합니다. (SCTP protocol 같은 경우 이 경우가 있을 수 있겠습니다.)
- hard_start_xmit 함수에 의해서 skb->csum_start 부터 skb->end 까지를 남은 계산을 수행하고 skb->csum_start + skb->csum_offset 에 checksum 결과를 기록합니다.
- 특정 부분까지 checksum을 수행(pseudo checksum)하였으나 나머지 L4 모든 부분에 대한 checksum이 수행되지 않은 상태 (보통 pseudo checksum은 계산하였으나 L4 checksum이 미완료인 상태)
- 송출 패킷 (TX 시에 skb->ip_summed)
- CHECKSUM_HW (장치 드라이버에서 내부적으로 사용)
- 수신 패킷 (인입시 장치로부터 설정되는 skb->ip_summed)
- NIC는 L4 헤더와 payload 에 대한 checksum을 수행하였고 skb->csum 필드에 복사하였음을 의미
- 수신부에서는 이를 의사헤더와 함께 체크하여 검증하기만 하면 됨
- 송출 패킷 (TX 시에 skb->ip_summed)
- protocol 의 의사 헤더의 checksum을 해당 Header에 저장 후 나머지에 대한 checksum을 장치에서 수행하도록 함
- 수신 패킷 (인입시 장치로부터 설정되는 skb->ip_summed)
- CHECKSUM_NONE 또는 CHECKSUM_COMPLETE
- 무언가 checksum 갱신이 될만한 사항은 이 값이 그에 맞는 값으로 상태값을 가져야 합니다.
- 장치가 checksum을 처리할 수 있는지에 따른 통제를 위한 플래그 net_device->features
- NETIF_F_NO_CSUM
- 장치가 매우 신뢰되므로 L4 checksum이 필요하지 않은 경우 (보통 lookback 장치들)
- NETIF_F_IP_CSUM
- 장치가 하드웨어에서 L4 checksum을 계산 가능하지만 TCP, UDP만 해당
- NETIF_F_HW_CSUM
- 장치가 어떠한 protocol 에 대해서도 L4 checksum을 계산 가능
- NETIF_F_NO_CSUM
- 리눅스에서 checksum을 담당하는 기본 주요 함수 요약 (실제로는 더 많이 있음, 이해를 좀 더 도모하기 위해서 대략적인 유사 구현을 함께 보여드리지만 실제 구현은 좀 다를 수 있습니다.)
- csum_fold / csum_unfold
- wsum type 이 32bits 인 경우 : 32bits 값의 최상위 16bits를 하위 16bits에 반영하는 과정(이를 fold한다고 함)을 처리하고 1의 보수 값
- wsum type 이 64bits 인 경우 : 64bits 값의 최상위 32bits를 하위 32bits에 반영하고 다시 그 하위 32bits의 상위 16bits를 하위 16bits에 반영하는 과정을 처리하고 1의 보수 값
- wsum type 과 sum16 type 을 이해할 필요가 있습니다.
- wsum type 은 sum16 type 보다 큰 bits로 정의하며 보통 architecture 에 최적화된 bits 로 정의합니다.
- wsum type 에서 sum16 type 으로 값을 맞추고 1의 보수 값을 취하는 것이 fold 과정이며 반대로 sum16에서 wsum type으로 확장하는 것은 unfold 과정입니다.
- 대략 다음과 유사한 구현입니다.
typedef uint16_t sum16_t; typedef uint32_t sum32_t; typedef uint64_t sum64_t; #if 0L # define wsum_width 32 typedef sum32_t wsum_t; #else # define wsum_width 64 typedef sum64_t wsum_t; #endif static __inline sum16_t csum_fold(wsum_t sum) { #if wsum_width == 64 /* 64bits 값을 32bits 로 줄일 때 상위 32bits를 하위에 합산 */ sum = (sum & 0xfffffffful) /* 하위 32bits */ + (sum >> 32) /* 상위 32bits */; sum = (sum & 0xfffffffful) /* 하위 32bits */ + (sum >> 32) /* 상위 32bits */; #endif /* 32bits 값을 16bits 로 줄일 때 상위 16bits를 하위에 합산 */ sum = (sum & 0xffffu) /* 하위 16bits */ + (sum >> 16) /* 상위 16bits */; sum = (sum & 0xffffu) /* 하위 16bits */ + (sum >> 16) /* 상위 16bits */; returm (sum16_t)(~sum); /* 1의 보수로 반환 */ } static __inline wsum_t csum_unfold(sum16_t sum) { return (wsum_t)sum; }
- csum_add / csum16_add / csumXX_add / ...
- 부호 없는 두 값을 더하고 이 때 발생되는 carry값을 다시 더하는 함수
- 대략 다음과 유사한 구현입니다.
static __inline wsum_t csum_add(wsum_t sum, wsum_t addend) { sum += addend; /* 주어진 sum 과 addend 를 합산 */ return sum + (sum < addend) /* carry */; } static __inline sum16_t csum16_add(sum16_t sum, sum16_t addend) { sum += addend; /* 주어진 sum 과 addend 를 합산 */ return sum + (sum < addend) /* carry */; }
- csum_sub / csum16_sub / csumXX_sub / ...
- 주어진 sum 에서 addend 를 빼는데 carry 도 함께 빼는 함수
- 대략 다음과 유사한 구현입니다.
static __inline wsum_t csum_sub(wsum_t sum, wsum_t addend) { return csum_add(sum, ~addend); } static __inline sum16_t csum16_sub(sum16_t sum, sum16_t addend) { return csum16_add(sum, ~addend); }
- csum_replace / csum_replace2 / csum_replace4 / csum_replaceX / ...
- 주어진 sum 에서 이전 값 from 을 빼고 최신 값 to를 더하여 from 에서 to 로 변경된 값 만큼은 sum을 갱신하는 함수
- 주로 NAT의 변환이나 TTL 감소 등에 의한 checksum 을 일부가 변경되는 경우 전체 checksum을 다시 계산하지 않고 변화된 부분만 checksum에 반영하는 함수
- 여기서 주어진 from과 to는 network order 기준으로 사용해야 합니다.
- 대략 다음과 유사한 구현입니다.
static __inline void csum_replace(wsum_t *sum_ptr, wsum_t from, wsum_t to) { *sum_ptr = ~csum_add(csum_sub(*sum_ptr, from), to); } static __inline void csum_replace2(sum16_t *sum_ptr, uint16_t from, uint16_t to) { *sum_ptr = ~csum16_add(csum16_sub(~(*sum_ptr), (sum16_t)from), (sum16_t)to); } static __inline void csum_replace4(sum16_t *sum_ptr, uint32_t from, uint32_t to) { *sum_ptr = csum_fold(csum_add(csum_sub(~csum_unfold(*sum_ptr), (wsum_t)from), (wsum_t)to)); }
- csum_partial / csum_partial_XX / ...
- 주어진 data 영역을 주어진 sum 에 합산합니다.
- 대략 다음과 유사한 구현입니다.
static __inline wsum_t csum_partial(const void *data, size_t size, wsum_t sum) { while (size >= sizeof(sum16_t)) { sum = csum_add(sum, (wsum_t)(*((const uint16_t *)data))); size -= sizeof(uint16_t); data = (const void *)(((const uint8_t *)data) + sizeof(uint16_t)); } if (size > ((size_t)0u)) { /* 남은 홀수 합산 */ sum = csum_add(sum, (wsum_t)htons(((uint16_t)(*((const uint8_t *)data))) << 8)); } return sum; }
- ip_compute_csum
- checksum을 수행하는 일반 목적의 함수
- csum_partial 후 csum_fold 하는 것
- 대략 다음과 유사한 구현입니다.
static __inline sum16_t ip_compute_csum(const void *data, size_t size) { return csum_fold(csum_partial(data, size, (wsum_t)0u)); }
- checksum을 수행하는 일반 목적의 함수
- ip_fast_csum
- IP Header와 길이(ip header의 ip length 필드 값 그대로)를 입력으로 받아 ip checksum을 계산하여 반환
- ip_compute_csum의 IP header만을 위한 최적화된 변형인 함수로 4 octet boundaries 조건 및 ihl 값의 범위제한조건등을 통하여 보다 최적화된 구현을 가질 수 있는 함수입니다.
- 대략 다음과 유사한 구현입니다.
static __inline sum16_t ip_fast_csum(const void *iph, unsigned int ihl) { /* ip_compute_csum optimized, 4 octet boundaries */ #if 0L /* fast, no carry */ wsum_t sum = (wsum_t)0u; while (ihl > 0u) { sum = csum_add(sum, (wsum_t)(*((const uint32_t *)iph))); --ihl; iph = (const void *)(((const uint8_t *)iph) + sizeof(uint32_t)); } return (sum16_t)(~wcsum_to_sum16((uint32_t)sum)); #else /* full */ return ip_compute_csum(iph, (size_t)(ihl << 2)); #endif }
- ip_send_check
- 송출되는 IP packet의 checksum을 계산
- iph->check를 0으로 초기화 하고 ip_fast_csum을 호출하는 wrapper 함수
- 대략 다음과 유사한 구현입니다.
static __inline void ip_send_check(struct iphdr *iph) { iph->check = 0; iph->check = ip_fast_csum((const void *)iph, (unsigned int)iph->ihl); }
- ip_decrease_ttl
- TTL을 감소시키고 갱신된 변화값만큼만 checksum을 갱신하는 함수 (checksum을 전체 계산하는 것이 아닌 TTL 1감소하는 것에 대한 update만 수행)
- ip_forward 에서 TTL 감소 및 checksum 갱신하는 목적으로 사용
- 대략 다음과 유사한 구현입니다.
static __inline int ip_decrease_ttl(struct iphdr *iph) { wsum_t sum = (wsum_t)iph->check; sum += (wsum_t)htons(0x0100); iph->check = (sum16_t)(sum + (sum >= ((wsum_t)0xffffu))); return --iph->ttl; }
- csum_tcpudp_nofold / csum_tcpudp_magic / tcp_v4_check / udp_v4_check / ...
- IPv4 TCP/UDP 의사헤더를 가상으로 만들어 checksum을 수행
- 보통은 csum_partial로 계산된 의사헤더를 제외한 부분의 checksum 결과를 먼저 얻고 이 값과 csum_tcpudp_magic에 의해서 계산된 의사헤더 checksum을 더하는 과정을 하게 됨
- 대략 다음과 유사한 구현입니다.
static __inline wsum_t csum_tcpudp_nofold(uint32_t saddr, uint32_t daddr, uint32_t len, uint8_t proto, wsum_t sum) { sum = csum_add(sum, (wsum_t)saddr); sum = csum_add(sum, (wsum_t)daddr); sum = csum_add(sum, (wsum_t)htonl(len)); sum = csum_add(sum, (wsum_t)htonl(proto)); return sum; } static __inline sum16_t csum_tcpudp_magic(uint32_t saddr, uint32_t daddr, uint32_t len, uint8_t proto, wsum_t sum) { return csum_fold(csum_tcpudp_nofold(saddr, daddr, len, proto, sum)); } static __inline sum16_t tcp_v4_check(uint32_t len, uint32_t saddr, uint32_t daddr, wsum_t sum) { return csum_tcpudp_magic(saddr, daddr, len, IPPROTO_TCP, sum); } static __inline sum16_t udp_v4_check(uint32_t len, uint32_t saddr, uint32_t daddr, wsum_t sum) { return csum_tcpudp_magic(saddr, daddr, len, IPPROTO_UDP, sum); }
- 참고) Pseudo header format (IPv4 인 경우, RFC793 Section-3.1)
Pseudo header format (IPv4) 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 Description 32-bit Source Address ↑↓8 bytes ↑↓12 bytes 32-bit Destination Address 8-bit Zero 8-bit PTCL(Protocol) 16-bit TCP Length (TCP Header + Data) ↑↓4 bytes
- csum_ipv6_magic / tcp_v6_check / udp_v6_check / ...
- IPv6 의사헤더를 가상으로 만들어 checksum을 수행
- 보통은 csum_partial로 계산된 의사헤더를 제외한 부분의 checksum 결과를 먼저 얻고 이 값과 csum_ipv6_magic에 의해서 계산된 의사헤더 checksum을 더하는 과정을 하게 됨
- 대략 다음과 유사한 구현입니다.
static __inline sum16_t csum_ipv6_magic(const struct in6_addr *saddr, const struct in6_addr *daddr, uint32_t len, uint8_t proto, wsum_t sum) { sum = csum_partial((const void *)saddr, sizeof(*saddr), sum); sum = csum_partial((const void *)daddr, sizeof(*saddr), sum); sum = csum_add(sum, (wsum_t)htonl(len)); sum = csum_add(sum, (wsum_t)htonl(proto)); return csum_fold(sum); } static __inline sum16_t tcp_v6_check(uint32_t len, const struct in6_addr *saddr, const struct in6_addr *daddr, wsum_t sum) { return csum_ipv6_magic(saddr, daddr, len, IPPROTO_TCP, sum); } static __inline sum16_t udp_v6_check(uint32_t len, const struct in6_addr *saddr, const struct in6_addr *daddr, wsum_t sum) { return csum_ipv6_magic(saddr, daddr, len, IPPROTO_UDP, sum); }
- 참고) Pseudo header format (IPv6 인 경우, RFC2460 Section-8.1)
Pseudo header format (IPv6) 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 Description 128-bit Source Address ↑↓32 bytes ↑↓40 bytes 128-bit Destination Address (Final destination address) 32-bit Upper-Layer Packet Length ↑↓4 bytes 24-bit Zero 8-bit Next Header ↑↓4 bytes
- csum_block_add
- checksum을 증가하는 방향으로 계산 (추가되는 block에 대한 checksum을 계산하여 이전 결과에 합산)
- csum_block_sub
- checksum을 빼는 방향으로 계산 (checksum된 결과에서 제거되는 block에 대한 checksum을 계산하여 뺌)
- skb_checksum
- skb에 특화된 일반 함수
- skb_checksum_help
- 인입 패킷에서는 L4 하드웨어 checksum을 검증하기 위해서 사용
- 인출 패킷에서는 L4 checksum을 계산
- skb->ip_summed 가 CHECKSUM_PARTIAL 상태인 경우 skb->ip_summed 가 CHECKSUM_NONE이 되도록 checksum 남은 부분을 완료처리하게 됩니다.
- csum_fold / csum_unfold
1.6. Linux Kernel 에서 checksum 관련 CASE by CASE 구현/대응 예시
- CASE) IPv4 header checksum 계산
- 방법1)
struct iphdr *iph = ip_hdr(skb); iph->check = 0; // 0으로 초기화 필요 iph->check = csum_fold(csum_partial(iph, iph->ihl << 2, 0));
- 방법2)
struct iphdr *iph = ip_hdr(skb); iph->check = 0; // 0으로 초기화 필요 iph->check = ip_compute_csum(iph, iph->ihl << 2);
- 방법3) => 대부분은 이 방법을 권장
struct iphdr *iph = ip_hdr(skb); iph->check = 0; // 0으로 초기화 필요 iph->check = ip_fast_csum(iph, iph->ihl /* 4 octet boundaries */); // ip_fast_csum 함수는 IPv4 header checksum 만을 위해서 성능적으로 특화된 함수입니다.
- 방법4)
struct iphdr *iph = ip_hdr(skb); ip_send_check(iph); // ip_send_check 함수는 내부적으로 iph->check을 0으로 초기화 하고 계산을 하여 iph->check 에 저장까지 합니다.
- 방법1)
- CASE) IPv4 header 의 저장된 checksum 값이 올바른지 검증
- 방법1)
struct iphdr *iph = ip_hdr(skb); if (unlikely(csum_fold(csum_partial(iph, iph->ihl << 2, 0)) != 0)) { /* checksum error */ }
- 방법2)
struct iphdr *iph = ip_hdr(skb); if (unlikely(ip_compute_csum(iph, iph->ihl << 2) != 0)) { /* checksum error */ }
- 방법3) => 대부분은 이 방법을 권장
struct iphdr *iph = ip_hdr(skb); if (unlikely(ip_fast_csum(iph, iph->ihl) != 0)) { /* checksum error */ }
- 방법1)
- CASE) IPv4 header 의 저장된 checksum 값이 올바른지 검증에서 checksum 오류가 검출되었을 때 iph->check 값을 다시 계산하는 방법 (일반적으로 요구되는 방법은 아니지만...)
- 방법1)
struct iphdr *iph = ip_hdr(skb); __sum16 verify_csum = ip_fast_csum(iph, iph->ihl); if (unlikely(verify_csum != 0)) { /* checksum error */ /* checksum 을 다시 계산하는 방법 (그냥 다시 계산하는 방법이지만 ip_fast_csum을 두 번이나 반복하는 계산이 있다는 점) */ iph->check = 0; iph->check = ip_fast_csum(iph, iph->ihl); /* 이제 IPv4 checksum이 올바르게 재계산되었습니다. */ }
- 방법2) => 대부분은 이 방법을 권장
struct iphdr *iph = ip_hdr(skb); __sum16 verify_csum = ip_fast_csum(iph, iph->ihl); if (unlikely(verify_csum != 0)) { /* checksum error */ /* 이미 ip_fast_csum 함수로 검증계산을 하였기 때문에 잘못된 값만 보정(뺄셈)하면 ip checksum 이 올바르게 된다는 점. */ iph->check = ~csum16_sub(~verify_csum /* 검증값이 1의 보수로 0이 아닌 상태 */, iph->check /* 잘못계산된 저장값을 빼주기 위해 */); /* 이제 IPv4 checksum이 올바르게 재계산되었습니다. */ }
- 방법1)
1.8. 참고사항
- OSI 7 계층모델
- TCP(Transmission Control Protocol)
- Linux Kernel의 skbuff(Socket buffer descriptors)에 대하여
- XDP(eXpress Data Path)
- DPDK(Data Plane Development Kit)
- RFC768 User Datagram Protocol
- RFC1071 - Computing the Internet Checksum
- O'REILLY사의 "Understanding LINUX NETWORK INTERNALS" 서적
- https://en.wikipedia.org/wiki/IPv4_header_checksum
- https://coding-factory.tistory.com/653
컴퓨터네트워크 제12강 UDP와Checksum 참고 영상