TCP/IP stack을 개발하는 것은 어려운 작업일 수 있으나 핵심사양은 비교적 간결하다.
중요한 부분은 TCP header 파싱, 상태 머신, 혼잡 제어, 재전송 시간 초과 계산이다.
가장 일반적인 Layer2와 Layer3 프로토콜인 Ethernet과 IP는 각각 TCP의 복잡성에 비하면 간단하다.
TUN/TAP 장치
리눅스 TAP 장치를 이용하여 리눅스 커널에서 저수준 네트워크 트래픽을 가로챌 수 있다.
TUN/TAP 장치는 일반적으로 User Space application이 각각 L3/L2 트래픽을 조작하는데 사용된다. 인기있는 예로는 패킷을 다른 패킷의 페이로드 내부에 래핑하는 터널링이 있다.
TUN/TAP 장치의 장점은 User Space 프로그램에서 설정이 쉽고 OpenVPN 과 같은 다수의 프로그램에서 이미 사용되고 있다는 점이다.
Layer2 부터 네트워크 스택을 구축하기 위해선 TAP 장치가 필요하다. 다음과 같이 인스턴스화 할 수 있다.
/*
* Kernel Documentation/networking/tuntap.txt에서 발췌
*/
int tun_alloc(char *dev)
{
struct ifreq ifr;
int fd, err;
if( (fd = open("/dev/net/tap", O_RDWR)) < 0 ) {
print_error("Cannot open TUN/TAP dev");
exit(1);
}
CLEAR(ifr);
/* Flags: IFF_TUN - TUN 장치 (이더넷 헤더 없음)
* IFF_TAP - TAP 장치
*
* IFF_NO_PI - 패킷 정보 제공 안 함
*/
ifr.ifr_flags = IFF_TAP | IFF_NO_PI;
if( *dev ) {
strncpy(ifr.ifr_name, dev, IFNAMSIZ);
}
if( (err = ioctl(fd, TUNSETIFF, (void *) &ifr)) < 0 ){
print_error("ERR: Could not ioctl tun: %s\n", strerror(errno));
close(fd);
return err;
}
strcpy(dev, ifr.ifr_name);
return fd;
}
이 후 반환된 파일 디스크립터 fd 를 사용하여 가상 장치의 이더넷 버퍼로 데이터를 읽고 쓸 수 있다.
여기서 IFF_NO_PI가 중요하다. 이 플래그를 넣지 않으면 이더넷 프레임에 불필요한 패킷 정보가 앞에 붙게 된다.
drivers/net/tun.c 파일을 보면 IFF_NO_PI 플래그가 설정되지 않은 경우, 패킷 정보 구조체를 프레임에 추가하는 것을 알 수 있다.
if (!(tun->flags & IFF_NO_PI)) {
if (len < sizeof(pi))
return -EINVAL;
len -= sizeof(pi);
if (!copy_from_iter_full(&pi, sizeof(pi), from))
return -EFAULT;
}
Ethernet Frame
용어 정리
- SFD : Start of Frame Delimiter
- DA : Destination MAC address
- SA : Source MAC address
- ToL : Type or Length
- FCS : Frame Check Sequence
- CRC : Cyclic Redundancy Check
- LLC : Logical Link Control
- DSAP : Destination Service Access Point
- SSAP : Source Service Access Point
- NIC : Network Interface Card
- SNAP : SubNetwork Access Protocol
다양한 이더넷 표준은 IEEE 802.3 작업 그룹에 의해 유지된다.
Ethernet Frame Header 는 다음과 같고 C 구조체로 선언할 수 있다. (https://www.ietf.org/rfc/rfc1042.txt)
Header Format
Header
...--------+--------+--------+
MAC Header | 802.{3/4/5} MAC
...--------+--------+--------+
+--------+--------+--------+
| DSAP=K1| SSAP=K1| Control| 802.2 LLC
+--------+--------+--------+
+--------+--------+---------+--------+--------+
|Protocol Id or Org Code =K2| EtherType | 802.2 SNAP
+--------+--------+---------+--------+--------+
#include <linux/if_ether.h>
struct eth_hdr
{
unsigned char dmac[6];
unsigned char smac[6];
uint16_t ethertype;
unsigned char payload[];
} __attribute__((packed));
dmac과 smac은 각각 dst, src mac address를 가리킨다.
overloaded 된 ethertype은 2 octet field로 그 값에 따라 payload length 또는 protocol type으로 사용될 수 있다.
- Payload의 길이 (Length)로 사용될 때: 1536(0x0600) 미만
- 페이로드의 길이를 바이트 단위로 나타냄
- 초기 이더넷 버전에서는 이 필드가 실제로 페이로드의 길이를 나타내는 데 사용됨
- 프로토콜 유형(Type)으로 사용될 때: 1536(0x0600) 이상
- 예를 들어, 0x0800은 IPv4를, 0x0806은 ARP를 나타냄
ethertype field 다음에는 ethernet frame 의 여러가지 tag가 올 수 있다. 여기서 tag는 frame의 vlan 또는 qos 유형을 설명하는데 사용될 수 있다. (여기선 생략)
payload field는 ethernet frame의 payload에 대한 pointer를 포함한다. 이 경우 ARP 또는 IPv4 패킷을 포함할 것이다. payload의 길이가 최소 48 byte(tag 제외) 보다 작으면 요구사항을 충족하기 위해 pad byte가 payload 끝에 추가된다.
또한 ethernet type과 해당 16진수 값 간의 매핑을 제공하는 if_ether.h 리눅스 헤더(이더넷 프로토콜에 관련된 상수와 구조체가 정의됨)를 포함한다.
#define ETH_P_IP 0x0800 // IPv4 프로토콜
#define ETH_P_ARP 0x0806 // ARP 프로토콜
#define ETH_P_IPV6 0x86DD // IPv6 프로토콜
마지막으로 ethernet frame 형식은 frame check sequence field 를 마지막에 포함하며 이는 순환 중복 검사(CRC)를 사용하여 프레임의 무결성을 검사하는데 사용한다. (여기선 생략)
Ethernet Frame Parsing
구조체 선언에서 packed 속성은 구현 세부 사항이다. 이는 GNU C 컴파일러에게 데이터 정렬을 위한 패딩 바이트로 구조체 메모리 레이아웃을 최적화하지 않도록 지시(구조체의 필드들 사이에 패딩 바이트를 추가하지 말라고 지시)하는데 사용된다.
packed 속성은 구조체의 필드들이 특정한 방법으로 메모리에 배치되도록 컴파일러에게 지시한다. 이를 통해 프로토콜 데이터와 같은 고정된 형식을 다룰 때 필요한 특정한 메모리 레이아웃을 강제할 수 있다. 프로토콜 데이터와 같은 정해진 형식과 직접적으로 매핑해야 할 때 유용하다.
( 구조체 필드 정렬 문제: 일반적으로 컴파일러는 구조체 필드의 정렬을 최적화하여 CPU의 성능을 향상시키려고 패딩 바이트를 추가한다. 하지만, 이러한 최적화는 고정된 바이너리 형식(예: 네트워크 패킷의 헤더)을 파싱할 때 문제를 일으킬 수 있다. packed를 사용하면 이러한 문제를 방지할 수 있다.)
( packed 속성을 사용하지 않고 데이터를 다루기 위해 수동으로 직렬화하는 방법도 있다. 수동 직렬화는 프로그래머가 직접 데이터의 바이트 순서를 조작하여 구조체 데이터를 특정 형식으로 변환하는 방법이다.)
// 수동 직렬화 예시
void serialize_eth_hdr(unsigned char *buf, struct eth_hdr *hdr) {
memcpy(buf, hdr->dmac, 6);
memcpy(buf + 6, hdr->smac, 6);
*(uint16_t *)(buf + 12) = htons(hdr->ethertype); // 네트워크 바이트 순서로 변환
// 이후에 payload 추가
}
packed 속성의 사용은 프로토콜 버퍼를 파싱하는 방법, 즉 적절한 프로토콜 구조체로 데이터 버퍼에 대한 타입캐스트로 구현한다.
struct eth_hdr *hdr = (struct eth_hdr *) buf;
수신된 ethernet frame을 파싱하고 처리하는 전체 시나리오는 간단하다.
if (tun_read(buf, BUFLEN) < 0) {
print_error("ERR: Read from tun_fd: %s\n", strerror(errno));
}
struct eth_hdr *hdr = init_eth_hdr(buf);
handle_frame(&netdev, hdr);
handle_frame 함수는 ethernet header의 ethertype field 를 살펴보고 그 값에 따라 다음 작업을 결정한다.
ARP(Address Resolution Protocol) Packet Structure
ARP는 48비트 이더넷 주소(MAC 주소) 를 프로토콜 주소 (예: IPv4) 로 동적으로 매핑하는데 사용된다. 여기서 핵심은 ARP를 사용하면 다양한 L3 프로토콜을 사용할 수 있다는 것이다. IPv4 뿐만 아니라 16비트 프로토콜 주소를 선언하는 CHAOS 와 같은 다른 프로토콜도 가능하다.
일반적인 경우 LAN에서 어떤 서비스의 IP 주소를 알고 있지만 실제 통신을 설정하려면 하드웨어 주소(MAC)도 알아야한다. 따라서 ARP는 네트워크에 브로드캐스트하고 쿼리하여 IP 주소의 소유자에게 하드웨어 주소를 보고하도록 요청한다.
ARP 패킷의 형식은 상대적으로 간단하다.
+--------+--------+--------+--------+
| HT | PT |
+--------+--------+--------+--------+
| HAL | PAL | OP |
+--------+--------+--------+--------+
| S_HA (bytes 0-3) |
+--------+--------+--------+--------+
| S_HA (bytes 4-5)|S_L32 (bytes 0-1)|
+--------+--------+--------+--------+
|S_L32 (bytes 2-3)|S_NID (bytes 0-1)|
+--------+--------+--------+--------+
| S_NID (bytes 2-5) |
+--------+--------+--------+--------+
|S_NID (bytes 6-7)| T_HA (bytes 0-1)|
+--------+--------+--------+--------+
| T_HA (bytes 3-5) |
+--------+--------+--------+--------+
| T_L32 (bytes 0-3) |
+--------+--------+--------+--------+
| T_NID (bytes 0-3) |
+--------+--------+--------+--------+
| T_NID (bytes 4-7) |
+--------+--------+--------+--------+
c로 구현하면 아래와 같다.
struct arp_hdr
{
uint16_t hwtype;
uint16_t protype;
unsigned char hwsize;
unsigned char prosize;
uint16_t opcode;
unsigned char data[];
} __attribute__((packed));
ARP 헤더 (arp_hdr)는 링크 계층 유형을 결정하는 2 octet hwtype을 포함한다. 이것은 ethernet의 경우이며 실제값은 0x0001이다.
2 octet의 protype field 는 protocol 유형을 나타낸다. 이 경우 IPv4 로 값 0x0800으로 전달된다.
hwsize, prosize field는 모두 1 octet 크기로 각각 하드웨어 및 프로토콜 필드의 크기를 포함한다.
이 경우 mac 주소 6 byte, ip 주소 4 byte가 된다.
2octet opcode field 는 ARP 메세지의 유형을 선언한다. 이는 ARP Request(1), ARP Response(2), RARP Request(3), RARP Response(4) 일 수 있다.
data field 는 ARP 메세지의 실제 payload 를 포함하며 이 경우 IPv4 의 특정 정보를 포함할 것이다.
struct arp_ipv4
{
unsigned char smac[6];
uint32_t sip;
unsigned char dmac[6];
uint32_t dip;
} __attribute__((packed));
smac, dmac은 송신자와 수신자의 6 byte MAC address를 포함하고 sip, dip 는 각각 송신자와 수신자의 IP address를 포함한다.
Address Resolution Algorithm
?Do I have the hardware type in ar$hrd?
Yes: (almost definitely)
[optionally check the hardware length ar$hln]
?Do I speak the protocol in ar$pro?
Yes:
[optionally check the protocol length ar$pln]
Merge_flag := false
If the pair <protocol type, sender protocol address> is
already in my translation table, update the sender
hardware address field of the entry with the new
information in the packet and set Merge_flag to true.
?Am I the target protocol address?
Yes:
If Merge_flag is false, add the triplet <protocol type,
sender protocol address, sender hardware address> to
the translation table.
?Is the opcode ares_op$REQUEST? (NOW look at the opcode!!)
Yes:
Swap hardware and protocol fields, putting the local
hardware and protocol addresses in the sender fields.
Set the ar$op field to ares_op$REPLY
Send the packet to the (new) target hardware address on
the same hardware on which the request was received.
변환 테이블은 ARP의 결과를 저장하는데 사용되며 이를 통해 호스트는 캐시에서 해당 항목을 이미 가지고 있는지 확인할 수 있다. 이를 통해 불필요한 ARP 요청으로 네트워크 스팸하는 것을 방지할 수 있다.
알고리즘은 arp.c에 구현되어있다.
마지막으로 ARP 구현의 궁극적인 테스트는 ARP 요청에 올바르게 응답하는지 확인하는 것이다.
$ ./arp_example
ARP CACHE INIT
Found ARP entry for IP: 3232235522
ARP Cache Contents:
State Timeout(s) HW Address IP Address
Resolved 30 06:05:04:03:02:01 192.168.0.2
Reference
- Let's code a TCP/IP stack, 1: Ethernet & ARP
- https://github.com/jc3wrld999/tap_ip
- https://www.iana.org/assignments/arp-parameters/arp-parameters.xml
'네트워크 보안 > 네트워크' 카테고리의 다른 글
Netlink Sockets # How to use (0) | 2024.11.03 |
---|---|
strongswan #1 IKEv2 소프트웨어 아키텍쳐, 메세지 포맷, config 설정 및 실행 방법 (0) | 2024.11.03 |
[C/C++] epoll (0) | 2024.07.30 |
Scalable Network Programming (3) | 2024.03.18 |
[Python/ MQTT] MQTT – Pub/Sub 모델 구현 (2) | 2022.08.26 |