패킷 캡처 (tcpdump / libpcap / AF_PACKET)
Linux 커널에서 tcpdump가 패킷을 캡처하는 전 과정을 설명합니다. RX/TX 경로별 캡처 포인트, AF_PACKET 소켓 동작, cBPF 필터 컴파일, TPACKET_V3 ring buffer, 그리고 tcpdump로 볼 수 없는 패킷 유형까지 커널 내부 관점에서 다룹니다.
핵심 요약
- AF_PACKET 소켓 — 커널이 tcpdump에 패킷 복사본을 전달하는 메커니즘.
ptype_all리스트에 등록되어 모든 수신/송신 패킷의 복제본을 받습니다. - cBPF 필터 — tcpdump 표현식이 커널 내부 바이트코드로 컴파일되어 불필요한 패킷을 커널에서 미리 걸러냅니다. 이를 통해 사용자 공간 복사 오버헤드(Overhead)를 최소화합니다.
- 캡처 포인트 — RX에서는
__netif_receive_skb_core()내 L3 처리 직전, TX에서는dev_queue_xmit_nit()의 드라이버 전달 직전. 이 위치가 tcpdump가 보고 못 보는 것을 결정합니다. - skb_clone — 패킷 복제 시 데이터 버퍼는 참조 카운트(Reference Count)로 공유하고 헤더 구조만 복사합니다. 완전한 메모리 복사보다 훨씬 빠릅니다.
- TPACKET_V3 — 블록(Block) 단위 mmap 공유 ring buffer로
recvmsg()시스템콜 없이 고성능 캡처를 실현합니다.
단계별 이해
- 소켓 생성과 등록
libpcap이socket(AF_PACKET, SOCK_RAW, ETH_P_ALL)을 호출하면 커널은packet_sock을 할당하고ptype_all리스트에 등록합니다. 이후 인터페이스를 통과하는 모든 패킷이 이 소켓으로 전달됩니다. - 패킷 복제 (skb_clone)
패킷이 수신될 때 커널은skb_clone()으로 sk_buff 헤더만 복제하고 실제 데이터는 참조 카운트를 높여 공유합니다. 원본 패킷 처리 경로는 영향을 받지 않습니다. - BPF 필터 실행
복제된 패킷마다 설치된 cBPF/eBPF 필터를 실행합니다. 필터를 통과하지 못한 패킷은 사용자 공간으로 복사되지 않아 CPU 부하를 줄입니다. - ring buffer 전달
TPACKET_V3에서는 필터를 통과한 패킷을 mmap 공유 메모리(ring buffer)의 현재 블록에 기록합니다. 블록이 가득 차거나 타임아웃이 되면 상태가TP_STATUS_USER로 변경됩니다. - 사용자 공간 읽기
libpcap이poll()로 블록 준비를 대기하다가 준비되면 mmap 포인터를 통해 직접 읽습니다. tcpdump에 전달하여 화면에 출력하거나 pcap 파일로 저장합니다.
개요: tcpdump, libpcap, AF_PACKET의 관계
tcpdump는 패킷 캡처 라이브러리인 libpcap을 기반으로 동작합니다. libpcap은 리눅스에서 AF_PACKET 소켓을 사용해 커널 네트워크 스택(Network Stack)의 특정 지점에서 패킷 복사본을 받습니다. 세 계층의 역할은 다음과 같이 구분됩니다.
| 계층 | 역할 | 위치 |
|---|---|---|
| tcpdump | 사용자 인터페이스 — 필터 표현식 파싱, 패킷 출력 포매팅, pcap 파일 저장 | User Space |
| libpcap | 플랫폼 추상화 — AF_PACKET 소켓 생성/관리, BPF 필터 컴파일·설치, TPACKET ring buffer 설정 | User Space |
| AF_PACKET | 커널 캡처 엔진 — 지정 인터페이스의 패킷을 복제하여 소켓 버퍼(Buffer)에 전달 | Kernel Space |
libpcap이 AF_PACKET 소켓을 열면 커널은 해당 인터페이스의 패킷 수신/송신 경로 훅 포인트에 이 소켓을 등록합니다. 이후 인터페이스를 통과하는 모든 패킷(또는 BPF 필터를 통과한 패킷)의 복사본이 소켓 버퍼에 쌓이고, libpcap이 이를 읽어 tcpdump로 전달합니다.
RX 경로: __netif_receive_skb에서 AF_PACKET 소켓으로 패킷이 복제됩니다.
커널 캡처 포인트: RX와 TX 경로
tcpdump가 패킷을 캡처하는 정확한 위치는 RX(수신)와 TX(송신) 경로에서 각각 다릅니다. 이 위치가 "tcpdump로 볼 수 있는 것"과 "볼 수 없는 것"을 결정합니다.
RX 경로 캡처 포인트
수신 방향의 캡처 포인트는 __netif_receive_skb_core() 함수 내부, 구체적으로 ptype_all 리스트를 순회하는 시점입니다.
RX 경로: XDP_DROP된 패킷은 캡처되지 않으며, GRO 병합 후 skb 형태가 캡처됩니다.
핵심 코드 경로는 다음과 같습니다. deliver_ptype_list_skb()가 ptype_all 리스트를 순회하며 등록된 AF_PACKET 소켓마다 deliver_skb()를 호출합니다.
/* net/core/dev.c */
static int __netif_receive_skb_core(struct sk_buff **pskb, bool pfmemalloc,
struct packet_type **ppt_prev)
{
struct net_device *orig_dev = skb->dev;
/* ptype_all: ETH_P_ALL로 등록된 소켓 (tcpdump 등) */
list_for_each_entry_rcu(ptype, &ptype_all, list) {
if (pt_prev)
deliver_skb(skb, pt_prev, orig_dev); /* 이전 핸들러에 전달 */
pt_prev = ptype;
}
/* 인터페이스별 ptype_all 리스트도 순회 */
list_for_each_entry_rcu(ptype, &skb->dev->ptype_all, list) {
if (pt_prev)
deliver_skb(skb, pt_prev, orig_dev);
pt_prev = ptype;
}
/* L3 프로토콜 핸들러 결정 (ip_rcv 등) */
deliver_ptype_list_skb(skb, &pt_prev, orig_dev, type, &ptype_base[...]);
}
코드 설명
- 7-11행
ptype_all은 전역 리스트입니다. AF_PACKET 소켓이ETH_P_ALL로 등록되면 여기에 추가됩니다. 모든 패킷에 대해 이 리스트가 순회됩니다. - 13-17행특정 인터페이스에만 바인딩된 AF_PACKET 소켓은
skb->dev->ptype_all에 등록됩니다.tcpdump -i eth0처럼 인터페이스를 지정하면 여기에 등록됩니다. - 20행ptype_all 처리 후에야 L3 프로토콜 핸들러(Handler)가 결정됩니다. 즉 tcpdump는 L3 처리 이전에 패킷을 받습니다.
TX 경로 캡처 포인트
송신 방향의 캡처 포인트는 dev_queue_xmit_nit() 함수입니다. 트래픽 제어(Traffic Control) 큐잉 레이어인 Qdisc를 거친 후, 드라이버의 ndo_start_xmit()에 패킷이 전달되기 직전에 호출됩니다.
TX 경로: Netfilter OUTPUT/POSTROUTING 이후, 드라이버 전달 직전에 캡처됩니다.
| 경로 | 캡처 함수 | 위치 | 특징 |
|---|---|---|---|
| RX | __netif_receive_skb_core() | L3 처리 전 | Netfilter PREROUTING 전에 캡처 → iptables DROP 패킷도 보임 |
| TX | dev_queue_xmit_nit() | 드라이버 전달 전 | Netfilter OUTPUT/POSTROUTING 후에 캡처 → NAT 변환 후 주소가 보임 |
AF_PACKET 소켓 동작
libpcap이 캡처를 시작할 때 내부적으로 수행하는 작업은 다음과 같습니다.
/* libpcap이 내부적으로 수행하는 시스템콜 순서 */
/* 1. AF_PACKET raw 소켓 생성 */
int fd = socket(AF_PACKET, SOCK_RAW, htons(ETH_P_ALL));
/* 2. 특정 인터페이스에 바인딩 (tcpdump -i eth0) */
struct sockaddr_ll sll = {
.sll_family = AF_PACKET,
.sll_ifindex = if_nametoindex("eth0"),
.sll_protocol = htons(ETH_P_ALL),
};
bind(fd, (struct sockaddr *)&sll, sizeof(sll));
/* 3. Promiscuous 모드 활성화 (다른 호스트 패킷도 수신) */
struct packet_mreq mreq = {
.mr_ifindex = if_nametoindex("eth0"),
.mr_type = PACKET_MR_PROMISC,
};
setsockopt(fd, SOL_PACKET, PACKET_ADD_MEMBERSHIP, &mreq, sizeof(mreq));
/* 4. BPF 필터 설치 (tcpdump 표현식 컴파일 결과) */
struct sock_fprog bpf = { .len = filter_len, .filter = filter_insns };
setsockopt(fd, SOL_SOCKET, SO_ATTACH_FILTER, &bpf, sizeof(bpf));
/* 5. TPACKET_V3 ring buffer 설정 (고성능 캡처) */
int ver = TPACKET_V3;
setsockopt(fd, SOL_PACKET, PACKET_VERSION, &ver, sizeof(ver));
struct tpacket_req3 req3 = { .tp_block_size = 1 << 22, ... };
setsockopt(fd, SOL_PACKET, PACKET_RX_RING, &req3, sizeof(req3));
코드 설명
- 4행
SOCK_RAW+ETH_P_ALL: 모든 EtherType의 패킷을 L2 프레임 형태로 수신합니다. 이 소켓이 커널의ptype_all리스트에 등록됩니다. - 7-12행인터페이스를 지정하면 해당 인터페이스의
ptype_all에만 등록됩니다. 지정하지 않으면 전역ptype_all에 등록되어 모든 인터페이스의 패킷을 수신합니다. - 15-19행Promiscuous(무차별) 모드: 커널이
ndo_set_rx_mode()를 호출하여 NIC 하드웨어를 무차별 수신 모드로 설정합니다. 다른 호스트를 목적지로 하는 패킷도 받게 됩니다. - 22-24행BPF 필터를 설치하면 커널 내부에서 패킷을 미리 필터링합니다. 필터를 통과하지 못한 패킷은 사용자 공간(User Space)으로 복사되지 않아 CPU/메모리 오버헤드(Overhead)가 줄어듭니다.
- 27-30행TPACKET_V3 ring buffer: mmap으로 커널-사용자 공간 공유 버퍼를 설정합니다.
recvmsg()시스템콜 없이 패킷을 읽을 수 있어 고성능 캡처가 가능합니다.
-i any: 모든 인터페이스 캡처
tcpdump -i any는 특정 인터페이스가 아닌 전역 ptype_all에 등록하여 모든 인터페이스의 패킷을 캡처합니다. 이때 실제 L2 헤더를 제공하는 대신 Linux Cooked Capture (SLL, LINKTYPE_LINUX_SLL) 가상 헤더를 사용합니다.
| 구분 | -i eth0 | -i any |
|---|---|---|
| 등록 위치 | eth0->ptype_all | 전역 ptype_all |
| L2 헤더 | 실제 Ethernet 헤더 | Linux SLL 가상 헤더 (16바이트) |
| 링크 타입 | LINKTYPE_ETHERNET (1) | LINKTYPE_LINUX_SLL (113) |
| loopback 캡처 | 불가 (eth0 한정) | 가능 |
| Promiscuous 모드 | 활성화됨 | 활성화되지 않음 |
| 성능 | 상대적으로 빠름 | 모든 인터페이스 처리로 약간 느림 |
AF_PACKET 소켓 내부 구현 분석
libpcap이 socket(AF_PACKET, SOCK_RAW, ETH_P_ALL)을 호출하면 커널의 packet_create() 함수가 실행됩니다. 소켓이 생성되고 패킷 수신 경로에 등록되는 전 과정을 함수 수준으로 분석합니다.
AF_PACKET 소켓 초기화 5단계: 생성 → 바인딩 → Promiscuous → BPF 필터 → TPACKET ring
/* net/packet/af_packet.c — packet_create() 핵심 흐름 */
static int packet_create(struct net *net, struct socket *sock,
int protocol, int kern)
{
struct sock *sk;
struct packet_sock *po;
__be16 proto = (__force __be16)protocol;
/* packet_sock 구조체 할당 (sock을 첫 필드로 내포) */
sk = sk_alloc(net, PF_PACKET, GFP_KERNEL, &packet_proto, kern);
if (!sk)
goto out;
sock->ops = &packet_ops; /* SOCK_RAW 연산 테이블 설정 */
sock_init_data(sock, sk);
po = pkt_sk(sk); /* sock → packet_sock 캐스트 */
sk->sk_family = PF_PACKET;
po->num = proto;
po->xmit = dev_queue_xmit;
/* 기본 콜백: TPACKET ring 미설정 시 packet_rcv() 사용 */
po->prot_hook.func = packet_rcv;
po->prot_hook.af_packet_priv = sk;
po->prot_hook.dev = NULL; /* bind() 전: 전역 ptype_all 대상 */
if (proto) {
po->prot_hook.type = proto;
__register_prot_hook(sk); /* dev_add_pack() → ptype_all 연결 */
}
/* 네트워크 네임스페이스별 소켓 목록 등록 */
sk_add_node_tail_rcu(sk, &net->packet.sklist);
return 0;
out:
return -ENOBUFS;
}
코드 설명
- 9행
sk_alloc()은packet_proto.obj_size에 지정된sizeof(struct packet_sock)만큼 메모리를 할당합니다.sock이 첫 번째 필드이므로pkt_sk()매크로(Macro)가 안전하게 포인터 캐스트를 수행합니다. - 14-16행
sock->ops = &packet_ops:recvmsg(),bind()등 소켓 연산이packet_ops구조체의 함수 포인터를 통해 처리됩니다. SOCK_RAW와 SOCK_DGRAM은 각각 다른 ops 구조체를 사용합니다. - 22-24행
prot_hook.dev = NULL: bind() 이전에는 전역ptype_all에 등록됩니다.bind(eth0)호출 이후에는unregister_prot_hook()으로 전역 목록에서 제거하고,prot_hook.dev = eth0_dev를 설정한 뒤eth0->ptype_all에 다시 등록합니다. - 30행
net->packet.sklist는 네트워크 네임스페이스별 AF_PACKET 소켓 목록입니다. 네임스페이스 A에서 생성한 소켓은 네임스페이스 B의 인터페이스를 캡처할 수 없는 격리의 근거입니다.
packet_sock 주요 구조
/* net/packet/internal.h — 주요 필드만 발췌 */
struct packet_sock {
/* ★ 반드시 첫 번째 필드 — pkt_sk() 캐스트 안전성 보장 */
struct sock sk;
/* TPACKET ring buffer */
struct packet_ring_buffer rx_ring;
struct packet_ring_buffer tx_ring;
/* 수신 통계: tp_packets(캡처 성공), tp_drops(버퍼 부족 드롭) */
union tpacket_stats_u stats;
/* ptype_all / dev->ptype_all 등록 핵심 구조체 */
struct packet_type prot_hook;
/* 바인딩 정보 */
int ifindex; /* -1: 전체 인터페이스 (bind 전) */
__be16 num; /* ETH_P_ALL = htons(0x0003) */
/* 보조 데이터 플래그 */
unsigned int auxdata:1, /* VLAN 정보 포함 여부 */
origdev:1, /* bonding 원본 디바이스 기록 */
tp_loss:1; /* ring buffer 드롭 시 TP_STATUS_LOSING 설정 */
/* RSS 기반 멀티스레드 캡처 지원 (PACKET_FANOUT) */
struct packet_fanout *fanout;
spinlock_t bind_lock;
struct mutex pg_vec_lock; /* ring buffer 생성/해제 직렬화 */
};
ptype_all 등록 구조
packet_type 구조체가 ptype_all 리스트에 등록되는 방식을 이해하면 여러 tcpdump 인스턴스가 동시에 실행될 때의 동작을 파악할 수 있습니다.
/* include/linux/netdevice.h */
struct packet_type {
__be16 type; /* ETH_P_ALL, ETH_P_IP 등 */
bool ignore_outgoing; /* TX 패킷 무시 여부 */
struct net_device *dev; /* NULL: 전역, 특정 dev: 인터페이스 한정 */
netdevice_tracker dev_tracker;
int (*func)( /* 패킷 수신 콜백: packet_rcv / tpacket_rcv */
struct sk_buff *,
struct net_device *,
struct packet_type *,
struct net_device *);
void *af_packet_priv; /* sock 포인터 */
struct list_head list; /* ptype_all 또는 dev->ptype_all 연결 */
};
/* net/core/dev.c */
static inline void __register_prot_hook(struct sock *sk)
{
struct packet_sock *po = pkt_sk(sk);
if (!po->running) {
if (po->prot_hook.dev)
dev_add_pack(&po->prot_hook); /* dev->ptype_all */
else
dev_add_pack(&po->prot_hook); /* 전역 ptype_all */
po->running = 1;
}
}
setsockopt(fd, SOL_PACKET, PACKET_FANOUT, ...)를 사용하면 여러 AF_PACKET 소켓이 같은 fanout 그룹을 공유하여 패킷을 분산 처리할 수 있습니다. Suricata, Snort 등 고성능 IDS에서 CPU 코어별로 소켓을 만들고 PACKET_FANOUT_CPU 모드로 RSS처럼 패킷을 코어별로 분산합니다.
skb_clone과 패킷 복제 메커니즘
AF_PACKET 소켓이 패킷을 캡처할 때 커널은 원본 sk_buff를 직접 전달하지 않고 복제본(Clone)을 만듭니다. 복제 방식에 따라 성능과 안전성이 달라집니다.
skb_clone(): 헤더 구조만 복제하고 데이터 버퍼는 참조 카운트를 증가시켜 공유합니다.
| 함수 | 헤더 | 데이터 | 용도 | 오버헤드 |
|---|---|---|---|---|
skb_clone() | 새 할당 | refcount 증가 (공유) | 읽기 전용(Read-Only) 복사 — tcpdump, AF_PACKET | 낮음 (데이터 복사 없음) |
skb_copy() | 새 할당 | 완전한 독립 사본 | 헤더·데이터 모두 수정이 필요한 경우 | 높음 (전체 메모리 복사) |
pskb_copy() | 새 할당 | 선형 부분만 복사 | 헤더 수정 + paged frags 공유 | 중간 |
skb_copy_expand() | 여유 공간 포함 새 할당 | 전체 복사 | 헤더/테일에 추가 공간 필요 시 | 높음 |
/* include/linux/skbuff.h — __skb_clone() 핵심 로직 */
static struct sk_buff *__skb_clone(struct sk_buff *n,
struct sk_buff *skb)
{
/* sk_buff 헤더 구조를 새 할당 공간에 복사 */
memcpy(n, skb, offsetof(struct sk_buff, tail));
n->cloned = 1;
n->nohdr = 0;
skb->cloned = 1; /* 원본도 클론 보유 중임을 표시 */
/* 데이터 버퍼는 복사하지 않고 참조 카운트만 증가 */
atomic_inc(&skb_shinfo(skb)->dataref);
/* 소켓 수신 버퍼 회계 (sk_rmem_alloc) */
skb_set_owner_r(n, skb->sk);
return n;
}
skb_clone()은 SoftIRQ 컨텍스트에서 GFP_ATOMIC으로 메모리를 할당합니다. 메모리 압박(Memory Pressure) 상황에서 할당이 실패하면 패킷이 조용히 드롭됩니다. tcpdump -v 종료 시 출력되는 "N packets dropped by kernel" 수치 또는 /proc/net/packet의 drops 열로 확인하세요. -B 131072로 커널 버퍼를 128MB로 늘리면 드롭을 줄일 수 있습니다.
BPF 필터 컴파일 경로
tcpdump에서 필터 표현식(tcp port 80 등)을 지정하면, libpcap이 이를 cBPF(Classic BPF) 바이트코드로 컴파일하여 커널에 설치합니다. 커널은 JIT 컴파일러를 통해 이를 네이티브 코드로 변환합니다.
tcpdump에서 -d 옵션으로 컴파일된 cBPF 바이트코드를 확인할 수 있습니다.
# "tcp port 80" 필터의 cBPF 바이트코드 덤프
$ tcpdump -d 'tcp port 80'
(000) ldh [12] # EtherType 로드
(001) jeq #0x86dd jt 2 jf 8 # IPv6?
(002) ldb [20] # IPv6 Next Header
(003) jeq #0x6 jt 4 jf 19 # TCP?
(004) ldh [54] # TCP src port
(005) jeq #0x50 jt 18 jf 6 # port 80?
(006) ldh [56] # TCP dst port
(007) jeq #0x50 jt 18 jf 19 # port 80?
(008) jeq #0x800 jt 9 jf 19 # IPv4?
(009) ldb [23] # IP Protocol
(010) jeq #0x6 jt 11 jf 19 # TCP?
...
(018) ret #65535 # 캡처 (전체 패킷)
(019) ret #0 # 폐기
설치된 BPF 필터는 커널이 각 패킷마다 실행하여 통과 여부를 결정합니다. 필터를 통과하지 못한 패킷은 사용자 공간으로 복사되지 않아 오버헤드를 최소화합니다.
SO_ATTACH_FILTER로 설치된 cBPF를 내부적으로 eBPF로 변환(bpf_migrate_filter())한 후 JIT 컴파일합니다. x86-64, ARM64, PowerPC, RISC-V 등 주요 64비트 아키텍처에서 JIT가 지원됩니다.Ubuntu, Fedora, Debian 등 현대 배포판은 대부분
CONFIG_BPF_JIT_ALWAYS_ON=y로 빌드되어 인터프리터 폴백(Fallback) 없이 JIT만 동작합니다. net.core.bpf_jit_enable sysctl이 고정값 1로 설정됩니다. 직접 eBPF 프로그램을 SO_ATTACH_BPF로 설치하면 BPF_PROG_TYPE_SOCKET_FILTER 타입을 사용하며, cBPF 변환 없이 더 복잡한 필터링 로직을 구현할 수 있습니다.
SO_ATTACH_BPF — eBPF 필터 직접 설치
libpcap의 cBPF 방식 대신 SO_ATTACH_BPF로 eBPF 프로그램을 직접 AF_PACKET 소켓에 부착할 수 있습니다. 임의의 상태 추적, 맵(Map) 기반 동적 필터, 복잡한 프로토콜 파싱이 가능합니다.
/* eBPF 필터를 AF_PACKET 소켓에 직접 부착하는 예시 */
/* 1. BPF_PROG_TYPE_SOCKET_FILTER 타입 eBPF 프로그램 로드 */
int prog_fd = bpf(BPF_PROG_LOAD, &attr, sizeof(attr));
/* attr.prog_type = BPF_PROG_TYPE_SOCKET_FILTER */
/* attr.insns = eBPF 명령어 배열 */
/* 반환값: 패킷 캡처 길이(바이트) or 0(드롭) */
/* 2. AF_PACKET 소켓에 eBPF 프로그램 부착 */
setsockopt(fd, SOL_SOCKET, SO_ATTACH_BPF, &prog_fd, sizeof(prog_fd));
/* SO_ATTACH_FILTER (cBPF)와 비교 */
/* SO_ATTACH_FILTER: libpcap이 컴파일한 cBPF struct sock_fprog */
/* SO_ATTACH_BPF: 사용자가 직접 로드한 eBPF 프로그램 fd */
/* 두 옵션은 상호 배타적 — 마지막으로 설치된 것이 유효 */
| 옵션 | 방식 | 사용처 | 복잡도 |
|---|---|---|---|
SO_ATTACH_FILTER | cBPF (struct sock_fprog) | tcpdump, libpcap | 낮음 — libpcap이 자동 처리 |
SO_ATTACH_BPF | eBPF (prog_fd) | 커스텀 고성능 필터 | 높음 — eBPF 프로그램 직접 작성 |
SO_LOCK_FILTER | 필터 잠금(Lock) | 보안 샌드박스(Sandbox) | — |
TPACKET ring buffer: 고성능 캡처
기본 AF_PACKET 소켓은 패킷마다 recvmsg() 시스템콜이 필요합니다. 고속 트래픽에서는 이 오버헤드가 병목(Bottleneck)이 됩니다. libpcap은 TPACKET(PACKET_MMAP)을 사용하여 커널과 사용자 공간 사이에 mmap된 공유 ring buffer를 구성합니다.
| 버전 | 특징 | 기본 사용처 |
|---|---|---|
TPACKET_V1 | 프레임 단위 ring buffer, 32비트 타임스탬프 | 구형 libpcap |
TPACKET_V2 | 64비트 타임스탬프, VLAN 정보 포함 | 중간 세대 libpcap |
TPACKET_V3 | 블록 단위 ring buffer, 타임아웃 기반 플러시(Flush), 최고 성능 | 현재 libpcap 기본값 |
TPACKET_V3에서는 개별 프레임이 아닌 블록(block) 단위로 사용자 공간에 전달됩니다. 블록이 가득 차거나 tp_retire_blk_tov 타임아웃이 만료되면 해당 블록의 상태가 TP_STATUS_KERNEL에서 TP_STATUS_USER로 변경되어 libpcap이 읽어갈 수 있게 됩니다.
tcpdump 실전 사용법
주요 옵션
# 기본 캡처 — eth0 인터페이스, 자세한 출력
sudo tcpdump -i eth0 -v
# 패킷을 파일로 저장 (pcap 형식, Wireshark에서 열기 가능)
sudo tcpdump -i eth0 -w capture.pcap
# 저장된 pcap 파일 읽기
tcpdump -r capture.pcap
# 호스트명 리졸빙 비활성화 (-n), 포트 번호 표시 (-nn) — 속도 향상
sudo tcpdump -i eth0 -nn
# 패킷 페이로드 16진수 + ASCII 출력
sudo tcpdump -i eth0 -XX
# 캡처 패킷 수 제한
sudo tcpdump -i eth0 -c 100
# 스냅 길이 지정 (기본 262144바이트, 0=전체)
sudo tcpdump -i eth0 -s 96 # 헤더만 캡처
sudo tcpdump -i eth0 -s 0 # 전체 패킷
# 모든 인터페이스 (loopback 포함)
sudo tcpdump -i any
필터 표현식
## 호스트/포트 필터
sudo tcpdump -i eth0 host 192.168.1.100
sudo tcpdump -i eth0 src host 192.168.1.100
sudo tcpdump -i eth0 dst port 443
sudo tcpdump -i eth0 port 80 or port 443
## 프로토콜 필터
sudo tcpdump -i eth0 tcp
sudo tcpdump -i eth0 udp
sudo tcpdump -i eth0 icmp
sudo tcpdump -i eth0 arp
## 네트워크/서브넷 필터
sudo tcpdump -i eth0 net 192.168.0.0/24
sudo tcpdump -i eth0 src net 10.0.0.0/8
## TCP 플래그 필터 (SYN 패킷만)
sudo tcpdump -i eth0 'tcp[tcpflags] & tcp-syn != 0'
## TCP 플래그 필터 (SYN-ACK)
sudo tcpdump -i eth0 'tcp[tcpflags] & (tcp-syn|tcp-ack) == (tcp-syn|tcp-ack)'
## 특정 패킷 크기 필터 (점보 프레임 감지)
sudo tcpdump -i eth0 'len > 1500'
## VLAN 태그 패킷
sudo tcpdump -i eth0 vlan 100
## 복합 필터 — 특정 호스트의 SSH 제외
sudo tcpdump -i eth0 host 192.168.1.100 and not port 22
tcpdump가 볼 수 없는 패킷
캡처 포인트의 위치 때문에 tcpdump로 관찰할 수 없는 패킷이 존재합니다. 이를 모르면 "패킷이 없다"고 잘못 진단할 수 있습니다.
| # | 볼 수 없는 경우 | 원인 | 대안 디버깅 방법 |
|---|---|---|---|
| ① | NIC 하드웨어 필터로 드롭된 패킷 | ethtool -U ntuple 필터, offload 규칙 | ethtool -S 통계, ethtool --show-ntuple |
| ② | XDP 프로그램이 DROP/TX한 패킷 | XDP_DROP, XDP_TX: sk_buff 할당 전 처리 | bpftrace, XDP 프로그램에 bpf_trace_printk 추가 |
| ③ | GRO 병합 전 개별 패킷 | GRO가 여러 패킷을 하나의 큰 skb로 합침 | ethtool -K eth0 gro off로 GRO 비활성화 |
| ④ | iptables/nftables PREROUTING DROP (TX 방향) | TX 캡처 포인트는 Netfilter 이후 | TX는 tcpdump에 보임; RX DROP 패킷은 tcpdump에 보임 |
| ⑤ | GSO 분할 전 큰 패킷 (TX) | TSO/GSO가 드라이버에서 분할 — 캡처 포인트는 분할 전 | ethtool -K eth0 tso off gso off로 비활성화 |
| ⑥ | 커널 내부 소켓 통신 | loopback은 캡처되지만 127.0.0.1 외 소켓은 다를 수 있음 | -i lo 또는 -i any |
RX 방향에서 iptables가 패킷을 DROP하더라도 tcpdump에는 패킷이 보입니다. tcpdump 캡처 포인트(
__netif_receive_skb_core)가 Netfilter PREROUTING 훅보다 먼저 실행되기 때문입니다. "tcpdump에 보이는데 애플리케이션에 도달하지 않는다"면 netfilter 규칙을 먼저 점검하세요.
xdpdump — XDP 수준 패킷 캡처
XDP 프로그램이 XDP_DROP으로 폐기한 패킷은 sk_buff(소켓 버퍼) 할당 전에 처리되므로 tcpdump는 이를 볼 수 없습니다. xdpdump는 xdp-tools 프로젝트에서 제공하는 전용 도구로, eBPF 트레이싱(Tracing) 프로그램을 XDP 훅의 진입(@entry)·종료(@exit) 두 지점에 부착하여 드롭된 패킷까지 캡처합니다.
# xdpdump 설치 (xdp-tools 패키지)
apt install xdp-tools # Debian/Ubuntu
dnf install xdp-tools # Fedora/RHEL
# eth0의 XDP 진입·종료 시점 모두 캡처 (pcap 파일로 저장)
sudo xdpdump -i eth0 -w xdp-capture.pcap
# XDP_DROP된 패킷만 필터 (--rx-capture exit 옵션)
sudo xdpdump -i eth0 --rx-capture=exit -w dropped.pcap
# 실시간 출력 (tcpdump 형식)
sudo xdpdump -i eth0 -x
xdpdump는 내부적으로 BPF_PROG_TYPE_TRACING 타입의 eBPF 프로그램을 fexit/fentry 훅으로 기존 XDP 프로그램에 부착합니다. 원본 XDP 프로그램을 수정하지 않고도 패킷 내용과 XDP 판정 결과(XDP_PASS/XDP_DROP/…)를 동시에 기록할 수 있습니다.
| 캡처 시점 | xdpdump 옵션 | 볼 수 있는 패킷 |
|---|---|---|
@entry (기본) | --rx-capture=entry | XDP 프로그램 실행 전 원본 패킷 |
@exit | --rx-capture=exit | XDP 판정 후 — DROP 포함 결과 확인 |
| 양쪽 모두 | --rx-capture=entry,exit | 각 패킷에 두 개의 캡처 레코드 생성 |
XDP_DROP된 패킷에는 pcapng 옵션 블록에 xdp-action=XDP_DROP 주석이 기록됩니다.
성능과 오버헤드
tcpdump는 패킷마다 skb_clone()을 호출합니다. 고속 트래픽에서는 이 복사 오버헤드가 상당할 수 있습니다.
오버헤드 요인
| 요인 | 설명 | 줄이는 방법 |
|---|---|---|
| 패킷 복사 | 모든 패킷에 skb_clone() 호출 | BPF 필터로 불필요한 패킷 사전 제거 |
| 시스템콜 | 기본 모드에서 패킷마다 recvmsg() | TPACKET_V3 ring buffer 사용 (libpcap 자동 적용) |
| 사용자 공간 복사 | 커널 → 사용자 공간 메모리 복사 | TPACKET mmap으로 복사 제거 |
| 소프트웨어 인터럽트(Interrupt) | 캡처 중 softirq 경합(Contention) 증가 | 캡처 인터페이스 RSS 설정, CPU 고정 |
| 스냅 길이 | 전체 패킷 캡처 시 메모리 증가 | -s 96으로 헤더만 캡처 |
# 패킷 드롭 통계 확인 (커널 버퍼 오버플로우)
tcpdump -i eth0 -v 2>&1 | grep "packets dropped"
# 더 많은 커널 버퍼 할당 (기본 2MB → 128MB)
sudo tcpdump -i eth0 -B 131072
# AF_PACKET 소켓 통계 확인
cat /proc/net/packet
# 인터페이스별 RX 드롭 통계
ip -s link show eth0
bpf-next의 xdpdump)나 suricata처럼 TPACKET_V3를 직접 최적화한 도구를 고려하세요. XDP 수준에서 캡처하면 sk_buff 할당 오버헤드 없이 와이어 스피드 패킷 처리가 가능합니다.
디버깅(Debugging) 활용 예제
TCP 연결 문제 디버깅
# TCP 3-way handshake 확인 (SYN, SYN-ACK, ACK)
sudo tcpdump -i eth0 -nn 'host 192.168.1.100 and tcp port 80' \
-v -c 20
# TCP RST 패킷 감지 (연결 거부/강제 종료)
sudo tcpdump -i eth0 'tcp[tcpflags] & tcp-rst != 0'
# 재전송 패킷 감지 (중복 ACK 또는 SYN)
sudo tcpdump -i eth0 'tcp[tcpflags] & tcp-syn != 0 and \
tcp[tcpflags] & tcp-ack = 0' -nn
NAT/방화벽(Firewall) 검증
# NAT 변환 전후 확인: 두 터미널에서 동시에 실행
# 터미널 1: 외부 인터페이스 (변환 후 IP)
sudo tcpdump -i eth0 -nn host 203.0.113.10
# 터미널 2: 내부 인터페이스 (원래 IP)
sudo tcpdump -i eth1 -nn host 192.168.1.100
# 또는 -i any로 양쪽 동시 관찰
sudo tcpdump -i any -nn 'host 203.0.113.10 or host 192.168.1.100'
DNS 문제 디버깅
# DNS 쿼리/응답 캡처 (UDP 53)
sudo tcpdump -i eth0 -nn 'udp port 53' -v
# DNS over TCP (큰 응답, zone transfer)
sudo tcpdump -i eth0 -nn 'tcp port 53'
# 특정 도메인 쿼리만 (페이로드 문자열 매칭)
sudo tcpdump -i eth0 -nn -A 'udp port 53' | grep example.com
캡처 파일 저장 및 분석
# 파일 저장 — 로테이션 (파일 크기 100MB, 최대 10개 유지)
sudo tcpdump -i eth0 -w /tmp/cap-%Y%m%d-%H%M%S.pcap \
-C 100 -W 10
# 저장된 pcap에서 필터 적용하여 읽기
tcpdump -r capture.pcap -nn 'tcp port 443'
# pcap 파일을 PCAP 분석기로 열기 (브라우저 기반 도구)
# → tools/pcap-analyzer.html 참고
커널 내부: packet_rcv와 tpacket_rcv
AF_PACKET 소켓이 ptype_all에 등록할 때 제공하는 콜백(Callback) 함수는 두 가지입니다.
| 함수 | 사용 조건 | 동작 |
|---|---|---|
packet_rcv() | TPACKET 미사용 (기본 소켓) | skb_clone() 후 소켓 수신 큐에 추가. 사용자가 recvmsg()로 읽음. |
tpacket_rcv() | TPACKET ring buffer 설정 후 | mmap 공유 ring buffer에 직접 복사. 시스템콜 없이 사용자가 폴링(Polling). |
/* net/packet/af_packet.c */
static int packet_rcv(struct sk_buff *skb, struct net_device *dev,
struct packet_type *pt, struct net_device *orig_dev)
{
struct sock *sk = pt->af_packet_priv;
/* BPF 필터 실행 — 통과하지 못하면 즉시 드롭 */
res = run_filter(skb, sk, snaplen);
if (!res)
goto drop_n_restore;
/* 패킷 복제 후 소켓 큐에 추가 */
copy = skb_clone(skb, GFP_ATOMIC);
skb_set_owner_r(copy, sk);
__skb_queue_tail(&sk->sk_receive_queue, copy);
sk->sk_data_ready(sk); /* poll() / epoll() wakeup */
return 0;
}
코드 설명
- 7-9행
run_filter()가 설치된 cBPF/eBPF 필터를 실행합니다. 반환값이 0이면 패킷을 폐기합니다. 필터가 없으면 모든 패킷을 통과시킵니다. - 12행
skb_clone()은 sk_buff 헤더 구조만 복제하고 실제 데이터 버퍼는 참조 카운트(Reference Count)를 증가시켜 공유합니다. 완전한 메모리 복사가 아니므로 오버헤드가 낮습니다. - 15행
sk_data_ready()는 소켓에 데이터가 도착했음을 wakeup 메커니즘을 통해 알립니다. libpcap의poll()대기가 깨어납니다.
TPACKET_V3 자료구조 상세
TPACKET_V3 ring buffer의 메모리 레이아웃은 블록(Block)과 프레임(Frame) 두 계층으로 구성됩니다. 커널은 블록 단위로 패킷을 채우고, 블록이 가득 차거나 tp_retire_blk_tov 타임아웃이 만료되면 사용자 공간에 전달합니다.
TPACKET_V3 ring buffer: 녹색 = 사용자 읽기 가능(TP_STATUS_USER), 파랑 = 커널 쓰기 중(TP_STATUS_KERNEL)
/* include/uapi/linux/if_packet.h — TPACKET_V3 핵심 자료구조 */
/* 블록 헤더: 각 블록의 시작에 배치 (4KB 정렬) */
struct tpacket_block_desc {
__u32 version; /* TPACKET_V3 = 3 */
__u32 offset_to_priv; /* 사용자 개인 데이터 오프셋 */
union {
struct tpacket_hdr_v1 {
__u32 block_status; /* TP_STATUS_KERNEL=0 / TP_STATUS_USER=1 */
__u32 num_pkts; /* 이 블록에 담긴 패킷 수 */
__u32 offset_to_first_pkt; /* 첫 tpacket3_hdr 오프셋 (바이트) */
__u32 blk_len; /* 블록 내 사용된 바이트 수 */
__aligned_u64 seq_num; /* 단조 증가 블록 시퀀스 번호 */
struct tpacket_bd_ts ts_first_pkt; /* 첫 패킷 타임스탬프 */
struct tpacket_bd_ts ts_last_pkt; /* 마지막 패킷 타임스탬프 */
} bh1;
} hdr;
} __attribute__((packed));
/* 프레임 헤더: 각 캡처 패킷 바로 앞에 배치 */
struct tpacket3_hdr {
__u32 tp_next_offset; /* 다음 프레임까지 바이트 오프셋 (0=마지막) */
__u32 tp_sec; /* 타임스탬프 초 (Unix epoch) */
__u32 tp_nsec; /* 타임스탬프 나노초 */
__u32 tp_snaplen; /* 실제 캡처 바이트 수 (≤ tp_len) */
__u32 tp_len; /* 원본 패킷 바이트 수 */
__u32 tp_status; /* TP_STATUS_USER | TP_STATUS_VLAN_VALID 등 */
__u16 tp_mac; /* MAC 헤더 시작 (tpacket3_hdr 기준 오프셋) */
__u16 tp_net; /* 네트워크 헤더 시작 오프셋 */
union {
struct tpacket_hdr_variant1 hv1; /* VLAN TCI/TPID 보조 데이터 */
};
__u8 tp_padding[8];
};
TP_STATUS 플래그와 상태 전환
| 플래그 | 값 | 의미 | 다음 전환 |
|---|---|---|---|
TP_STATUS_KERNEL | 0x0 | 커널 소유 — 사용자 읽기 금지 | 블록 가득 참 또는 타임아웃 → TP_STATUS_USER |
TP_STATUS_USER | 0x1 | 사용자 공간 소유 — 읽기 안전 | libpcap 처리 완료 → TP_STATUS_KERNEL (블록 반환) |
TP_STATUS_COPY | 0x2 | 패킷이 snaplen보다 길어 잘림 | — |
TP_STATUS_LOSING | 0x4 | 이 시점에서 패킷 드롭 발생 | — |
TP_STATUS_VLAN_VALID | 0x10 | VLAN TCI(hv1.tp_vlan_tci) 필드 유효 | — |
TP_STATUS_VLAN_TPID_VALID | 0x40 | VLAN TPID 필드 유효 (QinQ 지원) | — |
libpcap이 TPACKET_V3 ring buffer에서 패킷을 읽는 핵심 루프입니다.
/* libpcap 내부 — TPACKET_V3 ring buffer 읽기 루프 (단순화) */
while (1) {
struct tpacket_block_desc *pbd =
(struct tpacket_block_desc *)(ring->rd[ring->frame_num].iov_base);
/* 블록이 준비될 때까지 poll() 대기 */
while ((pbd->hdr.bh1.block_status & TP_STATUS_USER) == 0)
poll(&pfd, 1, timeout_ms);
/* 블록 내 첫 번째 tpacket3_hdr 위치 계산 */
struct tpacket3_hdr *ppd =
(struct tpacket3_hdr *)((uint8_t *)pbd +
pbd->hdr.bh1.offset_to_first_pkt);
/* 블록 내 모든 패킷 처리 */
for (uint32_t i = 0; i < pbd->hdr.bh1.num_pkts; i++) {
uint8_t *pkt_data = (uint8_t *)ppd + ppd->tp_mac;
pcap_callback(pkt_data, ppd->tp_snaplen, ppd->tp_len,
ppd->tp_sec, ppd->tp_nsec);
ppd = (struct tpacket3_hdr *)((uint8_t *)ppd + ppd->tp_next_offset);
}
/* 처리 완료 → 커널에 블록 반환 (원자적 쓰기) */
pbd->hdr.bh1.block_status = TP_STATUS_KERNEL;
__sync_synchronize(); /* 메모리 배리어(Memory Barrier) */
ring->frame_num = (ring->frame_num + 1) % ring->req3.tp_block_nr;
}
tpacket_req3.tp_retire_blk_tov 필드(밀리초 단위)로 블록 타임아웃을 설정합니다. 0이면 블록이 가득 찰 때만 전달됩니다. 저속 트래픽 환경에서 높은 지연(Latency)이 발생할 수 있으므로 보통 60ms로 설정합니다.
TX 경로 패킷 캡처 구현 분석
TX 방향의 패킷 캡처는 dev_queue_xmit_nit() 함수에서 이루어집니다. 이 함수는 Qdisc(Traffic Control Queue Discipline)를 통과한 패킷이 드라이버의 ndo_start_xmit()로 넘어가기 직전에 호출됩니다.
/* net/core/dev.c — dev_queue_xmit_nit() 구현 */
void dev_queue_xmit_nit(struct sk_buff *skb, struct net_device *dev)
{
struct packet_type *ptype;
struct net_device *orig_dev = skb->dev;
struct packet_type *pt_prev = NULL;
/* TX 방향임을 skb에 표시: tcpdump 출력에서 방향 구분에 사용 */
skb->pkt_type = PACKET_OUTGOING;
rcu_read_lock();
/* 전역 ptype_all 순회 (tcpdump -i any 또는 모든 인터페이스 대상) */
list_for_each_entry_rcu(ptype, &ptype_all, list) {
if (!ptype->dev || ptype->dev == skb->dev) {
if (pt_prev) {
/* N-1번은 skb_clone 후 deliver_skb */
struct sk_buff *skb2 = skb_clone(skb, GFP_ATOMIC);
deliver_skb(skb2, pt_prev, orig_dev);
}
pt_prev = ptype;
}
}
/* 인터페이스 전용 ptype_all (tcpdump -i eth0) */
list_for_each_entry_rcu(ptype, &skb->dev->ptype_all, list) {
if (pt_prev) {
struct sk_buff *skb2 = skb_clone(skb, GFP_ATOMIC);
deliver_skb(skb2, pt_prev, orig_dev);
}
pt_prev = ptype;
}
/* 마지막 소켓: 클론 없이 원본 전달로 복사 1회 절약 */
if (pt_prev)
deliver_skb(skb, pt_prev, orig_dev);
rcu_read_unlock();
}
코드 설명
- 9행
PACKET_OUTGOING: 송신 방향 표시입니다. tcpdump 출력에서 방향(화살표 또는 In/Out 표기)을 결정합니다. RX 패킷은PACKET_HOST(자신 목적지) 또는PACKET_BROADCAST등입니다. - 14-23행같은 인터페이스를 캡처 중인 소켓이 N개이면 N-1번
skb_clone()이 발생합니다. pt_prev 패턴을 사용하여 마지막 소켓은 원본을 직접 전달받아 불필요한 복제를 피합니다. - 25-32행인터페이스 전용
dev->ptype_all은 전역ptype_all이후에 처리됩니다. 같은 패킷에 대해 두 리스트 소켓이 모두 캡처에 참여할 수 있습니다.
GSO/TSO와 TX 캡처의 상호작용
TX 경로에서 TSO(TCP Segmentation Offload)/GSO(Generic Segmentation Offload)가 활성화된 경우 tcpdump가 보는 패킷 크기가 실제 전송 크기와 다를 수 있습니다.
| 상황 | tcpdump 캡처 포인트 | tcpdump가 보는 패킷 | 실제 전송 |
|---|---|---|---|
| GSO 활성화 (기본값) | Qdisc 이후, 드라이버 이전 | 큰 GSO 패킷 (예: 65KB TCP) | 드라이버/HW가 1460B 단위로 분할 |
| GSO 비활성화 | 동일 | 실제 세그먼트 크기 (예: 1460B) | 동일 |
| VXLAN TX | 내부 패킷 캡처 포인트 | 내부 패킷만 (캡슐화 전) | 외부 VXLAN 헤더 포함 전송 |
| XDP TX (XDP_TX) | 캡처 불가 | 볼 수 없음 | 드라이버에서 직접 재전송(Retransmission) |
ethtool -K eth0 gso off tso off로 오프로드를 비활성화하거나, 물리 스위치에서 패킷을 캡처하세요.
고급 BPF 필터 기법
tcpdump의 표준 필터 문법 외에 원시 오프셋 접근(Raw Offset Access)을 이용하면 임의의 프로토콜 필드를 필터 조건으로 사용할 수 있습니다. proto[offset:size] 형식으로 프로토콜의 특정 바이트에 접근합니다.
원시 오프셋(Offset) 기반 필터
# TCP 플래그 바이트: CWR|ECE|URG|ACK|PSH|RST|SYN|FIN = 비트(Bit) 7~0
# SYN=0x02, ACK=0x10, RST=0x04, FIN=0x01, PSH=0x08
# TCP RST 패킷 (RST 플래그만, tcp[13] = 오프셋 13번 바이트)
sudo tcpdump 'tcp[13] & 0x04 != 0'
# 연결 시작 (SYN only, SYN-ACK 제외)
sudo tcpdump 'tcp[tcpflags] == tcp-syn'
# 연결 수립 (SYN-ACK)
sudo tcpdump 'tcp[tcpflags] & (tcp-syn|tcp-ack) == (tcp-syn|tcp-ack)'
# TCP 창 크기 0 (수신 버퍼 가득 참 — 흐름 제어 확인)
sudo tcpdump 'tcp[14:2] == 0'
# IPv4 TTL이 1인 패킷 (traceroute 감지)
sudo tcpdump 'ip[8] == 1'
# IPv4 ToS(DSCP) 필드 — EF(Expedited Forwarding, 음성/영상 트래픽)
sudo tcpdump 'ip[1] & 0xfc == 0xb8'
# IPv4 분열(Fragment) 패킷 감지 (MF 플래그 또는 오프셋 > 0)
sudo tcpdump '(ip[6:2] & 0x2000 != 0) or (ip[6:2] & 0x1fff != 0)'
# ICMP 타입 3 (Destination Unreachable)
sudo tcpdump 'icmp[0] == 3'
# ICMP Redirect (타입 5) 감지
sudo tcpdump 'icmp[icmptype] == icmp-redirect'
# VLAN 태그 패킷 (EtherType 0x8100)
sudo tcpdump 'ether[12:2] == 0x8100'
# QinQ (이중 VLAN, 외부 0x88a8 + 내부 0x8100)
sudo tcpdump 'ether[12:2] == 0x88a8'
페이로드(Payload) 내용 매칭
# TCP 데이터 오프셋 계산: tcp[12] >> 4 * 4 = TCP 헤더 길이
# HTTP GET 요청 감지 (TCP 페이로드 첫 4바이트 = "GET " = 0x47455420)
sudo tcpdump 'tcp[((tcp[12:1] & 0xf0) >> 2):4] = 0x47455420'
# HTTP 응답 감지 (페이로드 첫 4바이트 = "HTTP" = 0x48545450)
sudo tcpdump 'tcp[((tcp[12:1] & 0xf0) >> 2):4] = 0x48545450'
# SSH 배너 (페이로드 첫 4바이트 = "SSH-" = 0x5353482d)
sudo tcpdump 'tcp dst port 22 and tcp[((tcp[12:1]&0xf0)>>2):4] = 0x5353482d'
# UDP DNS 질의 (QR=0 비트 확인)
sudo tcpdump 'udp port 53 and udp[10:2] & 0x8000 == 0'
# DNS 응답 중 NXDOMAIN (RCODE=3)
sudo tcpdump 'udp port 53 and udp[10:2] & 0x8003 == 0x8003'
# TLS ClientHello 감지 (레코드 타입 0x16 + 핸드셰이크 타입 0x01)
sudo tcpdump 'tcp[((tcp[12:1]&0xf0)>>2)] = 0x16 and \
tcp[((tcp[12:1]&0xf0)>>2)+5] = 0x01'
cBPF 바이트코드 확인과 최적화
# 컴파일된 cBPF 바이트코드 출력 (-d: 어셈블리, -dd: C 배열, -ddd: 십진수)
tcpdump -d 'tcp and port 443'
# eBPF 변환 후 JIT 코드 확인 (루트 권한 필요)
# 1. SO_ATTACH_FILTER로 설치 후:
cat /proc/sys/net/core/bpf_jit_enable # 1이면 JIT 활성화
cat /proc/sys/net/core/bpf_jit_harden # 0: 비하드닝, 2: 강화
# 2. JIT 컴파일된 코드 확인 (CONFIG_HAVE_EBPF_JIT=y 필요)
# bpftool prog dump jited id <id> 명령으로 확인
# 필터 성능 비교: 복잡한 필터는 -d로 명령어 수를 먼저 확인
tcpdump -d 'host 192.168.1.1 and tcp port 80'
tcpdump -d 'tcp port 80 and host 192.168.1.1'
# 패킷 수가 많은 조건을 먼저 두면 더 빠른 거부(early reject)가 가능합니다.
- 구체적 조건 우선: cBPF는 단락 평가(Short-circuit Evaluation)를 하므로, 가장 많은 패킷을 조기에 거부하는 조건을 앞에 두세요.
- 스냅 길이 최소화:
-s 96으로 헤더만 캡처하면skb_clone()후 사용자 공간 복사량이 줄어듭니다. -nn옵션 필수: DNS 리졸빙을 비활성화하면 각 패킷마다 발생하는 추가 syscall을 제거합니다.- 고속 환경: 10Gbps 이상에서는 필터를 반드시 사용하세요. 필터 없는 캡처는 모든 패킷을 사용자 공간으로 복사하여 시스템 전체에 심각한 영향을 줍니다.
PCAP 파일 형식
tcpdump의 -w 옵션으로 저장되는 파일은 PCAP 형식입니다. 단순한 고정 헤더 구조로 이루어져 있어 직접 파싱하거나 생성하기 쉽습니다.
/* pcap.h — PCAP 파일 자료구조 */
struct pcap_file_header {
uint32_t magic_number; /* 0xa1b2c3d4: us / 0xa1b23c4d: ns / 빅엔디안 감지 */
uint16_t version_major; /* 2 */
uint16_t version_minor; /* 4 */
int32_t thiszone; /* GMT 오프셋 (항상 0) */
uint32_t sigfigs; /* 타임스탬프 정밀도 (항상 0) */
uint32_t snaplen; /* 최대 캡처 길이 */
uint32_t linktype; /* DLT_EN10MB=1, DLT_LINUX_SLL=113 등 */
};
struct pcap_pkthdr {
uint32_t ts_sec; /* 타임스탬프 초 */
uint32_t ts_usec; /* 마이크로초 또는 나노초 (magic에 따라) */
uint32_t incl_len; /* 저장된 바이트 수 */
uint32_t orig_len; /* 원본 패킷 길이 */
};
/* 이후 incl_len 바이트의 패킷 데이터 (패킷마다 반복) */
| 형식 | magic_number | 타임스탬프 정밀도 | 특징 |
|---|---|---|---|
| PCAP (microsecond) | 0xa1b2c3d4 | 마이크로초 | 가장 널리 사용되는 전통적 형식 |
| PCAP (nanosecond) | 0xa1b23c4d | 나노초 | 고정밀 타임스탬프 — Wireshark 1.12+ |
| pcapng | 0x0a0d0d0a (SHB) | 나노초/마이크로초 | 다중 인터페이스, 코멘트, 메타데이터 지원 |
editcap -F pcapng in.pcap out.pcapng로 변환합니다. 단, 일부 오래된 분석 도구는 pcapng를 지원하지 않으므로 호환성이 필요하면 pcap 형식을 사용하세요.
네트워크 네임스페이스와 패킷 캡처
리눅스 컨테이너(Container, Docker, Kubernetes 파드 등)는 네트워크 네임스페이스(Network Namespace)로 격리(Isolation)됩니다. AF_PACKET 소켓은 net->packet.sklist에 네임스페이스별로 등록되므로, 호스트에서 실행한 tcpdump는 컨테이너 내부 인터페이스를 직접 캡처할 수 없습니다.
# 방법 1: nsenter — 컨테이너 네트워크 네임스페이스에 진입
# 컨테이너 PID 확인
docker inspect <container_id> --format='{{.State.Pid}}'
# 해당 네임스페이스에서 tcpdump 실행 (-n: 네트워크 네임스페이스)
sudo nsenter -t <PID> -n -- tcpdump -i eth0 -nn
# 방법 2: 호스트에서 veth 인터페이스 캡처 (컨테이너 트래픽 미러)
# 컨테이너 내 eth0의 peer index 확인
nsenter -t <PID> -n -- ip link show eth0
# 출력: 10: eth0@if11 → 호스트의 index=11인 veth 찾기
ip link | grep '^11:'
sudo tcpdump -i vethXXXX -nn
# 방법 3: ip netns exec (네임드 네임스페이스)
sudo ip netns exec <namespace_name> tcpdump -i eth0
# 방법 4: Kubernetes 파드 — kubectl debug 또는 ephemeral container
kubectl debug -it <pod_name> --image=nicolaka/netshoot \
--target=<container_name> -- tcpdump -i eth0
# 방법 5: Kubernetes — 파드 내 직접 실행 (tcpdump 설치된 경우)
kubectl exec -it <pod_name> -- tcpdump -i eth0 -w /tmp/cap.pcap
kubectl cp <pod_name>:/tmp/cap.pcap ./local.pcap
- veth 쪽 캡처: 호스트의 veth 인터페이스에서 캡처하면 실제 Ethernet 헤더가 보입니다. Linux Cooked 헤더(SLL)가 붙지 않아 더 정확합니다.
- Cilium/eBPF CNI: Cilium은 eBPF TC 훅에서 패킷을 처리하므로, tcpdump 캡처 포인트보다 먼저 패킷이 드롭·변경될 수 있습니다.
cilium monitor또는hubble observe로 eBPF 레벨 흐름을 관찰하는 것이 더 정확합니다. - Calico iptables 모드: 표준 iptables 기반이므로 tcpdump가 정상 동작합니다. Calico VXLAN 모드에서는 VTEP 인터페이스(
vxlan.calico)도 캡처 대상이 됩니다.
터널링 및 오버레이(Overlay) 환경에서의 패킷 캡처
VXLAN, GRE, WireGuard, IPsec 등 터널(Tunnel)링 기술을 사용하는 환경에서는 패킷이 캡슐화(Encapsulation)되므로, 캡처 위치에 따라 보이는 내용이 크게 달라집니다.
| 기술 | 캡처 인터페이스 | 보이는 내용 | 용도 |
|---|---|---|---|
| VXLAN | 컨테이너 내부 eth0 / veth | 캡슐화 전 원본 패킷 (내부 IP/L4) | 애플리케이션 트래픽 분석 |
| 물리 NIC (eth0 호스트) | UDP/4789 캡슐화된 패킷 (외부 IP만) | 언더레이 네트워크 문제 분석 | |
| WireGuard | wg0 인터페이스 | 복호화된 내부 IP 패킷 | VPN 터널 내부 트래픽 분석 |
| 물리 NIC | 암호화(Encryption)된 WireGuard UDP 패킷 | VPN 연결 자체 디버깅 | |
| GRE | GRE 터널 인터페이스 (gre0) | GRE 역캡슐화(Decapsulation) 후 내부 패킷 | 터널 내 트래픽 분석 |
| 물리 NIC | GRE 캡슐화 패킷 (proto=47) | 터널링 자체 문제 분석 | |
| IPsec (ESP) | 물리 NIC | 암호화된 ESP 패킷 (proto=50) | IPsec SA 협상 문제 |
| xfrm 복호화(Decryption) 후 인터페이스 | 복호화된 내부 패킷 | 내부 트래픽 분석 |
# VXLAN 분석 — 물리 NIC에서 캡슐화 패킷 캡처
sudo tcpdump -i eth0 'udp port 4789' -nn -v
# VXLAN 내부 패킷의 내부 IP 출력 (Wireshark 스타일: -w 후 분석 권장)
sudo tcpdump -i eth0 'udp port 4789' -XX
# VXLAN 인터페이스에서 내부 패킷 직접 캡처
sudo tcpdump -i vxlan0 -nn
# WireGuard 복호화된 트래픽
sudo tcpdump -i wg0 -nn
# WireGuard 암호화된 패킷 (UDP 51820 기본 포트)
sudo tcpdump -i eth0 'udp port 51820' -nn
# GRE 터널 — 캡슐화된 패킷 (IP 프로토콜 47)
sudo tcpdump -i eth0 'proto 47' -nn -v
# GRE 터널 내부 인터페이스
sudo tcpdump -i gre0 -nn
# IPsec ESP 패킷 확인 (프로토콜 50)
sudo tcpdump -i eth0 'proto 50' -nn
# 특정 VNI(VXLAN Network Identifier)의 VXLAN 패킷 필터
# VNI는 VXLAN 헤더의 오프셋 4~6바이트에 위치
# UDP 페이로드 첫 4바이트 = 플래그(1B) + VNI MSB(3B), 이후 VNI(3B) + 예약(1B)
sudo tcpdump -i eth0 'udp port 4789 and udp[11:3] = 0x000064' # VNI=100
- "패킷이 전달되나?" 확인 → 물리 NIC (가장 바깥쪽)에서 캡처
- "내용이 올바른가?" 확인 → 애플리케이션에 가까운 내부 인터페이스에서 캡처
- 암호화된 구간 문제 → 터널 인터페이스(wg0, xfrm 등) 전·후 양쪽에서 캡처하여 비교
- NAT 변환 확인 → RX: Netfilter PREROUTING 이전(변환 전 IP), TX: POSTROUTING 이후(변환 후 IP)
- 드롭 원인 추적 →
bpftrace -e 'kprobe:kfree_skb { @[kstack] = count(); }'로 드롭 지점 스택 추적(Stack Trace)
참고자료
- tcpdump man page
- pcap-filter(7) — 필터 표현식 문법
- tcpdump & libpcap 공식 사이트 — libpcap 1.10.6 / tcpdump 4.99.6 (2025-12-30 릴리스): CVE 보안 수정, CAN XL 링크 타입 지원,
DLT_LINUX_SLL2ifindex 기반 필터링 개선, DPDK 캡처 지원 강화 - Linux Socket Filtering (BPF) — kernel.org
- PACKET_MMAP (TPACKET) — kernel.org
- xdp-tools (xdpdump 포함) — GitHub — XDP 수준 패킷 캡처 도구
- 네트워크 스택 개요 — RX/TX 전체 경로
- sk_buff — AF_PACKET, TPACKET 소켓 심화
- BPF / eBPF — cBPF와 eBPF 비교, JIT 컴파일
- XDP — 드라이버 수준 패킷 처리 (tcpdump보다 빠른 캡처)
- PCAP 분석기 도구 — 브라우저에서 pcap 파일 분석
- BPF / eBPF — tcpdump의 필터 기반인 BPF 심화 학습
- XDP (eXpress Data Path) — tcpdump가 볼 수 없는 드라이버 수준 패킷 처리
- 네트워크 패킷 흐름 & 디버깅 — tcpdump를 포함한 종합 네트워크 디버깅 기법