OSI 계층 vs 실제 구현 위치
질문: tcpdump(L7)가 어떻게 TCP(L4) 패킷을 볼 수 있나?
이것은 매우 중요한 개념적 질문입니다!
오해: "L4는 커널이니까 User Space에서 접근 불가능"
진실: "L4 처리는 커널이 하지만, Raw 데이터는 접근 가능"
1. OSI 계층 vs 구현 위치
OSI 계층의 의미
L7: Application → "역할": 사용자 서비스 제공
L4: Transport → "역할": End-to-End 신뢰성 보장
L3: Network → "역할": 라우팅, 주소 지정OSI 계층은 "누가 이 데이터를 처리하는가"가 아니라 "이 데이터의 역할이 무엇인가"입니다.
실제 구현 위치
User Space:
- 일반 애플리케이션 (HTTP 클라이언트 등)
- tcpdump, Wireshark (특수 권한 필요)
Kernel Space:
- TCP/UDP 프로토콜 처리
- IP 라우팅
- Ethernet 프레임 처리
Hardware:
- NIC (물리적 전송)2. tcpdump의 동작 원리
tcpdump는 어떻게 L4 패킷을 볼까?
c
// tcpdump의 핵심: AF_PACKET Raw Socket
#include <sys/socket.h>
#include <linux/if_packet.h>
#include <net/ethernet.h>
int main() {
// 1. Raw Socket 생성 (모든 패킷을 받음)
int sock = socket(AF_PACKET, SOCK_RAW, htons(ETH_P_ALL));
// ↑ ↑ ↑
// | | 모든 프로토콜
// | Raw 모드 (가공되지 않은 데이터)
// Packet 레벨 (L2부터 접근)
if (sock < 0) {
perror("socket"); // root 권한 필요!
return 1;
}
// 2. 패킷 수신
unsigned char buffer[65536];
while (1) {
ssize_t size = recvfrom(sock, buffer, sizeof(buffer), 0, NULL, NULL);
// 3. 모든 계층의 헤더를 직접 파싱
struct ethhdr *eth = (struct ethhdr *)buffer;
struct iphdr *ip = (struct iphdr *)(buffer + sizeof(struct ethhdr));
struct tcphdr *tcp = (struct tcphdr *)(buffer + sizeof(struct ethhdr) + ip->ihl * 4);
printf("L2: Src MAC=%02x:%02x:...\n", eth->h_source[0], eth->h_source[1]);
printf("L3: Src IP=%d.%d.%d.%d\n", ...);
printf("L4: Src Port=%d, Dst Port=%d\n", ntohs(tcp->source), ntohs(tcp->dest));
printf("L4: TCP Flags: SYN=%d ACK=%d\n", tcp->syn, tcp->ack);
}
return 0;
}핵심 포인트
tcpdump는 커널이 처리하기 전의 "Raw 데이터"를 복사해서 받습니다.
┌─────────────────────────────────────────────────────────────┐
│ User Space │
│ │
│ ┌────────────────┐ ┌─────────────────┐ │
│ │ 일반 애플리케이션 │ │ tcpdump │ │
│ │ (HTTP client) │ │ (AF_PACKET) │ │
│ │ │ │ │ │
│ │ send() │ │ recvfrom() │ │
│ └────┬───────────┘ └────────▲────────┘ │
│ │ │ │
│ │ System Call │ Raw Packet Copy │
└───────┼──────────────────────────────┼─────────────────────┘
│ │
▼ │
┌─────────────────────────────────────────────────────────────┐
│ Kernel Space │
│ │
│ ┌──────────────────────────────────────────────────────┐ │
│ │ Socket Layer │ │
│ └───────────┬──────────────────────────────────────────┘ │
│ ▼ │
│ ┌──────────────────────────────────────────────────────┐ │
│ │ TCP Layer (L4) │ │
│ │ - Segmentation, ACK, Retransmit │ │
│ └───────────┬──────────────────────────────────────────┘ │
│ ▼ │
│ ┌──────────────────────────────────────────────────────┐ │
│ │ IP Layer (L3) │ │
│ │ - Routing, Fragmentation │ │
│ └───────────┬──────────────────────────────────────────┘ │
│ ▼ │
│ ┌──────────────────────────────────────────────────────┐ │
│ │ Ethernet Layer (L2) │ │
│ │ - MAC addressing │ │
│ └───────────┬──────────────────────────────────────────┘ │
│ │ │
│ │ │
│ ├──────────────────────┐ │
│ │ │ Packet Copy (tap) │
│ ▼ ▼ │
│ ┌────────────────┐ ┌────────────────────────┐ │
│ │ Network Driver │ │ AF_PACKET Socket │ │
│ │ │ │ (tcpdump용 복사본) │ │
│ └───────┬────────┘ └───────────▲────────────┘ │
└──────────┼─────────────────────────┼────────────────────────┘
▼ │
[NIC] └─ recvfrom()으로 전달3. 커널의 Packet Tap 메커니즘
커널 내부 동작
c
// net/core/dev.c
// 패킷 수신 시
int netif_receive_skb(struct sk_buff *skb) {
// 1. 먼저 AF_PACKET 소켓으로 복사 전달
list_for_each_entry_rcu(ptype, &ptype_all, list) {
if (ptype->type == htons(ETH_P_ALL)) {
// tcpdump 같은 애플리케이션으로 복사본 전달
deliver_skb(skb, ptype->func, orig_dev);
// ↑
// packet_rcv() → AF_PACKET 소켓의 큐에 추가
}
}
// 2. 그 다음 정상적인 프로토콜 처리
// L2 → L3 → L4 순서대로 처리
__netif_receive_skb_core(skb);
return 0;
}
// net/packet/af_packet.c
int packet_rcv(struct sk_buff *skb, struct net_device *dev,
struct packet_type *pt, struct net_device *orig_dev) {
struct sock *sk;
// 1. skb 복사 (원본은 손상시키지 않음)
skb = skb_share_check(skb, GFP_ATOMIC);
// 2. AF_PACKET 소켓의 수신 큐에 추가
skb_queue_tail(&sk->sk_receive_queue, skb);
// 3. User Space의 recvfrom()을 깨움
sk->sk_data_ready(sk);
return 0;
}핵심: "복사본"을 전달
원본 패킷 → 정상 처리 (L2 → L3 → L4 → L7)
|
└─→ 복사본 → AF_PACKET 소켓 → tcpdumptcpdump는 패킷의 복사본을 받기 때문에, 커널의 정상적인 처리를 방해하지 않습니다.
4. 왜 root 권한이 필요한가?
bash
$ tcpdump
tcpdump: socket: Operation not permitted
$ sudo tcpdump
listening on eth0...이유: 보안
AF_PACKET 소켓은 모든 네트워크 트래픽을 볼 수 있습니다:
- 다른 사용자의 패킷
- 암호화되지 않은 비밀번호
- 민감한 정보
따라서 Linux 커널은 CAP_NET_RAW capability를 가진 프로세스만 허용합니다.
c
// net/packet/af_packet.c
static int packet_create(struct net *net, struct socket *sock, int protocol) {
// CAP_NET_RAW 권한 확인
if (!ns_capable(net->user_ns, CAP_NET_RAW))
return -EPERM; // Permission denied
// ...
}권한 확인:
bash
# tcpdump의 capabilities 확인
getcap /usr/bin/tcpdump
# 출력: /usr/bin/tcpdump cap_net_raw=eip
# 또는 setuid 비트 확인
ls -l /usr/bin/tcpdump
# 출력: -rwxr-xr-x root root ...5. 일반 애플리케이션 vs tcpdump
일반 애플리케이션 (HTTP 클라이언트 등)
c
int sock = socket(AF_INET, SOCK_STREAM, 0);
// ↑ ↑
// | TCP 사용
// IPv4
connect(sock, ...);
send(sock, "GET / HTTP/1.1\r\n...", ...);
// ↑
// L7 데이터만 전달
// L4, L3, L2는 커널이 자동으로 추가
recv(sock, buffer, ...);
// ↑
// L7 데이터만 받음
// L4 헤더는 이미 커널이 제거함일반 애플리케이션은 L7 데이터만 다룹니다. 하위 계층은 커널이 자동 처리.
tcpdump
c
int sock = socket(AF_PACKET, SOCK_RAW, htons(ETH_P_ALL));
// ↑ ↑ ↑
// | | 모든 프로토콜
// | 가공되지 않은 Raw 데이터
// L2부터 전부 접근
recvfrom(sock, buffer, ...);
// ↑
// L2, L3, L4, L7 모든 헤더 + 데이터
// 직접 파싱해야 함
struct ethhdr *eth = (struct ethhdr *)buffer;
struct iphdr *ip = (struct iphdr *)(buffer + 14);
struct tcphdr *tcp = (struct tcphdr *)(buffer + 14 + 20);
// ↑ 모든 헤더를 직접 읽음tcpdump는 모든 계층의 Raw 데이터를 받아서 직접 파싱합니다.
6. Socket API 비교
일반 소켓 (AF_INET)
c
// User Space
send(sock, "Hello", 5, 0);
↓ System Call
// Kernel Space
sys_send()
↓
tcp_sendmsg() // L4: TCP 헤더 추가
↓
ip_queue_xmit() // L3: IP 헤더 추가
↓
dev_queue_xmit() // L2: Ethernet 헤더 추가
↓
[NIC]
// 결과 패킷:
// [Eth Header][IP Header][TCP Header]["Hello"]User Space는 "Hello"만 알고, 헤더는 커널이 추가.
Raw 소켓 (AF_PACKET)
c
// User Space
unsigned char packet[100];
struct ethhdr *eth = (struct ethhdr *)packet;
struct iphdr *ip = (struct iphdr *)(packet + 14);
struct tcphdr *tcp = (struct tcphdr *)(packet + 34);
char *data = packet + 54;
// 모든 헤더를 직접 작성
memcpy(eth->h_dest, dst_mac, 6);
// ...
memcpy(data, "Hello", 5);
sendto(sock, packet, 100, 0, ...);
↓ System Call
// Kernel Space
packet_sendmsg()
↓
dev_queue_xmit() // 그대로 전달 (헤더 추가 안 함)
↓
[NIC]User Space가 모든 헤더를 직접 작성.
7. 실습: 계층별 데이터 접근
실습 1: 일반 소켓 (L7만 접근)
c
// normal_socket.c
#include <stdio.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <string.h>
#include <unistd.h>
int main() {
int sock = socket(AF_INET, SOCK_STREAM, 0);
struct sockaddr_in server;
server.sin_family = AF_INET;
server.sin_port = htons(80);
inet_pton(AF_INET, "93.184.216.34", &server.sin_addr);
connect(sock, (struct sockaddr*)&server, sizeof(server));
// L7 데이터만 전송
char *request = "GET / HTTP/1.1\r\nHost: example.com\r\n\r\n";
send(sock, request, strlen(request), 0);
// L7 데이터만 수신
char buffer[4096];
recv(sock, buffer, sizeof(buffer), 0);
printf("Received L7 data:\n%s\n", buffer);
// ↑ L4(TCP), L3(IP), L2(Ethernet) 헤더는 볼 수 없음
close(sock);
return 0;
}실습 2: Raw 소켓 (모든 계층 접근)
c
// raw_socket_capture.c
#include <stdio.h>
#include <sys/socket.h>
#include <linux/if_packet.h>
#include <net/ethernet.h>
#include <netinet/ip.h>
#include <netinet/tcp.h>
#include <arpa/inet.h>
int main() {
// Root 권한 필요!
int sock = socket(AF_PACKET, SOCK_RAW, htons(ETH_P_ALL));
if (sock < 0) {
perror("socket (root 권한 필요)");
return 1;
}
unsigned char buffer[65536];
printf("모든 계층의 패킷을 캡처합니다...\n\n");
while (1) {
ssize_t size = recvfrom(sock, buffer, sizeof(buffer), 0, NULL, NULL);
// L2: Ethernet 헤더
struct ethhdr *eth = (struct ethhdr *)buffer;
printf("=== L2: Ethernet ===\n");
printf("Dst MAC: %02x:%02x:%02x:%02x:%02x:%02x\n",
eth->h_dest[0], eth->h_dest[1], eth->h_dest[2],
eth->h_dest[3], eth->h_dest[4], eth->h_dest[5]);
printf("Src MAC: %02x:%02x:%02x:%02x:%02x:%02x\n",
eth->h_source[0], eth->h_source[1], eth->h_source[2],
eth->h_source[3], eth->h_source[4], eth->h_source[5]);
if (ntohs(eth->h_proto) == ETH_P_IP) {
// L3: IP 헤더
struct iphdr *ip = (struct iphdr *)(buffer + sizeof(struct ethhdr));
printf("\n=== L3: IP ===\n");
struct in_addr src, dst;
src.s_addr = ip->saddr;
dst.s_addr = ip->daddr;
printf("Src IP: %s\n", inet_ntoa(src));
printf("Dst IP: %s\n", inet_ntoa(dst));
printf("Protocol: %d (", ip->protocol);
if (ip->protocol == IPPROTO_TCP) printf("TCP");
else if (ip->protocol == IPPROTO_UDP) printf("UDP");
else if (ip->protocol == IPPROTO_ICMP) printf("ICMP");
printf(")\n");
if (ip->protocol == IPPROTO_TCP) {
// L4: TCP 헤더
struct tcphdr *tcp = (struct tcphdr *)(buffer + sizeof(struct ethhdr) + ip->ihl * 4);
printf("\n=== L4: TCP ===\n");
printf("Src Port: %u\n", ntohs(tcp->source));
printf("Dst Port: %u\n", ntohs(tcp->dest));
printf("Seq: %u\n", ntohl(tcp->seq));
printf("Ack: %u\n", ntohl(tcp->ack_seq));
printf("Flags: ");
if (tcp->syn) printf("SYN ");
if (tcp->ack) printf("ACK ");
if (tcp->psh) printf("PSH ");
if (tcp->fin) printf("FIN ");
if (tcp->rst) printf("RST ");
printf("\n");
// L7: Application 데이터
int header_size = sizeof(struct ethhdr) + ip->ihl * 4 + tcp->doff * 4;
int data_size = size - header_size;
if (data_size > 0) {
printf("\n=== L7: Application Data ===\n");
printf("Size: %d bytes\n", data_size);
printf("Data (first 100 bytes): ");
for (int i = 0; i < (data_size < 100 ? data_size : 100); i++) {
char c = buffer[header_size + i];
if (c >= 32 && c <= 126) // 출력 가능한 문자만
printf("%c", c);
else
printf(".");
}
printf("\n");
}
}
}
printf("\n========================================\n\n");
// HTTP 패킷만 보고 싶으면 여기서 break
// break;
}
close(sock);
return 0;
}컴파일 및 실행:
bash
# 일반 소켓 (root 불필요)
gcc -o normal_socket normal_socket.c
./normal_socket
# Raw 소켓 (root 필요)
gcc -o raw_socket_capture raw_socket_capture.c
sudo ./raw_socket_capture8. 정리: 계층 vs 접근 권한
데이터 접근 레벨
| Socket Type | 접근 가능 계층 | 권한 요구 | 용도 |
|---|---|---|---|
| AF_INET + SOCK_STREAM | L7만 | 일반 사용자 | 일반 애플리케이션 (웹, 이메일 등) |
| AF_INET + SOCK_DGRAM | L7만 | 일반 사용자 | UDP 애플리케이션 |
| AF_INET + SOCK_RAW (IPPROTO_TCP) | L4 + L7 (IP 헤더 자동) | CAP_NET_RAW | ping, traceroute |
| AF_INET + SOCK_RAW (IPPROTO_RAW) | L3 + L4 + L7 | CAP_NET_RAW | 커스텀 IP 패킷 |
| AF_PACKET + SOCK_RAW | L2 + L3 + L4 + L7 | CAP_NET_RAW | tcpdump, Wireshark |
| AF_PACKET + SOCK_DGRAM | L3 + L4 + L7 (L2 자동) | CAP_NET_RAW | 드물게 사용 |
계층별 처리 위치 vs 접근 가능성
계층 역할 처리 위치 User Space 접근
────────────────────────────────────────────────────────────
L7 Application User Space ✓ (모든 소켓)
L6 Presentation User Space ✓ (모든 소켓)
L5 Session User Space ✓ (모든 소켓)
L4 Transport Kernel ✓ (AF_PACKET, SOCK_RAW)
L3 Network Kernel ✓ (AF_PACKET, SOCK_RAW)
L2 Data Link Kernel ✓ (AF_PACKET만)
L1 Physical Hardware ✗ (불가능)핵심: "처리는 커널이 하지만, 데이터 읽기는 User Space에서 가능"
9. 왜 이런 설계를 했을까?
일반 애플리케이션 (AF_INET)
장점:
- 간단한 API (send/recv만 사용)
- 커널이 복잡한 처리 담당 (재전송, 라우팅 등)
- 안전 (다른 사용자 패킷 볼 수 없음)
단점:
- 하위 계층 제어 불가능
Raw Socket (AF_PACKET)
장점:
- 모든 계층 접근 가능
- 네트워크 분석, 보안 도구 제작 가능
- 커스텀 프로토콜 구현 가능
단점:
- 복잡한 구현 (모든 헤더 직접 파싱)
- Root 권한 필요
- 보안 위험 (잘못 사용하면)
결론
계층 분리의 목적은 추상화입니다.
- 일반 애플리케이션은 복잡한 네트워크 세부사항을 몰라도 됨
- 특수 도구(tcpdump)는 필요할 때 하위 계층 접근 가능
- 커널은 두 가지 모드를 모두 제공
10. 다른 예시: BPF (Berkeley Packet Filter)
tcpdump는 단순히 모든 패킷을 받는 게 아니라, 커널 내부에서 필터링할 수 있습니다.
bash
# HTTP 패킷만 캡처 (커널이 필터링)
tcpdump 'tcp port 80'
# SYN 패킷만 캡처
tcpdump 'tcp[tcpflags] & tcp-syn != 0'BPF 동작
c
// User Space: tcpdump
struct sock_filter bpf_code[] = {
// BPF 어셈블리 (패킷 필터링 조건)
{ 0x28, 0, 0, 0x0000000c }, // Load EtherType
{ 0x15, 0, 3, 0x00000800 }, // If IPv4, continue
{ 0x30, 0, 0, 0x00000017 }, // Load Protocol
{ 0x15, 0, 1, 0x00000006 }, // If TCP, continue
{ 0x06, 0, 0, 0x00040000 }, // Return: match
{ 0x06, 0, 0, 0x00000000 }, // Return: no match
};
struct sock_fprog bpf = {
.len = sizeof(bpf_code) / sizeof(bpf_code[0]),
.filter = bpf_code,
};
setsockopt(sock, SOL_SOCKET, SO_ATTACH_FILTER, &bpf, sizeof(bpf));
// ↓ 커널에 BPF 코드 전달
// Kernel Space: net/core/filter.c
// 패킷 도착 시 BPF 코드 실행
// 매치되면 AF_PACKET 소켓으로 전달
// 매치 안 되면 버림이렇게 하면 불필요한 패킷이 User Space로 복사되지 않아 효율적입니다.
최종 정리
질문: "L4는 커널인데 tcpdump(L7)가 왜 볼 수 있나?"
답변:
OSI 계층은 "역할"이지 "구현 위치"가 아닙니다
- L4는 "전송 계층 역할"
- 처리는 커널이 하지만, 데이터는 User Space에서 읽을 수 있음
tcpdump는 AF_PACKET Raw Socket 사용
- 커널이 처리하기 전 패킷의 복사본을 받음
- 모든 계층(L2~L7)의 헤더를 직접 파싱
일반 애플리케이션과의 차이
- 일반 앱: send("Hello") → 커널이 L4/L3/L2 헤더 추가
- tcpdump: 모든 헤더가 포함된 Raw 데이터 수신
보안을 위해 root 권한 필요
- AF_PACKET은 모든 네트워크 트래픽 볼 수 있음
- CAP_NET_RAW capability 필요
비유
일반 애플리케이션 = 우편 서비스 사용자
- 편지만 쓰면 됨 (L7 데이터)
- 우체국(커널)이 봉투(L4), 주소(L3), 운송(L2)을 처리
tcpdump = 우체국 감시 카메라
- 모든 우편물의 봉투, 주소, 내용물까지 볼 수 있음
- 우편 서비스는 방해하지 않음 (복사본만 봄)
- 관리자 권한(root) 필요이제 이해가 되셨나요? 추가 질문 있으시면 말씀해주세요!