Skip to content

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 소켓 → tcpdump

tcpdump는 패킷의 복사본을 받기 때문에, 커널의 정상적인 처리를 방해하지 않습니다.


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_capture

8. 정리: 계층 vs 접근 권한

데이터 접근 레벨

Socket Type접근 가능 계층권한 요구용도
AF_INET + SOCK_STREAML7만일반 사용자일반 애플리케이션 (웹, 이메일 등)
AF_INET + SOCK_DGRAML7만일반 사용자UDP 애플리케이션
AF_INET + SOCK_RAW (IPPROTO_TCP)L4 + L7 (IP 헤더 자동)CAP_NET_RAWping, traceroute
AF_INET + SOCK_RAW (IPPROTO_RAW)L3 + L4 + L7CAP_NET_RAW커스텀 IP 패킷
AF_PACKET + SOCK_RAWL2 + L3 + L4 + L7CAP_NET_RAWtcpdump, Wireshark
AF_PACKET + SOCK_DGRAML3 + 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)가 왜 볼 수 있나?"

답변:

  1. OSI 계층은 "역할"이지 "구현 위치"가 아닙니다

    • L4는 "전송 계층 역할"
    • 처리는 커널이 하지만, 데이터는 User Space에서 읽을 수 있음
  2. tcpdump는 AF_PACKET Raw Socket 사용

    • 커널이 처리하기 전 패킷의 복사본을 받음
    • 모든 계층(L2~L7)의 헤더를 직접 파싱
  3. 일반 애플리케이션과의 차이

    • 일반 앱: send("Hello") → 커널이 L4/L3/L2 헤더 추가
    • tcpdump: 모든 헤더가 포함된 Raw 데이터 수신
  4. 보안을 위해 root 권한 필요

    • AF_PACKET은 모든 네트워크 트래픽 볼 수 있음
    • CAP_NET_RAW capability 필요

비유

일반 애플리케이션 = 우편 서비스 사용자
  - 편지만 쓰면 됨 (L7 데이터)
  - 우체국(커널)이 봉투(L4), 주소(L3), 운송(L2)을 처리

tcpdump = 우체국 감시 카메라
  - 모든 우편물의 봉투, 주소, 내용물까지 볼 수 있음
  - 우편 서비스는 방해하지 않음 (복사본만 봄)
  - 관리자 권한(root) 필요

이제 이해가 되셨나요? 추가 질문 있으시면 말씀해주세요!