TUN/TAP 가상 네트워크 인터페이스
TUN/TAP는 userspace 애플리케이션이 가상 네트워크 인터페이스를 통해 패킷(Packet)을 직접 송수신할 수 있게 하는 커널 드라이버입니다.
VPN, 네트워크 에뮬레이션, 가상화(Virtualization)에서 핵심적으로 사용됩니다.
tun_struct/tun_file의 내부 필드 분석, XDP 프로그램 연동,
vhost-net 제로카피 전송 경로, 멀티큐 아키텍처와 스티어링 프로그램,
macvtap/macvlan과의 비교, virtio-net+TAP 통합, 오프로딩(Offloading) 제어,
패킷 전달 메커니즘의 커널 내부 함수 호출 체인, 성능 튜닝 파라미터, 디버깅(Debugging) 절차까지
운영 실무 관점으로 다룹니다.
핵심 요약
- TUN — IP 패킷(L3) 단위 가상 인터페이스. 라우팅 기반 VPN에 적합
- TAP — Ethernet 프레임(L2) 단위 가상 인터페이스. 브리지(Bridge)/가상화에 적합
- /dev/net/tun — userspace와 커널을 연결하는 문자 디바이스 (major 10, minor 200)
- tun_struct — 커널 내부에서 TUN/TAP 디바이스 하나를 대표하는 핵심 구조체(Struct)
- tun_file — 파일 디스크립터(File Descriptor)(fd)별 큐 상태를 관리하는 구조체. 멀티큐의 핵심
- 주요 활용 — VPN, QEMU/KVM virtio-net, 컨테이너(Container) 네트워크, 패킷 테스트
단계별 이해
- 용도별 모드 선택
라우팅 기반 VPN은 TUN, 브리지/가상 스위치 연동은 TAP부터 검토합니다. - 디바이스 생성 확인
open()+TUNSETIFF로 인터페이스가 실제 생성되는지 먼저 검증합니다. - 패킷 경로 추적
userspace read/write와 커널 네트워크 스택 사이 흐름을 tcpdump/trace로 확인합니다. - 운영 옵션 고정
IFF_NO_PI, 멀티큐,IFF_VNET_HDR, 권한 모델을 서비스 요구사항에 맞춰 표준화합니다.
개요
TUN/TAP는 커널 공간(Kernel Space)과 userspace 간 패킷을 전달하는 가상 네트워크 디바이스입니다.
물리적 NIC가 하드웨어로부터 패킷을 수신하는 것처럼, TUN/TAP는 userspace 프로세스(Process)로부터 패킷을 수신합니다.
커널 소스에서 drivers/net/tun.c 한 파일(약 3,500줄)에 모든 로직이 집중되어 있습니다.
TUN vs TAP 상세 비교
| 구분 | TUN (Network TUNnel) | TAP (Network TAP) |
|---|---|---|
| OSI 계층 | Layer 3 (Network) | Layer 2 (Data Link) |
| 패킷 형식 | IP 패킷 (IPv4/IPv6) | Ethernet 프레임 (MAC 포함) |
| 주요 용도 | VPN, IP 터널(Tunnel)링 | 가상화 (QEMU/KVM), 브리지 |
| MAC 주소 | 없음 | 있음 (커널 할당 또는 사용자 지정) |
| ARP 처리 | 불필요 | 필수 (L2 주소 해석) |
| 브리지 연결 | 불가 | 가능 (ip link set tapN master brN) |
| net_device type | ARPHRD_NONE |
ARPHRD_ETHER |
| IFF_NO_PI 없을 때 헤더 | 4바이트 PI 헤더 | 4바이트 PI 헤더 |
| 예시 | OpenVPN (routing), WireGuard 초기 | QEMU VM, OpenVPN (bridging), Docker |
Packet Information 헤더
IFF_NO_PI 플래그 없이 생성한 경우, 모든 패킷 앞에 4바이트 PI 헤더가 붙습니다.
| 오프셋(Offset) | 크기 | 필드 | 설명 |
|---|---|---|---|
| 0 | 2 bytes | flags | TUN_PKT_STRIP 등 플래그 |
| 2 | 2 bytes | proto | Ethernet 프로토콜 타입 (big-endian, 예: 0x0800=IPv4) |
/* include/uapi/linux/if_tun.h */
struct tun_pi {
__u16 flags;
__be16 proto;
};
/* IFF_NO_PI 권장 이유:
* 1. 4바이트 오버헤드 제거
* 2. 패킷 파싱 로직 단순화
* 3. virtio-net, vhost-net에서 필수
*/
IFF_NO_PI를 거의 항상 사용하세요. PI 헤더가 필요한 경우는 매우 드물며, 대부분의 현대 소프트웨어(QEMU, OpenVPN, WireGuard)는 IFF_NO_PI를 기본 사용합니다.
아키텍처
디바이스 생성
TUN/TAP 디바이스는 /dev/net/tun 문자 디바이스를 open()한 뒤 TUNSETIFF ioctl로 생성합니다.
커널 내부에서는 tun_set_iff() 함수가 net_device를 할당하고 tun_struct를 초기화합니다.
TUN 디바이스 생성
#include <linux/if.h>
#include <linux/if_tun.h>
#include <fcntl.h>
#include <sys/ioctl.h>
int tun_alloc(char *dev, int flags)
{
struct ifreq ifr;
int fd, err;
/* /dev/net/tun 열기 */
if ((fd = open("/dev/net/tun", O_RDWR)) < 0)
return fd;
memset(&ifr, 0, sizeof(ifr));
/* IFF_TUN: Layer 3 (IP), IFF_TAP: Layer 2 (Ethernet) */
ifr.ifr_flags = flags;
/* 디바이스 이름 지정 (선택적) */
if (*dev)
strncpy(ifr.ifr_name, dev, IFNAMSIZ);
/* TUN/TAP 인터페이스 생성 */
if ((err = ioctl(fd, TUNSETIFF, (void *)&ifr)) < 0) {
close(fd);
return err;
}
/* 할당된 디바이스 이름 반환 */
strcpy(dev, ifr.ifr_name);
return fd;
}
/* 사용 예시 */
char tun_name[IFNAMSIZ] = "tun0";
int tun_fd = tun_alloc(tun_name, IFF_TUN | IFF_NO_PI);
printf("TUN device: %s (fd=%d)\n", tun_name, tun_fd);
TAP 디바이스 생성
/* TAP (Ethernet) 디바이스 생성 */
char tap_name[IFNAMSIZ] = "tap0";
int tap_fd = tun_alloc(tap_name, IFF_TAP | IFF_NO_PI);
/* IFF_NO_PI: Packet Information 헤더 없이 순수 패킷만 전달 */
커널 내부 생성 흐름
TUN/TAP 플래그
| 플래그 | 값 | 설명 | 주요 소비자 |
|---|---|---|---|
IFF_TUN |
0x0001 |
TUN 디바이스 (Layer 3, IP 패킷) | VPN |
IFF_TAP |
0x0002 |
TAP 디바이스 (Layer 2, Ethernet 프레임) | QEMU, 브리지 |
IFF_NO_PI |
0x1000 |
Packet Information 헤더 생략 (권장) | 대부분의 현대 소프트웨어 |
IFF_MULTI_QUEUE |
0x0100 |
Multi-queue 지원 (성능 향상) | QEMU, vhost-net |
IFF_VNET_HDR |
0x4000 |
Virtio 네트워크 헤더 사용 | QEMU, vhost-net |
IFF_PERSIST |
0x0800 |
파일 디스크립터 닫아도 디바이스 유지 | persistent 인터페이스 |
IFF_ONE_QUEUE |
0x2000 |
단일 큐 (deprecated) | 레거시 |
IFF_NAPI |
0x0010 |
NAPI 기반 RX 처리 | 고성능 시나리오 |
IFF_NAPI_FRAGS |
0x0020 |
NAPI + 프래그먼트 지원 | vhost-net 제로카피 |
IFF_TUN과 IFF_TAP는 상호 배타적입니다. 동시에 설정하면 -EINVAL이 반환됩니다.
IFF_MULTI_QUEUE는 Linux 3.8 이상에서만 사용 가능합니다.
패킷 송수신
패킷 읽기 (커널 TX 경로 → Userspace)
네트워크 스택이 TUN/TAP 인터페이스로 패킷을 보내면(dev_queue_xmit), 해당 패킷은 tun_net_xmit()에 의해 tun_file의 소켓(Socket) 수신 큐에 저장됩니다.
Userspace는 read() / readv() 시스템 콜(System Call)로 이 패킷을 가져갑니다.
/* TUN: IP 패킷 읽기 */
unsigned char buffer[65535]; /* 최대 패킷 크기 */
ssize_t nread;
while (1) {
nread = read(tun_fd, buffer, sizeof(buffer));
if (nread < 0) {
if (errno == EAGAIN)
continue; /* non-blocking mode */
perror("read");
break;
}
/* IP 패킷 분석 */
struct iphdr *ip = (struct iphdr *)buffer;
printf("IP packet: %d.%d.%d.%d -> %d.%d.%d.%d, proto=%d, len=%d\n",
ip->saddr & 0xff, (ip->saddr >> 8) & 0xff,
(ip->saddr >> 16) & 0xff, (ip->saddr >> 24) & 0xff,
ip->daddr & 0xff, (ip->daddr >> 8) & 0xff,
(ip->daddr >> 16) & 0xff, (ip->daddr >> 24) & 0xff,
ip->protocol, ntohs(ip->tot_len));
/* 패킷 처리 (VPN 암호화, 로깅 등) */
process_packet(buffer, nread);
}
패킷 쓰기 (Userspace → 커널 RX 경로)
/* TUN: IP 패킷 주입 */
unsigned char packet[1500];
size_t packet_len;
/* 패킷 생성 (예: ICMP Echo Reply) */
build_icmp_reply(packet, &packet_len);
/* 커널로 패킷 전달 — netif_rx_ni()를 통해 네트워크 스택 진입 */
ssize_t nwrite = write(tun_fd, packet, packet_len);
if (nwrite < 0) {
perror("write");
}
패킷 흐름 상세
TAP: Ethernet 프레임 처리
/* TAP: Ethernet 프레임 읽기 */
unsigned char buffer[1518]; /* 1500 + 14 (Ethernet header) + 4 (VLAN) */
ssize_t nread = read(tap_fd, buffer, sizeof(buffer));
/* Ethernet 헤더 분석 */
struct ethhdr *eth = (struct ethhdr *)buffer;
printf("Ethernet: %02x:%02x:%02x:%02x:%02x:%02x -> "
"%02x:%02x:%02x:%02x:%02x:%02x, proto=0x%04x\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],
eth->h_dest[0], eth->h_dest[1], eth->h_dest[2],
eth->h_dest[3], eth->h_dest[4], eth->h_dest[5],
ntohs(eth->h_proto));
네트워크 설정
IP 주소 및 라우팅
# TUN 인터페이스 IP 설정
$ sudo ip addr add 10.0.0.1/24 dev tun0
$ sudo ip link set tun0 up
# 라우팅 추가
$ sudo ip route add 10.1.0.0/16 dev tun0
# TAP 브리지 연결
$ sudo ip link add br0 type bridge
$ sudo ip link set tap0 master br0
$ sudo ip link set br0 up
$ sudo ip link set tap0 up
Persistent 디바이스
# ip tuntap 명령으로 persistent 디바이스 생성
$ sudo ip tuntap add dev tun0 mode tun
$ sudo ip tuntap add dev tap0 mode tap user alice
# 목록 확인
$ ip tuntap show
tun0: tun
tap0: tap persist user 1000
# 프로그래밍 방식으로 persist 설정
# ioctl(tun_fd, TUNSETPERSIST, 1);
# 삭제
$ sudo ip tuntap del dev tun0 mode tun
권한 모델
| 방법 | 명령/코드 | 보안 수준 |
|---|---|---|
| root 직접 생성 | sudo ip tuntap add ... |
높음 (root 필요) |
| 특정 사용자 허용 | sudo ip tuntap add dev tap0 mode tap user alice |
중간 (지정 사용자만) |
| 특정 그룹 허용 | sudo ip tuntap add dev tap0 mode tap group kvm |
중간 (지정 그룹만) |
| CAP_NET_ADMIN | setcap cap_net_admin+ep ./myapp |
낮음-중간 |
| /dev/net/tun 권한 | chmod 0666 /dev/net/tun |
낮음 (비권장) |
/dev/net/tun의 권한을 0666으로 설정하면 모든 사용자가 가상 네트워크 인터페이스를 생성할 수 있어 보안 위험이 있습니다.
운영 환경에서는 반드시 user/group 옵션 또는 CAP_NET_ADMIN을 사용하세요.
커널 내부 구조
tun_struct 상세
tun_struct는 TUN/TAP 디바이스 하나를 대표하는 핵심 구조체입니다. net_device의 priv 영역에 내장됩니다.
/* drivers/net/tun.c — Linux 6.x */
struct tun_struct {
/* === 큐 관리 === */
struct tun_file __rcu *tfiles[MAX_TAP_QUEUES]; /* 큐별 tun_file */
unsigned int numqueues; /* 활성 큐 수 */
unsigned int numdisabled; /* 비활성 큐 수 */
struct list_head disabled; /* 비활성 큐 리스트 */
/* === 디바이스 속성 === */
unsigned int flags; /* IFF_TUN, IFF_TAP 등 */
kuid_t owner; /* 소유자 UID */
kgid_t group; /* 소유자 GID */
/* === 네트워크 디바이스 === */
struct net_device *dev; /* net_device 포인터 */
struct net_device_stats stats; /* deprecated, rtnl_link_stats64 사용 */
netdev_features_t set_features; /* 설정된 오프로드 피처 */
/* === BPF/XDP === */
struct bpf_prog __rcu *xdp_prog; /* XDP 프로그램 */
struct tun_prog __rcu *steering_prog; /* 큐 스티어링 eBPF */
struct tun_prog __rcu *filter_prog; /* 패킷 필터 eBPF */
/* === 클래식 BPF 필터 === */
struct sock_fprog fprog; /* cBPF 필터 프로그램 */
bool filter_attached;
/* === 플로우 관리 === */
u32 flow_count; /* 활성 플로우 수 */
u32 ageing_time; /* 플로우 에이징 타임 */
struct hlist_head flows[TUN_NUM_FLOW_ENTRIES]; /* 플로우 해시 */
struct timer_list flow_gc_timer; /* 플로우 GC 타이머 */
/* === vnet header === */
int vnet_hdr_sz; /* virtio-net 헤더 크기 */
int align; /* 정렬 바이트 */
/* === 기타 === */
struct hlist_node hash_link; /* tun 글로벌 해시 */
int sndbuf; /* 송신 버퍼 크기 */
struct mutex lock;
};
tun_file 상세
tun_file은 각 파일 디스크립터(큐)별 상태를 관리합니다. 멀티큐 모드에서는 하나의 tun_struct에 여러 tun_file이 연결됩니다.
struct tun_file {
struct sock sk; /* 소켓 (수신 큐 관리) */
struct socket socket; /* 소켓 구조체 */
struct tun_struct __rcu *tun; /* 소속 tun_struct */
struct fasync_struct *fasync; /* async I/O 통지 */
unsigned int flags; /* 파일별 플래그 */
union {
u16 queue_index; /* 큐 인덱스 */
unsigned int ifindex; /* detach 시 인덱스 */
};
struct napi_struct napi; /* NAPI 폴링 구조체 */
bool napi_enabled; /* NAPI 활성화 여부 */
bool napi_frags_enabled;
struct mutex napi_mutex;
struct list_head next; /* disabled 리스트 연결 */
struct ptr_ring tx_ring; /* TX 링 버퍼 */
struct xdp_rxq_info xdp_rxq; /* XDP 수신 큐 정보 */
};
ioctl 인터페이스
| ioctl | 값 | 설명 | 인자 |
|---|---|---|---|
TUNSETIFF | 0x400454ca | 인터페이스 생성/연결 | struct ifreq |
TUNSETPERSIST | 0x400454cb | persistent 모드 설정 | int (0/1) |
TUNSETOWNER | 0x400454cc | 소유자 UID 설정 | uid_t |
TUNSETGROUP | 0x400454ce | 소유자 GID 설정 | gid_t |
TUNSETOFFLOAD | 0x400454d0 | 오프로드 플래그 설정 | unsigned int |
TUNSETVNETHDRSZ | 0x400454d8 | vnet 헤더 크기 설정 | int |
TUNSETQUEUE | 0x400454d9 | 큐 활성화/비활성화 | struct ifreq |
TUNSETSNDBUF | 0x400454d4 | 송신 버퍼 크기 설정 | int |
TUNGETFEATURES | 0x800454cf | 지원 기능 조회 | unsigned int * |
TUNSETFILTEREBPF | 0x800454e1 | eBPF 필터 설정 | int (fd) |
TUNSETSTEEREBPF | 0x800454e0 | eBPF 스티어링 설정 | int (fd) |
net_device_ops
/* drivers/net/tun.c */
static const struct net_device_ops tun_netdev_ops = {
.ndo_init = tun_net_init,
.ndo_uninit = tun_net_uninit,
.ndo_open = tun_net_open,
.ndo_stop = tun_net_close,
.ndo_start_xmit = tun_net_xmit, /* TX: 커널 -> userspace */
.ndo_fix_features = tun_net_fix_features,
.ndo_select_queue = tun_select_queue, /* 멀티큐 선택 */
.ndo_set_rx_mode = tun_net_mclist,
.ndo_set_mac_address = eth_mac_addr, /* TAP만 */
.ndo_change_mtu = tun_net_change_mtu,
.ndo_get_stats64 = tun_net_get_stats64,
.ndo_bpf = tun_xdp, /* XDP 프로그램 로드 */
.ndo_xdp_xmit = tun_xdp_xmit, /* XDP 전송 */
};
tun_struct 내부 구조
tun_struct는 TUN/TAP 디바이스의 모든 상태를 관장하는 중심 구조체입니다.
net_device의 priv 영역에 내장되며, alloc_netdev_mqs()를 통해 할당됩니다.
아래에서는 각 필드 그룹의 역할과 커널 내부 사용 패턴을 심층 분석합니다.
큐 관리 필드 상세
tfiles[] 배열은 RCU로 보호되며, 활성 큐와 비활성 큐를 독립적으로 관리합니다.
numqueues와 numdisabled의 합이 총 연결된 tun_file 수입니다.
/* 큐 관리 핵심 함수들 — drivers/net/tun.c */
/* tun_file 연결 (attach) */
static int tun_attach(struct tun_struct *tun,
struct tun_file *tfile,
bool skip_filter, bool napi,
bool napi_frags, bool publish_tun)
{
int err;
err = security_tun_dev_attach_queue(tun->security);
if (err < 0)
goto out;
err = -EINVAL;
if (rtnl_dereference(tfile->tun) && !tfile->detached)
goto out;
err = -E2BIG;
if (!tfile->detached &&
tun->numqueues + tun->numdisabled == MAX_TAP_QUEUES)
goto out;
/* RCU 보호 하에 tfiles[] 배열 갱신 */
rcu_assign_pointer(tun->tfiles[tun->numqueues], tfile);
tfile->queue_index = tun->numqueues;
tun->numqueues++;
if (publish_tun)
rcu_assign_pointer(tfile->tun, tun);
/* NAPI 활성화 */
if (napi)
tun_napi_init(tun, tfile, napi_frags);
...
out:
return err;
}
/* 큐 동적 해제 (detach) */
static void __tun_detach(struct tun_file *tfile, bool clean)
{
struct tun_struct *tun;
tun = rtnl_dereference(tfile->tun);
if (tun && !tfile->detached) {
u16 index = tfile->queue_index;
/* 마지막 큐와 swap하여 배열 압축 */
struct tun_file *ntfile =
rtnl_dereference(tun->tfiles[tun->numqueues - 1]);
ntfile->queue_index = index;
rcu_assign_pointer(tun->tfiles[index], ntfile);
tun->numqueues--;
...
}
}
플로우 해시 테이블(Hash Table)
TUN/TAP는 패킷의 src/dst 정보를 기반으로 플로우를 해시(Hash)하여,
동일한 플로우의 패킷이 같은 큐로 전달되도록 합니다.
flows[] 해시 테이블은 TUN_NUM_FLOW_ENTRIES(1024) 슬롯으로 구성됩니다.
/* 플로우 엔트리 구조체 */
struct tun_flow_entry {
struct hlist_node hash_link; /* 해시 체인 */
struct rcu_head rcu; /* RCU 콜백 */
struct tun_struct *tun; /* 소유 tun_struct */
u32 rxhash; /* 플로우 해시 값 */
u32 rps_rxhash; /* RPS 해시 */
int queue_index; /* 매핑된 큐 인덱스 */
unsigned long updated; /* 마지막 갱신 시점 (jiffies) */
};
/* 플로우 lookup */
static struct tun_flow_entry *tun_flow_find(
struct hlist_head *head, u32 rxhash)
{
struct tun_flow_entry *e;
hlist_for_each_entry_rcu(e, head, hash_link) {
if (e->rxhash == rxhash)
return e;
}
return NULL;
}
/* 플로우 GC — ageing_time 경과 시 삭제 */
static void tun_flow_cleanup(struct timer_list *t)
{
struct tun_struct *tun =
from_timer(tun, t, flow_gc_timer);
unsigned long delay = tun->ageing_time;
unsigned long count = 0;
int i;
spin_lock(&tun->lock);
for (i = 0; i < TUN_NUM_FLOW_ENTRIES; i++) {
struct tun_flow_entry *e;
struct hlist_node *n;
hlist_for_each_entry_safe(e, n,
&tun->flows[i], hash_link) {
if (time_after(jiffies,
e->updated + delay)) {
tun_flow_delete(tun, e);
count++;
}
}
}
spin_unlock(&tun->lock);
if (count)
netdev_dbg(tun->dev,
"tun_flow_cleanup: removed %lu flows\n", count);
mod_timer(&tun->flow_gc_timer, jiffies + delay);
}
RCU 보호 패턴
tun_struct와 tun_file 사이의 참조는 RCU(Read-Copy-Update)로 보호됩니다.
이를 통해 패킷 경로(hot path)에서 잠금(Lock) 없이 구조체에 접근할 수 있습니다.
/* Hot path에서의 RCU 패턴 */
static netdev_tx_t tun_net_xmit(struct sk_buff *skb,
struct net_device *dev)
{
struct tun_struct *tun = netdev_priv(dev);
int txq = skb->queue_mapping;
struct tun_file *tfile;
rcu_read_lock();
tfile = rcu_dereference(tun->tfiles[txq]);
if (!tfile) {
rcu_read_unlock();
goto drop;
}
/* 락 없이 tfile에 안전하게 접근 */
...
rcu_read_unlock();
return NETDEV_TX_OK;
}
tfiles[] 배열 갱신(attach/detach)은 반드시 rtnl_lock을 보유한 상태에서 수행해야 합니다.
읽기 경로(tun_net_xmit, tun_chr_read_iter)는 RCU read-side critical section에서 실행되므로 잠금이 필요 없습니다.
XDP 지원
TUN/TAP는 XDP(eXpress Data Path) 프로그램을 지원하여, 패킷이 네트워크 스택에 도달하기 전에
드라이버 수준에서 고속 처리할 수 있습니다. TUN/TAP의 XDP는 XDP_DRIVER 모드가 아닌
XDP_SKB(generic) 모드와 native 모드를 모두 지원합니다.
XDP 프로그램 부착
# TUN/TAP에 XDP 프로그램 로드
$ ip link set dev tun0 xdpgeneric obj xdp_prog.o sec xdp_filter
# 상태 확인
$ ip link show tun0
3: tun0: <POINTOPOINT,MULTICAST,NOARP,UP> ...
prog/xdp id 42 tag abc123def456
/* XDP 프로그램 예시: 특정 IP 차단 */
SEC("xdp")
int xdp_filter(struct xdp_md *ctx)
{
void *data_end = (void *)(long)ctx->data_end;
void *data = (void *)(long)ctx->data;
struct iphdr *iph = data;
if ((void *)(iph + 1) > data_end)
return XDP_PASS;
/* 10.0.0.100 차단 */
if (iph->saddr == __constant_htonl(0x0a000064))
return XDP_DROP;
return XDP_PASS;
}
XDP_PASS(스택 전달), XDP_DROP(폐기), XDP_TX(같은 인터페이스 재전송(Retransmission)), XDP_REDIRECT(다른 인터페이스 전달), XDP_ABORTED(에러 처리). TUN/TAP는 5가지 액션을 모두 지원합니다.
XDP 커널 내부 처리 흐름
TUN/TAP에서 XDP 프로그램은 tun_get_user() 내에서 실행됩니다.
패킷이 userspace에서 커널로 진입하는 시점, 즉 write() 시스템 콜 경로에서
netif_receive_skb() 호출 전에 XDP 프로그램이 먼저 실행됩니다.
/* tun_get_user() 내 XDP 처리 경로 — drivers/net/tun.c (간략화) */
static ssize_t tun_get_user(struct tun_struct *tun,
struct tun_file *tfile,
void *msg_control,
struct iov_iter *from,
int noblock, bool more)
{
struct bpf_prog *xdp_prog;
...
/* XDP 프로그램 확인 (RCU read-side) */
xdp_prog = rcu_dereference(tun->xdp_prog);
if (xdp_prog) {
struct xdp_buff xdp;
u32 act;
/* XDP 버퍼 초기화 */
xdp_init_buff(&xdp, buflen, &tfile->xdp_rxq);
xdp_prepare_buff(&xdp, buf, XDP_PACKET_HEADROOM, len, 0);
/* BPF 프로그램 실행 */
act = bpf_prog_run_xdp(xdp_prog, &xdp);
switch (act) {
case XDP_REDIRECT:
xdp_do_redirect(tun->dev, &xdp, xdp_prog);
return total_len;
case XDP_TX:
/* 같은 인터페이스의 다른 큐로 재전송 */
tun_xdp_tx(tun->dev, &xdp);
return total_len;
case XDP_PASS:
/* 정상 경로 진행 — skb 할당 후 네트워크 스택 */
break;
default:
bpf_warn_invalid_xdp_action(tun->dev, xdp_prog, act);
/* fallthrough */
case XDP_ABORTED:
trace_xdp_exception(tun->dev, xdp_prog, act);
/* fallthrough */
case XDP_DROP:
atomic_long_inc(&tun->dev->rx_dropped);
return total_len;
}
}
...
}
XDP_REDIRECT 구현: tun_xdp_xmit()
XDP_REDIRECT 액션에서 다른 인터페이스가 TUN/TAP인 경우,
tun_xdp_xmit()이 호출되어 해당 TUN/TAP의 수신 큐에 패킷을 넣습니다.
/* tun_xdp_xmit() — XDP redirect 대상일 때 호출 */
static int tun_xdp_xmit(struct net_device *dev,
int n, struct xdp_frame **frames,
u32 flags)
{
struct tun_struct *tun = netdev_priv(dev);
struct tun_file *tfile;
int nxmit = 0;
u32 numqueues;
rcu_read_lock();
numqueues = READ_ONCE(tun->numqueues);
if (!numqueues)
goto out;
tfile = rcu_dereference(
tun->tfiles[smp_processor_id() % numqueues]);
for (int i = 0; i < n; i++) {
struct xdp_frame *xdpf = frames[i];
/* ptr_ring에 프레임 삽입 (lock-free) */
if (__ptr_ring_produce(&tfile->tx_ring, xdpf))
break;
nxmit++;
}
out:
rcu_read_unlock();
return nxmit;
}
/* XDP 프로그램: TUN/TAP 간 패킷 리다이렉트 예시 */
struct {
__uint(type, BPF_MAP_TYPE_DEVMAP);
__uint(max_entries, 256);
__type(key, __u32);
__type(value, __u32);
} tx_port SEC(".maps");
SEC("xdp")
int xdp_redirect_tun(struct xdp_md *ctx)
{
/* tun0 -> tun1 리다이렉트 */
return bpf_redirect_map(&tx_port, 0, 0);
}
ptr_ring을 사용하여 lock-free로 패킷을 전달합니다.
멀티큐와 XDP를 함께 사용하면 CPU별 큐에서 독립적으로 XDP 프로그램이 실행되어 확장성이 향상됩니다.
다만, TUN/TAP XDP는 하드웨어 NIC의 네이티브 XDP보다 성능이 낮을 수 있습니다.
이는 패킷이 userspace에서 메모리 복사를 거쳐 오기 때문입니다.
TUN AF_XDP Rx zero-copy(2025 업스트림)
2025년 netdev 트리에 TUN 드라이버의 AF_XDP Rx zero-copy 패치(Patch)셋이 제출되었습니다.
기존에는 TUN/TAP 기반 가상 NIC에서 AF_XDP 소켓이 copy mode로만 동작하여,
userspace에서 UMEM으로 패킷 데이터가 복사되는 오버헤드가 있었습니다.
패치셋은 tun_sendmsg 경로에서 UMEM descriptor를 직접 참조하여
복사 없이 RX 링에 푸시합니다.
/* drivers/net/tun.c — AF_XDP zero-copy RX 경로 (개념 코드) */
static int tun_put_user_xdp_zc(struct tun_file *tfile,
struct xdp_buff *xdp)
{
struct xsk_buff_pool *pool = READ_ONCE(tfile->xsk_pool);
if (!pool)
return -ENOTSUPP;
/* UMEM descriptor 할당 + XDP 버퍼 이식 */
return xsk_rcv_zc(pool, xdp, xdp->data_end - xdp->data);
}
# AF_XDP 바인딩 확인 (XDP_ZEROCOPY 플래그)
cat /proc/net/xdp_sockets | column -t
# ifindex rx tx rx-copy zc pid ...
# 12 1048576 1048576 0 1 12345
# ↑ zc=1 이면 zero-copy 모드
# bpftrace로 TUN zc 경로 검증
bpftrace -e 'kprobe:tun_put_user_xdp_zc { @[comm] = count(); }'
CVE-2025-37920 — AF_XDP shared UMEM race 수정
2025년 공개된 CVE-2025-37920은 AF_XDP의 공유 UMEM 모드에서
여러 소켓이 동일한 xsk_buff_pool을 공유할 때 발생하는 경쟁 조건(race condition)입니다.
FILL 큐는 pool 단위로 공유되지만 RX 큐는 소켓-로컬이므로,
두 CPU 코어가 서로 다른 소켓의 RX/FILL 상태를 동시에 갱신하여
잘못된 인덱스 상태를 만들 수 있었습니다.
/* net/xdp/xsk_buff_pool.c — 수정 후 */
static void xp_release_deferred(struct work_struct *work)
{
struct xsk_buff_pool *pool = container_of(work,
struct xsk_buff_pool, work);
/* 수정 핵심: RX 경로와 FILL 큐 접근을 mutex로 직렬화 */
mutex_lock(&pool->mtx);
xp_clear_dev(pool);
mutex_unlock(&pool->mtx);
xp_put_pool(pool);
}
vhost-net 제로카피
vhost-net은 커널 공간에서 TAP 디바이스와 virtio-net 링을 직접 연결하여 QEMU의 userspace 전환 오버헤드(Overhead)를 제거합니다.
제로카피(zero-copy) 모드에서는 게스트 메모리의 패킷 데이터를 복사하지 않고 바로 TAP으로 전달합니다.
vhost-net 설정
# vhost-net 커널 모듈 확인
$ lsmod | grep vhost_net
vhost_net 32768 1
vhost 53248 1 vhost_net
# QEMU에서 vhost-net 활성화
$ qemu-system-x86_64 \
-netdev tap,id=net0,ifname=tap0,script=no,downscript=no,vhost=on \
-device virtio-net-pci,netdev=net0,mrg_rxbuf=on \
...
전송 경로 비교
| 항목 | 일반 TAP (QEMU 처리) | vhost-net | vhost-net + zero-copy |
|---|---|---|---|
| 데이터 경로 | Guest → QEMU → TAP | Guest → vhost-net → TAP | Guest → vhost-net → TAP (복사 없음) |
| 컨텍스트 전환 | Guest → Host User → Kernel | Guest → Kernel만 | Guest → Kernel만 |
| 메모리 복사 | 2회 | 1회 | 0회 |
| 성능 (64B 패킷) | 기준 | 약 +30~50% | 약 +50~80% |
| 설정 | 기본 | vhost=on | vhost=on + 커널 지원 |
vhost-net 커널 내부 경로
vhost-net은 drivers/vhost/net.c에 구현된 커널 스레드(Kernel Thread)로,
QEMU를 경유하지 않고 virtqueue와 TAP 사이에서 직접 패킷을 전달합니다.
핵심 함수는 TX 경로의 handle_tx()와 RX 경로의 handle_rx()입니다.
/* vhost-net TX 경로 (Guest TX → TAP) — drivers/vhost/net.c (간략화) */
static void handle_tx(struct vhost_net *net)
{
struct vhost_net_virtqueue *nvq = &net->vqs[VHOST_NET_VQ_TX];
struct vhost_virtqueue *vq = &nvq->vq;
struct socket *sock;
mutex_lock(&vq->mutex);
sock = vhost_vq_get_backend(vq); /* TAP 소켓 */
for (;;) {
struct iov_iter msg_iter;
ssize_t err;
int head;
/* virtqueue에서 사용 가능한 버퍼 가져오기 */
head = vhost_get_vq_desc(vq, vq->iov,
ARRAY_SIZE(vq->iov), &out, &in, NULL, NULL);
if (head < 0)
break;
/* TAP 소켓으로 직접 전송 (sendmsg) */
msg.msg_control = NULL;
iov_iter_init(&msg_iter, WRITE, vq->iov, out, len);
err = sock->ops->sendmsg(sock, &msg, len);
/* tun_sendmsg() 호출 → tun_get_user() */
vhost_add_used_and_signal(&net->dev, vq, head, 0);
}
mutex_unlock(&vq->mutex);
}
/* vhost-net RX 경로 (TAP → Guest RX) */
static void handle_rx(struct vhost_net *net)
{
/* TAP에서 recvmsg()로 패킷 수신
* → virtqueue에 패킷을 넣고 guest에 인터럽트 */
sock->ops->recvmsg(sock, &msg, peek_head_len, MSG_DONTWAIT);
/* tun_recvmsg() 호출 → tun_put_user() */
vhost_add_used_and_signal(&net->dev, vq, head, len);
}
제로카피 상세 메커니즘
vhost-net 제로카피는 TX 경로(Guest → Host)에서 게스트 메모리의 패킷 데이터를
호스트 커널의 TAP으로 전달할 때 복사를 생략합니다. 이는 get_user_pages()로
게스트 물리 메모리(Physical Memory)를 직접 참조하여 DMA scatter-gather I/O로 전달하는 방식입니다.
| 항목 | 일반 경로 | 제로카피 경로 |
|---|---|---|
| 메모리 접근 | memcpy_from_msg() 1회 | get_user_pages() + DMA |
| 패킷 완료 통지 | 즉시 used ring 갱신 | DMA 완료 후 콜백(Callback)에서 갱신 |
| 대기 시간(Latency) | 짧음 (동기) | 길 수 있음 (비동기) |
| 최적 시나리오 | 소형 패킷 (64~256B) | 대형 패킷 (TSO/GSO, 1500B+) |
| 조건 | 항상 사용 가능 | sndbuf 설정, experimental |
| 커널 함수 | tun_sendmsg() | tun_sendmsg() + zerocopy_sg_from_iter() |
# vhost-net 제로카피 활성화 확인
$ cat /sys/module/vhost_net/parameters/experimental_zcopytx
0 # 0=비활성, 1=활성
# 모듈 파라미터로 활성화
$ sudo modprobe vhost_net experimental_zcopytx=1
# QEMU 측에서 sndbuf 설정 (제로카피 튜닝)
$ qemu-system-x86_64 \
-netdev tap,id=net0,ifname=tap0,vhost=on,sndbuf=0 \
-device virtio-net-pci,netdev=net0 ...
Multi-Queue TUN/TAP
Multi-queue는 여러 CPU 코어에서 병렬로 패킷을 처리하여 성능을 향상시킵니다.
하나의 TUN/TAP 디바이스에 최대 MAX_TAP_QUEUES(256)개의 큐를 생성할 수 있으며,
각 큐는 독립적인 파일 디스크립터와 tun_file 구조체를 갖습니다.
Multi-Queue 설정
/* Multi-queue TUN 생성 (4개 큐) */
#define NUM_QUEUES 4
int fds[NUM_QUEUES];
char dev[IFNAMSIZ] = "tap0";
for (int i = 0; i < NUM_QUEUES; i++) {
fds[i] = tun_alloc(dev, IFF_TAP | IFF_NO_PI | IFF_MULTI_QUEUE | IFF_VNET_HDR);
if (fds[i] < 0) {
perror("tun_alloc");
return -1;
}
}
/* 각 큐를 별도 스레드에서 처리 */
for (int i = 0; i < NUM_QUEUES; i++) {
pthread_create(&threads[i], NULL, tun_thread, &fds[i]);
}
/* 런타임 큐 비활성화 (동적 스케일링) */
struct ifreq ifr;
memset(&ifr, 0, sizeof(ifr));
strncpy(ifr.ifr_name, dev, IFNAMSIZ);
ifr.ifr_flags = IFF_DETACH_QUEUE;
ioctl(fds[3], TUNSETQUEUE, &ifr); /* 큐 3 비활성화 */
/* 큐 재활성화 */
ifr.ifr_flags = IFF_ATTACH_QUEUE;
ioctl(fds[3], TUNSETQUEUE, &ifr); /* 큐 3 다시 활성화 */
스티어링 프로그램
eBPF 스티어링 프로그램으로 패킷이 어떤 큐로 전달될지 제어할 수 있습니다.
/* eBPF 스티어링 프로그램: 소스 IP 해시 기반 큐 선택 */
SEC("tun_steering")
int tun_queue_select(struct __sk_buff *skb)
{
__u32 src_ip;
/* IP 헤더에서 소스 주소 추출 */
bpf_skb_load_bytes(skb, 12, &src_ip, 4);
/* 간단한 해시로 큐 선택 (4개 큐 가정) */
return jhash_1word(src_ip, 0) % 4;
}
멀티큐 커널 구현 상세
tun_select_queue()는 TX 경로에서 패킷이 어떤 큐로 전달될지 결정하는 핵심 함수입니다.
이 함수는 ndo_select_queue 콜백으로 등록되어 dev_queue_xmit()에서 호출됩니다.
/* tun_select_queue() — 큐 선택 로직 */
static u16 tun_select_queue(struct net_device *dev,
struct sk_buff *skb,
struct net_device *sb_dev)
{
struct tun_struct *tun = netdev_priv(dev);
struct tun_prog *peer;
u32 numqueues = 0;
rcu_read_lock();
numqueues = READ_ONCE(tun->numqueues);
/* 1) eBPF 스티어링 프로그램이 있으면 최우선 */
peer = rcu_dereference(tun->steering_prog);
if (peer) {
u32 ret = bpf_prog_run_clear_cb(peer->prog, skb);
rcu_read_unlock();
return ret % numqueues;
}
/* 2) 플로우 해시 기반 자동 분배 */
if (numqueues > 1 && skb->hash) {
struct tun_flow_entry *e;
e = tun_flow_find(&tun->flows[tun_hashfn(skb->hash)],
skb->hash);
if (e) {
rcu_read_unlock();
return e->queue_index;
}
}
/* 3) Fallback: 현재 CPU 기반 선택 */
rcu_read_unlock();
return reciprocal_scale(skb_get_hash(skb), numqueues);
}
동적 큐 활성화/비활성화
멀티큐 TUN/TAP는 런타임에 큐를 동적으로 활성화/비활성화할 수 있습니다. 이 기능은 QEMU의 vCPU 수가 변경되거나, 부하에 따라 큐 수를 조정할 때 사용됩니다.
/* 큐 비활성화 (TUNSETQUEUE + IFF_DETACH_QUEUE) */
static int tun_queue_disable(struct tun_file *tfile)
{
struct tun_struct *tun = rtnl_dereference(tfile->tun);
/* tfiles[] 배열에서 제거 */
__tun_detach(tfile, 0);
/* disabled 리스트에 추가 */
tfile->detached = 1;
list_add_tail(&tfile->next, &tun->disabled);
tun->numdisabled++;
return 0;
}
/* 큐 재활성화 (TUNSETQUEUE + IFF_ATTACH_QUEUE) */
static int tun_queue_enable(struct tun_file *tfile)
{
/* disabled 리스트에서 제거 후 tfiles[]에 다시 추가 */
tfile->detached = 0;
list_del_init(&tfile->next);
tun->numdisabled--;
tun_attach(tun, tfile, ...);
return 0;
}
NAPI 기반 수신 처리
IFF_NAPI 플래그를 사용하면 각 큐에서 NAPI 기반 배치 수신 처리가 활성화됩니다.
고부하 환경에서 인터럽트(Interrupt) 빈도를 줄이고 처리 효율을 높입니다.
/* NAPI 초기화 — 큐별 napi_struct 설정 */
static void tun_napi_init(struct tun_struct *tun,
struct tun_file *tfile,
bool napi_frags)
{
tfile->napi_enabled = true;
tfile->napi_frags_enabled = napi_frags;
netif_napi_add_tx(tun->dev, &tfile->napi, tun_napi_poll);
napi_enable(&tfile->napi);
}
/* NAPI 폴링 함수 */
static int tun_napi_poll(struct napi_struct *napi, int budget)
{
struct tun_file *tfile =
container_of(napi, struct tun_file, napi);
struct sk_buff_head *queue = &tfile->sk.sk_write_queue;
struct sk_buff *skb;
int received = 0;
while (received < budget &&
(skb = __skb_dequeue(queue)) != NULL) {
netif_receive_skb(skb);
received++;
}
if (received < budget)
napi_complete_done(napi, received);
return received;
}
IFF_NAPI와 IFF_NAPI_FRAGS를 함께 활성화하면
vhost-net의 scatter-gather 패킷도 NAPI 경로에서 처리되어 성능이 향상됩니다.
QEMU 6.0 이상에서 자동으로 설정합니다.
macvtap 비교
macvtap은 MACVLAN과 TAP을 결합한 가상 디바이스로, TAP과 유사한 용도이지만 다른 아키텍처를 가집니다.
| 항목 | TAP | macvtap | macvlan |
|---|---|---|---|
| 계층 | L2 (가상 NIC) | L2 (가상 NIC) | L2 (MAC 기반) |
| 물리 NIC 필요 | 아니오 | 예 (상위 인터페이스 필수) | 예 |
| 브리지 필요 | 예 (외부 연결 시) | 아니오 (자체 브리지) | 아니오 |
| userspace 접근 | /dev/net/tun | /dev/tapN | 없음 (커널만) |
| QEMU 지원 | 예 (-netdev tap) | 예 (-netdev tap,fd=...) | 불가 |
| 성능 (VM 네트워크) | 중간 | 높음 (브리지 생략) | 해당 없음 |
| 호스트-VM 통신 | 브리지 통해 가능 | VEPA 모드만 가능 | 해당 없음 |
| SR-IOV 호환 | 아니오 | 패스스루 모드 | 아니오 |
# macvtap 생성
$ sudo ip link add link eth0 name macvtap0 type macvtap mode bridge
$ sudo ip link set macvtap0 up
# 캐릭터 디바이스 확인
$ ls /dev/tap*
/dev/tap12 # ifindex 기반 디바이스 파일
# QEMU에서 macvtap 사용
$ qemu-system-x86_64 \
-netdev tap,id=net0,fd=3 3<>/dev/tap12 \
-device virtio-net-pci,netdev=net0 \
...
macvtap 운영 모드
| 모드 | 동작 | 호스트-VM 통신 | 사용 사례 |
|---|---|---|---|
bridge | 같은 물리 NIC 하위 macvtap 간 직접 통신 | 불가 | VM 간 통신 필요 |
vepa | 모든 트래픽이 외부 스위치를 경유 | 외부 스위치 경유 가능 | VEPA 호환 스위치 환경 |
private | macvtap 간 통신 차단 | 불가 | 완전 격리(Isolation) 요구 |
passthrough | 물리 NIC 직접 할당 (SR-IOV VF) | 불가 | 최대 성능 |
TAP vs macvtap 성능 비교
| 시나리오 | TAP + Bridge | macvtap (bridge) | 비고 |
|---|---|---|---|
| 처리량(Throughput) (TCP, 1 stream) | 기준 (100%) | 약 105~115% | 브리지 오버헤드 제거 |
| 처리량 (TCP, 8 streams) | 기준 | 약 110~120% | 멀티큐 + 브리지 생략 |
| 지연 시간 (평균) | 기준 | 약 -5~10% | 경로 단축 |
| CPU 사용률 | 기준 | 약 -10~15% | 브리지 처리 제거 |
| 호스트-VM 통신 | 가능 (브리지) | 제한적 (VEPA만) | TAP 유리 |
| 설정 복잡도 | 높음 (브리지 필요) | 낮음 | macvtap 유리 |
- TAP + Bridge: 호스트-VM 통신이 필요하거나, OVS/Netfilter 같은 고급 네트워크 기능이 필요한 경우
- macvtap: 단순한 외부 네트워크 연결만 필요하고, 최대 성능이 목표인 경우
- macvtap passthrough: SR-IOV VF를 사용하여 하드웨어 수준 성능이 필요한 경우
virtio-net + TAP 통합
QEMU/KVM 환경에서 virtio-net과 TAP의 조합은 가상 네트워킹의 표준입니다.
게스트의 virtio-net 드라이버가 virtqueue를 통해 패킷을 전달하면,
호스트의 TAP 디바이스가 이를 물리 네트워크로 전달합니다.
virtio-net 기능 제어
| virtio-net 기능 | 호스트 TAP 대응 | 설명 |
|---|---|---|
VIRTIO_NET_F_CSUM | TUN_F_CSUM | 체크섬(Checksum) 오프로드 |
VIRTIO_NET_F_GSO | IFF_VNET_HDR | GSO 지원 |
VIRTIO_NET_F_HOST_TSO4 | TUN_F_TSO4 | 호스트 TCP Segmentation (IPv4) |
VIRTIO_NET_F_HOST_TSO6 | TUN_F_TSO6 | 호스트 TCP Segmentation (IPv6) |
VIRTIO_NET_F_HOST_UFO | TUN_F_UFO | 호스트 UDP Fragmentation |
VIRTIO_NET_F_MRG_RXBUF | vnet_hdr_sz 조정 | 병합 수신 버퍼 |
VIRTIO_NET_F_MQ | IFF_MULTI_QUEUE | 멀티큐 지원 |
virtio-net 데이터 경로 상세
게스트 내부의 virtio-net 드라이버가 패킷을 전송하면, virtqueue의 available ring에 디스크립터가 추가됩니다.
vhost-net 커널 스레드가 이를 감지하고, TAP 소켓으로 sendmsg()를 호출하여 패킷을 전달합니다.
/* Guest virtio-net TX 경로 (간략화) */
/* 1. Guest 커널: virtio-net 드라이버 */
/* drivers/net/virtio_net.c */
static netdev_tx_t start_xmit(struct sk_buff *skb,
struct net_device *dev)
{
struct send_queue *sq = &vi->sq[qnum];
/* virtio-net 헤더 설정 (GSO, checksum 정보) */
struct virtio_net_hdr_mrg_rxbuf *hdr;
hdr = skb_vnet_hdr(skb);
if (skb->ip_summed == CHECKSUM_PARTIAL)
hdr->hdr.flags = VIRTIO_NET_HDR_F_NEEDS_CSUM;
if (skb_is_gso(skb))
hdr->hdr.gso_type = VIRTIO_NET_HDR_GSO_TCPV4;
/* virtqueue에 버퍼 추가 */
virtqueue_add_outbuf(sq->vq, sg, num_sg, skb, GFP_ATOMIC);
virtqueue_kick(sq->vq);
/* → vhost-net eventfd 알림 → handle_tx() */
}
/* 2. Host 커널: vhost-net handle_tx() */
/* virtqueue에서 디스크립터 가져오기 → TAP sendmsg() */
/* 3. Host 커널: tun_sendmsg() */
static int tun_sendmsg(struct socket *sock,
struct msghdr *m,
size_t total_len)
{
struct tun_file *tfile =
container_of(sock, struct tun_file, socket);
struct tun_struct *tun = tun_get(tfile);
int ret;
/* tun_get_user()로 패킷을 네트워크 스택에 주입 */
ret = tun_get_user(tun, tfile, m->msg_control,
&m->msg_iter,
m->msg_flags & MSG_DONTWAIT,
m->msg_flags & MSG_MORE);
tun_put(tun);
return ret;
}
virtio-net 기능 협상 흐름
오프로딩
VNET_HDR (Virtio 헤더)
IFF_VNET_HDR 플래그를 설정하면 모든 패킷 앞에 virtio_net_hdr 구조체가 붙습니다.
이 헤더는 체크섬 오프로드와 GSO 정보를 전달하여 호스트와 게스트 사이의 불필요한 체크섬 계산과 세그먼테이션을 생략할 수 있게 합니다.
/* IFF_VNET_HDR 활성화 */
int tap_fd = tun_alloc("tap0", IFF_TAP | IFF_NO_PI | IFF_VNET_HDR);
/* virtio_net_hdr 구조체 (v1) */
struct virtio_net_hdr_v1 {
__u8 flags; /* VIRTIO_NET_HDR_F_NEEDS_CSUM 등 */
__u8 gso_type; /* VIRTIO_NET_HDR_GSO_TCPV4 등 */
__le16 hdr_len; /* Ethernet + IP + TCP/UDP 헤더 길이 */
__le16 gso_size; /* GSO 세그먼트 크기 */
__le16 csum_start; /* 체크섬 시작 오프셋 */
__le16 csum_offset; /* 체크섬 필드 오프셋 */
__le16 num_buffers; /* MRG_RXBUF에서 사용 */
};
/* vnet_hdr 크기 설정 */
int hdr_sz = sizeof(struct virtio_net_hdr_v1);
ioctl(tap_fd, TUNSETVNETHDRSZ, &hdr_sz);
/* GSO 플래그 */
#define VIRTIO_NET_HDR_GSO_NONE 0 /* GSO 없음 */
#define VIRTIO_NET_HDR_GSO_TCPV4 1 /* TCP/IPv4 세그먼테이션 */
#define VIRTIO_NET_HDR_GSO_UDP 3 /* UDP fragmentation */
#define VIRTIO_NET_HDR_GSO_TCPV6 4 /* TCP/IPv6 세그먼테이션 */
#define VIRTIO_NET_HDR_GSO_UDP_L4 5 /* UDP/L4 segmentation */
오프로드 설정
/* 오프로드 기능 활성화 */
unsigned int offload = TUN_F_CSUM | TUN_F_TSO4 | TUN_F_TSO6 | TUN_F_UFO;
ioctl(tun_fd, TUNSETOFFLOAD, offload);
#define TUN_F_CSUM 0x01 /* Checksum offload */
#define TUN_F_TSO4 0x02 /* TCP Segmentation Offload (IPv4) */
#define TUN_F_TSO6 0x04 /* TCP Segmentation Offload (IPv6) */
#define TUN_F_TSO_ECN 0x08 /* TSO + ECN 지원 */
#define TUN_F_UFO 0x10 /* UDP Fragmentation Offload */
#define TUN_F_USO4 0x20 /* UDP Segmentation Offload (IPv4) */
#define TUN_F_USO6 0x40 /* UDP Segmentation Offload (IPv6) */
# ethtool로 TUN/TAP 오프로드 상태 확인
$ ethtool -k tun0
Features for tun0:
tx-checksum-ipv4: on
tx-checksum-ipv6: on
tcp-segmentation-offload: on
generic-segmentation-offload: on
generic-receive-offload: on
# 오프로드 비활성화 (디버깅 시)
$ sudo ethtool -K tun0 tso off gso off gro off
오프로딩 커널 구현
TUNSETOFFLOAD ioctl은 tun_set_offload()를 호출하여
net_device의 features 비트마스크를 설정합니다.
이 비트마스크에 따라 네트워크 스택은 해당 기능을 TUN/TAP에 위임합니다.
/* tun_set_offload() — drivers/net/tun.c (간략화) */
static int tun_set_offload(struct tun_struct *tun,
unsigned long arg)
{
netdev_features_t features = 0;
netdev_features_t feature_mask = NETIF_F_ALL_CSUM;
if (arg & TUN_F_CSUM) {
features |= NETIF_F_HW_CSUM;
/* TX checksum offload 활성화 */
if (arg & (TUN_F_TSO4 | TUN_F_TSO6)) {
if (arg & TUN_F_TSO_ECN)
features |= NETIF_F_TSO_ECN;
if (arg & TUN_F_TSO4)
features |= NETIF_F_TSO;
if (arg & TUN_F_TSO6)
features |= NETIF_F_TSO6;
}
if (arg & TUN_F_UFO)
features |= NETIF_F_UFO;
if (arg & TUN_F_USO4)
features |= NETIF_F_GSO_UDP_L4;
if (arg & TUN_F_USO6)
features |= NETIF_F_GSO_UDP_L4;
}
tun->set_features = features;
tun->dev->wanted_features &= ~feature_mask;
tun->dev->wanted_features |= features;
netdev_update_features(tun->dev);
return 0;
}
GSO 처리 흐름
GSO(Generic Segmentation Offload) 활성화 시, 네트워크 스택은 최대 64KB의 슈퍼 패킷을
하나의 sk_buff로 만들어 TUN/TAP에 전달합니다.
virtio_net_hdr의 gso_type과 gso_size 필드를 통해
userspace가 실제 세그먼테이션을 수행하거나, 다시 커널에 위임할 수 있습니다.
ethtool -K tun0 gro off) 수신 측 성능이 저하될 수 있습니다.
BPF/eBPF 필터링
TUN/TAP는 두 가지 BPF 필터를 지원합니다: 클래식 BPF(cBPF)와 확장 BPF(eBPF).
클래식 BPF 필터
/* TAP에 BPF 필터 적용: ARP만 허용 */
struct sock_filter filter[] = {
BPF_STMT(BPF_LD | BPF_H | BPF_ABS, 12), /* EtherType 로드 */
BPF_JUMP(BPF_JMP | BPF_JEQ | BPF_K, 0x0806, 0, 1), /* ARP이면 통과 */
BPF_STMT(BPF_RET | BPF_K, 0xffffffff), /* 통과 */
BPF_STMT(BPF_RET | BPF_K, 0), /* 차단 */
};
struct sock_fprog fprog = {
.len = sizeof(filter) / sizeof(filter[0]),
.filter = filter,
};
ioctl(tap_fd, TUNATTACHFILTER, &fprog);
eBPF 필터
/* eBPF 필터 프로그램 부착 */
int prog_fd = bpf_load_program(BPF_PROG_TYPE_SOCKET_FILTER, ...);
ioctl(tap_fd, TUNSETFILTEREBPF, &prog_fd);
/* eBPF 스티어링 프로그램 부착 (멀티큐) */
int steer_fd = bpf_load_program(BPF_PROG_TYPE_SOCKET_FILTER, ...);
ioctl(tap_fd, TUNSETSTEEREBPF, &steer_fd);
- 필터 (TUNSETFILTEREBPF): 패킷 수락/거부를 결정 (반환값 0=거부, 그 외=수락)
- 스티어링 (TUNSETSTEEREBPF): 멀티큐에서 패킷이 전달될 큐 인덱스를 결정 (반환값=큐 번호)
패킷 전달 메커니즘
TUN/TAP의 패킷 전달은 두 가지 핵심 함수에 집중됩니다:
RX 경로(userspace → kernel)의 tun_get_user()와
TX 경로(kernel → userspace)의 tun_put_user()입니다.
이 두 함수의 내부 동작을 상세히 분석합니다.
tun_get_user() 상세 분석
/* tun_get_user() — Userspace write → Kernel RX (간략화)
* 이 함수는 userspace로부터 패킷을 받아 네트워크 스택으로 전달합니다. */
static ssize_t tun_get_user(struct tun_struct *tun,
struct tun_file *tfile,
void *msg_control,
struct iov_iter *from,
int noblock, bool more)
{
struct tun_pi pi = { 0, cpu_to_be16(ETH_P_IP) };
struct sk_buff *skb;
size_t total_len = iov_iter_count(from);
size_t len = total_len;
int good_linear = SKB_MAX_HEAD(NET_IP_ALIGN);
/* Step 1: PI 헤더 처리 (IFF_NO_PI가 없으면) */
if (!(tun->flags & IFF_NO_PI)) {
if (len < sizeof(pi))
return -EINVAL;
copy_from_iter(&pi, sizeof(pi), from);
len -= sizeof(pi);
}
/* Step 2: VNET_HDR 처리 */
if (tun->flags & IFF_VNET_HDR) {
struct virtio_net_hdr gso = { 0 };
copy_from_iter(&gso, tun->vnet_hdr_sz, from);
len -= tun->vnet_hdr_sz;
/* GSO 정보와 checksum 오프셋 검증 */
}
/* Step 3: sk_buff 할당 */
skb = tun_alloc_skb(tfile, good_linear + NET_IP_ALIGN,
len, noblock);
if (IS_ERR(skb))
return PTR_ERR(skb);
/* Step 4: 데이터 복사 (userspace → skb) */
skb_put(skb, len);
skb_copy_datagram_from_iter(skb, 0, from, len);
/* Step 5: 프로토콜 설정 */
switch (tun->flags & TUN_TYPE_MASK) {
case IFF_TUN:
skb->protocol = pi.proto;
skb->dev = tun->dev;
break;
case IFF_TAP:
skb->protocol = eth_type_trans(skb, tun->dev);
break;
}
/* Step 6: XDP 프로그램 실행 (있는 경우) */
/* ... (위의 XDP 커널 내부 처리 흐름 참조) ... */
/* Step 7: 네트워크 스택으로 전달 */
if (tfile->napi_enabled) {
napi_gro_receive(&tfile->napi, skb);
} else {
netif_rx_ni(skb);
}
/* 통계 갱신 */
dev_sw_netstats_rx_add(tun->dev, len);
return total_len;
}
tun_put_user() 상세 분석
/* tun_put_user() — Kernel TX → Userspace read (간략화)
* 커널에서 패킷을 받아 userspace로 전달합니다. */
static ssize_t tun_put_user(struct tun_struct *tun,
struct tun_file *tfile,
struct sk_buff *skb,
struct iov_iter *iter)
{
struct tun_pi pi = { 0, skb->protocol };
ssize_t total;
/* PI 헤더 전달 */
if (!(tun->flags & IFF_NO_PI)) {
if (iov_iter_count(iter) < sizeof(pi))
return -EINVAL;
copy_to_iter(&pi, sizeof(pi), iter);
total = sizeof(pi);
}
/* VNET_HDR 전달 */
if (tun->flags & IFF_VNET_HDR) {
struct virtio_net_hdr gso;
tun_skb_to_vnet_hdr(tun, skb, &gso);
copy_to_iter(&gso, tun->vnet_hdr_sz, iter);
total += tun->vnet_hdr_sz;
}
/* 패킷 데이터 전달 */
total += skb_copy_datagram_iter(skb, 0, iter, skb->len);
/* 통계 갱신 */
dev_sw_netstats_tx_add(tun->dev, 1, skb->len);
return total;
}
poll/epoll 통합
TUN/TAP의 파일 디스크립터는 poll()/epoll()을 지원합니다.
tun_chr_poll()은 tun_file의 소켓 수신 큐에 패킷이 있으면 POLLIN을,
송신 큐에 공간이 있으면 POLLOUT을 반환합니다.
/* 고성능 이벤트 루프 패턴 (epoll) */
#include <sys/epoll.h>
int epfd = epoll_create1(0);
struct epoll_event ev;
/* 멀티큐의 각 fd를 epoll에 등록 */
for (int i = 0; i < NUM_QUEUES; i++) {
ev.events = EPOLLIN | EPOLLET; /* Edge-triggered */
ev.data.fd = fds[i];
epoll_ctl(epfd, EPOLL_CTL_ADD, fds[i], &ev);
}
/* 이벤트 루프 */
struct epoll_event events[NUM_QUEUES];
while (1) {
int nfds = epoll_wait(epfd, events, NUM_QUEUES, -1);
for (int i = 0; i < nfds; i++) {
if (events[i].events & EPOLLIN) {
unsigned char buf[65535];
ssize_t n;
/* Edge-triggered: drain all packets */
while ((n = read(events[i].data.fd,
buf, sizeof(buf))) > 0) {
process_packet(buf, n);
}
}
}
}
/* splice() 제로카피 전달 (Linux 5.1+) */
/* TUN/TAP에서 소켓으로 직접 전달 */
splice(tun_fd, NULL, pipe_fds[1], NULL, 65536, SPLICE_F_NONBLOCK);
splice(pipe_fds[0], NULL, sock_fd, NULL, 65536, SPLICE_F_NONBLOCK);
- epoll + Edge-triggered: Level-triggered보다 시스템 콜 횟수 감소
- readv/writev: 여러 버퍼를 한 번에 전달하여 시스템 콜 오버헤드 감소
- NAPI 모드: 고부하 시 배치 처리로 인터럽트 오버헤드 감소
- busy-poll:
sysctl net.core.busy_poll=50으로 저지연 달성
활용 사례
VPN (Virtual Private Network)
/* 간단한 VPN 터널 구현 패턴 */
int tun_fd = tun_alloc("tun0", IFF_TUN | IFF_NO_PI);
int udp_sock = socket(AF_INET, SOCK_DGRAM, 0);
/* poll 기반 이벤트 루프 */
struct pollfd pfds[2] = {
{ .fd = tun_fd, .events = POLLIN },
{ .fd = udp_sock, .events = POLLIN },
};
while (1) {
poll(pfds, 2, -1);
if (pfds[0].revents & POLLIN) {
/* TUN -> 암호화 -> UDP 전송 */
ssize_t n = read(tun_fd, plaintext, sizeof(plaintext));
size_t enc_len = encrypt(plaintext, n, ciphertext);
sendto(udp_sock, ciphertext, enc_len, 0,
(struct sockaddr *)&peer, sizeof(peer));
}
if (pfds[1].revents & POLLIN) {
/* UDP 수신 -> 복호화 -> TUN 주입 */
ssize_t n = recvfrom(udp_sock, ciphertext, sizeof(ciphertext),
0, NULL, NULL);
size_t dec_len = decrypt(ciphertext, n, plaintext);
write(tun_fd, plaintext, dec_len);
}
}
QEMU/KVM 가상 머신
# 최적화된 QEMU 네트워크 설정
$ sudo ip tuntap add dev tap0 mode tap user $USER
$ sudo ip link set tap0 up
$ sudo ip link set tap0 master br0
# QEMU 실행 (vhost-net + 멀티큐)
$ qemu-system-x86_64 \
-netdev tap,id=net0,ifname=tap0,script=no,downscript=no,\
vhost=on,queues=4 \
-device virtio-net-pci,netdev=net0,mq=on,vectors=10 \
-smp 4 ...
네트워크 에뮬레이션
# TUN으로 패킷 지연/손실 시뮬레이션
$ sudo ip tuntap add dev tun0 mode tun
$ sudo ip link set tun0 up
$ sudo ip addr add 10.0.0.1/24 dev tun0
# tc (Traffic Control)로 지연 추가
$ sudo tc qdisc add dev tun0 root netem delay 100ms loss 5%
# 대역폭 제한
$ sudo tc qdisc add dev tun0 root tbf rate 10mbit burst 32kbit latency 400ms
컨테이너 네트워크
Docker와 Podman은 컨테이너 네트워크를 위해 TUN/TAP 대신 주로 veth pair를 사용하지만, 특수한 경우(VPN 컨테이너, 네트워크 테스트 도구 등)에서 TUN/TAP을 직접 사용합니다.
# Docker 컨테이너에서 TUN 사용
$ docker run -it --cap-add=NET_ADMIN --device /dev/net/tun \
alpine sh -c '
apk add iproute2
ip tuntap add dev tun0 mode tun
ip addr add 10.0.0.1/24 dev tun0
ip link set tun0 up
ip link show tun0
'
# Podman: rootless TUN/TAP (user namespace 내)
$ podman run --cap-add=NET_ADMIN --device /dev/net/tun \
--userns=keep-id alpine ip tuntap add dev tun0 mode tun
# Kubernetes Pod에서 TUN/TAP 사용 (VPN sidecar 패턴)
# Pod spec에 다음 추가:
# securityContext:
# capabilities:
# add: ["NET_ADMIN"]
# volumeMounts:
# - name: dev-net-tun
# mountPath: /dev/net/tun
# volumes:
# - name: dev-net-tun
# hostPath:
# path: /dev/net/tun
# type: CharDevice
투명 프록시/패킷 캡처
/* TUN을 이용한 투명 프록시 구현 패턴 */
int tun_fd = tun_alloc("tun_proxy", IFF_TUN | IFF_NO_PI);
/* 정책 라우팅으로 특정 트래픽을 TUN으로 유도 */
/* ip rule add fwmark 1 table 100 */
/* ip route add default dev tun_proxy table 100 */
/* iptables -t mangle -A PREROUTING -p tcp --dport 80 -j MARK --set-mark 1 */
while (1) {
unsigned char buf[65535];
ssize_t n = read(tun_fd, buf, sizeof(buf));
/* IP 패킷 분석 */
struct iphdr *iph = (struct iphdr *)buf;
/* HTTP 요청 가로채기, 수정, 또는 로깅 */
inspect_and_forward(buf, n);
/* 수정된 패킷을 다시 TUN에 주입 */
write(tun_fd, modified_buf, modified_len);
}
성능 최적화
성능 관련 파라미터
| 파라미터 | 경로/설정 | 기본값 | 권장값 | 설명 |
|---|---|---|---|---|
| 송신 버퍼 | TUNSETSNDBUF | 자동 | 워크로드 의존 | 큐 깊이 제어 |
| TX 큐 길이 | ip link set txqueuelen | 500 | 1000~5000 | 대량 전송 시 증가 |
| 큐 수 | IFF_MULTI_QUEUE | 1 | CPU 수 | CPU 병렬 처리 |
| TSO/GSO | TUNSETOFFLOAD | off | on | 대량 전송 최적화 |
| VNET_HDR | IFF_VNET_HDR | off | on (VM) | 오프로드 정보 전달 |
| NAPI | IFF_NAPI | off | on (고부하) | 배치 수신 처리 |
시스템 레벨 튜닝
# TX 큐 길이 증가 (패킷 드롭 방지)
$ sudo ip link set tun0 txqueuelen 5000
# 소켓 버퍼 크기 증가 (sysctl)
$ sudo sysctl -w net.core.rmem_max=16777216
$ sudo sysctl -w net.core.wmem_max=16777216
# backlog 큐 증가
$ sudo sysctl -w net.core.netdev_max_backlog=5000
# vhost-net의 경우 ksoftirqd CPU 고정
$ taskset -cp 0-3 $(pgrep vhost)
# 인터럽트 어피니티 (물리 NIC)
$ echo 2 > /proc/irq/27/smp_affinity
ip -s link show tun0에서 TX/RX dropped가 증가하면 큐 길이 부족/proc/net/softnet_stat에서 time_squeeze(3번째 열)가 증가하면 CPU 부족perf top에서tun_net_xmit이 상위에 있으면 TAP 자체가 병목
성능 벤치마크 방법
# iperf3를 이용한 TUN/TAP 처리량 측정
# 1. TUN 인터페이스 생성 및 설정
$ sudo ip tuntap add dev tun0 mode tun
$ sudo ip addr add 10.0.0.1/24 dev tun0
$ sudo ip link set tun0 up
$ sudo ip link set tun0 txqueuelen 5000
# 2. 네임스페이스에서 반대편 설정
$ sudo ip netns add test
$ sudo ip link set tun0 netns test
$ sudo ip netns exec test ip addr add 10.0.0.2/24 dev tun0
$ sudo ip netns exec test ip link set tun0 up
# 3. iperf3 서버 실행 (네임스페이스 내)
$ sudo ip netns exec test iperf3 -s &
# 4. 클라이언트 실행
$ iperf3 -c 10.0.0.2 -t 30 -P 4
# 5. 멀티큐 환경에서 CPU별 대역폭 측정
$ mpstat -P ALL 1
# %soft 열이 높은 CPU 확인 = 패킷 처리 CPU
# 6. 패킷 처리율 측정 (pktgen 활용)
$ sudo modprobe pktgen
$ echo "add_device tap0" > /proc/net/pktgen/kpktgend_0
$ echo "pkt_size 64" > /proc/net/pktgen/tap0
$ echo "count 10000000" > /proc/net/pktgen/tap0
$ echo "start" > /proc/net/pktgen/pgctrl
$ cat /proc/net/pktgen/tap0
고급 성능 튜닝
| 튜닝 항목 | 명령/설정 | 효과 | 적용 시나리오 |
|---|---|---|---|
| Busy polling | sysctl net.core.busy_poll=50 | 지연 시간 -30~50% | 저지연 요구 |
| RPS (Receive Packet Steering) | echo f > /sys/class/net/tun0/queues/rx-0/rps_cpus | 멀티코어 분산 | 단일큐 모드 |
| XPS (Transmit Packet Steering) | echo 1 > /sys/class/net/tun0/queues/tx-0/xps_cpus | TX CPU 고정 | 캐시(Cache) 효율 |
| GRO flush timeout | sysctl net.core.gro_flush_timeout=2000 | GRO 배치 크기 증가 | 처리량 최적화 |
| NAPI weight | 드라이버 코드 수정 | 배치 크기 조정 | 고부하 서버 |
| SO_BUSY_POLL | setsockopt(fd, SOL_SOCKET, SO_BUSY_POLL, &val, ...) | 소켓별 busy-poll | 특정 fd만 저지연 |
# CPU 어피니티 기반 최적화 (vhost-net + TAP)
# vhost 워커 스레드 CPU 고정
$ VHOST_PID=$(pgrep -f "vhost-$(pgrep qemu)")
$ taskset -cp 2-3 $VHOST_PID
# 물리 NIC IRQ 어피니티 분리
$ echo 4 > /proc/irq/28/smp_affinity # CPU 2
$ echo 8 > /proc/irq/29/smp_affinity # CPU 3
# QEMU vCPU 고정
$ taskset -cp 0-1 $QEMU_PID
# NUMA 인지 설정 (NUMA 시스템)
$ numactl --cpubind=0 --membind=0 qemu-system-x86_64 ...
# 결과 확인: 처리량과 지연 시간 측정
$ sar -n DEV 1 | grep tap0
tap0 15:30:01 1000000 1000000 125000 125000 0.00 0.00 0.00
디버깅
통계 확인
# 인터페이스 통계
$ ip -s link show tun0
5: tun0: <POINTOPOINT,MULTICAST,NOARP,UP,LOWER_UP> mtu 1500 qdisc fq_codel
link/none
RX: bytes packets errors dropped overrun mcast
123456 789 0 0 0 0
TX: bytes packets errors dropped carrier collsns
654321 987 0 0 0 0
# 64비트 통계 (정확한 카운터)
$ ip -s -s link show tun0
# tcpdump로 패킷 캡처
$ sudo tcpdump -i tun0 -n -v
# TUN/TAP 디바이스 목록
$ ip tuntap show
tun0: tun persist
tap0: tap persist user 1000
Kernel Tracing
# TUN/TAP 드라이버 함수 추적
# cd /sys/kernel/debug/tracing
# echo 'tun_*' > set_ftrace_filter
# echo function > current_tracer
# echo 1 > tracing_on
# cat trace
# bpftrace로 패킷 크기 히스토그램
$ sudo bpftrace -e 'kprobe:tun_get_user { @bytes = hist(arg2); }'
# perf로 핫 함수 분석
$ sudo perf record -g -p $(pgrep qemu) -- sleep 10
$ sudo perf report
고급 추적 기법
# bpftrace: TUN/TAP 패킷 크기 분포 히스토그램
$ sudo bpftrace -e '
kprobe:tun_get_user {
@rx_bytes = hist(arg4); /* iov_iter count */
}
kprobe:tun_put_user {
@tx_bytes = hist(arg3); /* skb length */
}
interval:s:10 { exit(); }
'
# bpftrace: 큐별 패킷 분배 확인
$ sudo bpftrace -e '
kprobe:tun_select_queue {
@queue = hist(retval);
}
interval:s:5 { exit(); }
'
# bpftrace: tun_net_xmit() 지연 시간 측정
$ sudo bpftrace -e '
kprobe:tun_net_xmit { @start[tid] = nsecs; }
kretprobe:tun_net_xmit /@start[tid]/ {
@latency_ns = hist(nsecs - @start[tid]);
delete(@start[tid]);
}
interval:s:10 { exit(); }
'
# perf probe: 동적 트레이스포인트 추가
$ sudo perf probe -a 'tun_get_user len=total_len:u64'
$ sudo perf record -e probe:tun_get_user -aR sleep 5
$ sudo perf script
자주 발생하는 문제
| 증상 | 원인 | 해결 |
|---|---|---|
| 패킷이 안 읽힘 | 인터페이스 DOWN 상태 | ip link set tun0 up |
| write 후 패킷 사라짐 | 라우팅 미설정 | ip route 확인 |
| EPERM (권한 거부) | CAP_NET_ADMIN 없음 | root 또는 capabilities 설정 |
| EIO 에러 | 인터페이스 삭제됨 | fd 재생성 필요 |
| 패킷 손실 (TX dropped) | 큐 오버플로 | txqueuelen 증가 |
| 성능 저하 | 단일 큐 모드 | 멀티큐 + vhost-net 활성화 |
| checksum 오류 | 오프로드 불일치 | ethtool -K 확인 |
| ARP 실패 (TAP) | 브리지 미설정 또는 promisc off | ip link set br0 promisc on |
| vhost-net 미동작 | 모듈 미로드 | modprobe vhost_net |
| 멀티큐 EINVAL | 커널 3.8 미만 | 커널 업그레이드 |
| XDP 프로그램 미동작 | BPF verifier 거부 | bpftool prog show으로 상태 확인 |
디버깅 도구 요약
| 도구 | 용도 | 명령 예시 |
|---|---|---|
ip -s link | 인터페이스 통계 (패킷/바이트/에러) | ip -s link show tun0 |
tcpdump | 패킷 캡처 및 분석 | tcpdump -i tun0 -n -vv |
ethtool | 오프로드 상태/드라이버 정보 | ethtool -k tun0 |
bpftool | BPF/XDP 프로그램 상태 | bpftool prog show |
ftrace | 커널 함수 추적 | echo 'tun_*' > set_ftrace_filter |
bpftrace | 동적 커널 프로빙 | bpftrace -e 'kprobe:tun_get_user { ... }' |
perf | 성능 프로파일링(Profiling) | perf top -g -p $(pgrep qemu) |
ss/netstat | 소켓 상태 | ss -xp | grep tun |
lsof | 열린 fd 확인 | lsof /dev/net/tun |
strace | 시스템 콜 추적 | strace -e ioctl,read,write -p PID |
커널 설정
# 필수
CONFIG_TUN=y # TUN/TAP 드라이버
# 관련 가상 네트워크 디바이스
CONFIG_VETH=y # Virtual Ethernet (veth pair)
CONFIG_MACVLAN=y # MACVLAN
CONFIG_MACVTAP=y # MACVTAP (TAP + MACVLAN)
CONFIG_IPVLAN=y # IPVLAN
# vhost-net (VM 네트워킹 최적화)
CONFIG_VHOST_NET=m # vhost-net 커널 모듈
CONFIG_VHOST=y # vhost 프레임워크
# XDP 지원
CONFIG_BPF=y
CONFIG_BPF_SYSCALL=y
CONFIG_XDP_SOCKETS=y
# 브리지 (TAP 외부 연결)
CONFIG_BRIDGE=y
CONFIG_BRIDGE_NETFILTER=y
sysfs 인터페이스
| 경로 | 읽기/쓰기 | 설명 |
|---|---|---|
/sys/class/net/tun0/type | R | 인터페이스 타입 (ARPHRD_NONE=65534, ARPHRD_ETHER=1) |
/sys/class/net/tun0/mtu | R/W | MTU 크기 |
/sys/class/net/tun0/flags | R | 인터페이스 플래그 (IFF_UP 등) |
/sys/class/net/tun0/operstate | R | 동작 상태 (up/down/unknown) |
/sys/class/net/tun0/tx_queue_len | R/W | TX 큐 길이 |
/sys/class/net/tun0/queues/ | R | 큐별 상태 디렉터리 |
/sys/class/net/tun0/statistics/ | R | 패킷/바이트 통계 |
/sys/class/net/tun0/tun_flags | R | TUN/TAP 전용 플래그 (IFF_TUN 등) |
/sys/class/net/tun0/owner | R | 디바이스 소유자 UID (-1=미설정) |
/sys/class/net/tun0/group | R | 디바이스 소유자 GID (-1=미설정) |
# sysfs를 통한 TUN/TAP 디바이스 정보 확인
$ cat /sys/class/net/tun0/tun_flags
0x1001 # IFF_TUN | IFF_NO_PI
$ cat /sys/class/net/tun0/owner
1000
$ ls /sys/class/net/tap0/queues/
rx-0 rx-1 rx-2 rx-3 tx-0 tx-1 tx-2 tx-3
# 큐별 통계
$ cat /sys/class/net/tap0/queues/tx-0/tx_bytes
1234567890
지원 기능 확인
/* 런타임에서 커널이 지원하는 TUN/TAP 기능 확인 */
unsigned int features;
int fd = open("/dev/net/tun", O_RDWR);
ioctl(fd, TUNGETFEATURES, &features);
printf("TUN features: 0x%x\n", features);
if (features & IFF_MULTI_QUEUE)
printf(" Multi-queue supported\n");
if (features & IFF_VNET_HDR)
printf(" VNET_HDR supported\n");
if (features & IFF_NAPI)
printf(" NAPI supported\n");
보안 고려사항
| 위협 | 설명 | 대책 |
|---|---|---|
| 무단 인터페이스 생성 | 권한 없는 사용자가 가상 NIC 생성 | user/group 제한, CAP_NET_ADMIN |
| 패킷 인젝션 | 악의적 패킷을 네트워크 스택에 주입 | BPF 필터, netfilter 규칙 |
| 정보 유출 | TUN/TAP을 통한 트래픽 감청 | fd 전달 제한, 네임스페이스(Namespace) 격리 |
| DoS | 대량 패킷으로 커널 큐 포화 | txqueuelen 제한, rate limiting |
| 권한 상승 | persistent 디바이스 소유권 탈취 | 소유자 검증, seccomp 정책 |
| fd 누출 | TUN/TAP fd가 자식 프로세스에 상속 | O_CLOEXEC 플래그, SOCK_CLOEXEC |
| VLAN 우회 | TAP으로 VLAN 태그 조작 | VLAN 필터링, 브리지 정책 |
LSM(Linux Security Module) 통합
TUN/TAP는 LSM 프레임워크와 통합되어, SELinux/AppArmor 등이 TUN/TAP 디바이스 접근을 제어할 수 있습니다.
핵심 보안 훅은 security_tun_dev_* 계열 함수입니다.
/* TUN/TAP LSM 보안 훅 — include/linux/security.h */
/* 디바이스 생성 권한 확인 */
int security_tun_dev_create(void);
/* 큐 연결 권한 확인 */
int security_tun_dev_attach_queue(void *security);
/* 네트워크 인터페이스 연결 시 보안 컨텍스트 설정 */
int security_tun_dev_attach(struct sock *sk,
void *security);
/* persistent 디바이스 열기 권한 확인 */
int security_tun_dev_open(void *security);
/* tun_chr_open()에서 호출되는 예시 */
static int tun_chr_open(struct inode *inode,
struct file *file)
{
int err;
err = security_tun_dev_create();
if (err < 0)
return err;
...
}
컨테이너 보안 모범 사례
# Docker에서 TUN/TAP 안전하게 사용하기
# 1. 네트워크 네임스페이스와 함께 사용 (기본)
$ docker run --cap-add=NET_ADMIN --device /dev/net/tun myapp
# 2. seccomp 프로파일로 위험한 ioctl 제한
$ cat seccomp-tuntap.json
{
"defaultAction": "SCMP_ACT_ALLOW",
"syscalls": [{
"names": ["ioctl"],
"action": "SCMP_ACT_ERRNO",
"args": [{"index": 1, "value": 1074025680, "op": "SCMP_CMP_EQ"}]
}]
}
$ docker run --security-opt seccomp=seccomp-tuntap.json myapp
# 3. AppArmor 프로파일 (TUN/TAP 접근 제한)
# /etc/apparmor.d/docker-tuntap
# profile docker-tuntap flags=(attach_disconnected) {
# /dev/net/tun rw,
# deny /dev/net/tun w, # 쓰기 차단 (읽기만 허용)
# }
보안 감사 및 모니터링
# auditd로 TUN/TAP 접근 감사
$ sudo auditctl -w /dev/net/tun -p rwxa -k tuntap_access
# TUN/TAP 디바이스 생성/삭제 추적
$ sudo bpftrace -e '
kprobe:tun_set_iff {
printf("pid=%d comm=%s tun_set_iff called\n", pid, comm);
}
kprobe:tun_chr_close {
printf("pid=%d comm=%s tun close\n", pid, comm);
}'
# 활성 TUN/TAP 인터페이스와 소유자 확인
$ ip tuntap show
tun0: tun persist
tap0: tap persist user 1000 group 36
# 열린 TUN/TAP fd 확인
$ lsof /dev/net/tun
COMMAND PID USER FD TYPE DEVICE SIZE/OFF NODE NAME
qemu-sys 1234 root 25u CHR 10,200 0t0 123 /dev/net/tun
openvpn 5678 root 6u CHR 10,200 0t0 123 /dev/net/tun
/dev/net/tun을 마운트(Mount)하면 컨테이너가 호스트 네트워크 스택에 패킷을 주입할 수 있습니다.
반드시 네트워크 네임스페이스와 함께 사용하고, 필요한 최소 권한만 부여하세요.
CAP_NET_ADMIN 대신 --device /dev/net/tun만 부여하고 persistent 디바이스를 사전에 생성하는 것이 더 안전합니다.
- 운영 환경에서는
/dev/net/tun의 권한을0660으로 유지하고,kvm그룹에만 접근 허용 - persistent 디바이스 사용 시
TUNSETOWNER/TUNSETGROUP으로 소유자 명시 - 컨테이너에서는 네트워크 네임스페이스 +
seccomp조합 사용 - BPF/eBPF 필터로 허용 패킷 유형 제한 (예: ARP + IPv4/IPv6만 허용)
auditd또는bpftrace로 TUN/TAP 접근 감사 로그 수집
공식 문서 기준 최신 운영 포인트
2026년 4월 21일 기준 커널 공식 문서를 보면, TUN/TAP 운영의 핵심은 여전히 유저스페이스와 커널이 공유하는 장치 핸들 모델에 있습니다. 공식 문서는 /dev/net/tun을 열고 TUNSETIFF로 장치를 만들거나 연결하며, 사용하지 않는다고 해서 노드를 없앨 필요는 없다고 설명합니다. 즉 TUN/TAP은 일반 netdev처럼 "장치가 먼저 있고 프로세스가 붙는" 모델이 아니라, 파일 디스크립터(File Descriptor)와 장치 수명, 권한이 함께 움직이는 인터페이스로 봐야 합니다.
권한은 장치 노드보다 ioctl 경로를 기준으로 봐야 합니다
공식 문서는 /dev/net/tun 자체는 넓게 열려 있어도 되고, 실제 장치 생성이나 기존 장치 연결은 CAP_NET_ADMIN이 없는 사용자가 임의로 할 수 없다고 설명합니다. 따라서 최근 운영에서는 단순 파일 권한보다 어떤 프로세스가 어떤 장치를 어떤 소유자/그룹으로 여는지를 더 중요하게 봐야 합니다. 권한 설계가 애매하면 "장치 파일은 열리는데 인터페이스 생성이 안 되는" 식의 혼란이 자주 생깁니다.
고처리량 경로는 multiqueue 전제를 먼저 확인합니다
공식 문서는 multiqueue tuntap 장치를 만들 때 IFF_MULTI_QUEUE로 같은 이름의 장치에 여러 큐를 붙이는 방식을 예제로 보여줍니다. 이는 최근 TUN/TAP 성능 논의에서 단순 read()/write() 루프보다 큐를 몇 개 열었는가가 먼저 확인할 항목이라는 뜻입니다. 고속 VPN, 사용자 공간 스위치, 가상화 백엔드에서는 single queue인지 multiqueue인지에 따라 CPU 분산과 지연 특성이 크게 달라집니다.
IFF_MULTI_QUEUE 기반 multiqueue인지부터 확인하세요.
처리량 한계가 여기서 먼저 결정되는 경우가 많습니다.
- TUN/TAP은 netdev이면서 동시에 fd 기반 인터페이스라는 점을 운영 문서에 명시합니다.
- 권한 문제는
/dev/net/tun접근과TUNSETIFF성공을 분리해서 봅니다. - 고부하 경로는 multiqueue 여부와 큐별 worker 배치를 같이 점검합니다.
- persistent 장치는 편리하지만, 누가 수명과 소유권을 관리하는지 더 엄격히 정해야 합니다.
참고자료
- Linux TUN/TAP Documentation
- TUN/TAP Interface Tutorial
drivers/net/tun.c— TUN/TAP 드라이버 소스include/uapi/linux/if_tun.h— TUN/TAP API 정의drivers/vhost/net.c— vhost-net 구현- OpenVPN — TUN/TAP 기반 VPN
- QEMU Networking Documentation
- LWN: Kernel TUN/TAP multiqueue
- KVM virtio Documentation
drivers/vhost/net.c— vhost-net 커널 구현drivers/net/macvtap.c— macvtap 드라이버 소스include/linux/virtio_net.h— virtio-net 헤더 정의- LWN: XDP for TUN/TAP
- Red Hat: virtio Networking First Steps
- tun(4) man page — TUN/TAP 디바이스의 공식 매뉴얼 페이지(Page)입니다
- 커널 소스: drivers/net/tun.c — TUN/TAP 드라이버 커널 소스 코드입니다
- 커널 소스: include/uapi/linux/if_tun.h — TUN/TAP 사용자 공간(User Space) API 헤더 파일입니다
- kernel.org: TUN/TAP device driver — 커널 공식 TUN/TAP 문서입니다
- LWN: Multiqueue networking for tun — TUN 디바이스의 멀티큐 지원에 대한 LWN 문서입니다
- LWN: virtio 1.0 — virtio 표준과 virtio-net의 발전을 다루는 LWN 문서입니다
- 네트워크 스택 — 패킷 처리 흐름
- Linux Bridge — TAP 브리지 통합
- 가상화 (KVM) — QEMU 네트워킹
- BPF/XDP — 고급 패킷 필터링
- VPP (FD.io) — 고성능 유저스페이스 패킷 처리
- Network Device 드라이버 — net_device 내부
컨테이너 네트워킹에서의 TAP
TAP 디바이스는 마이크로VM 기반 컨테이너 런타임에서 핵심 네트워크 인터페이스로 사용됩니다. Kata Containers, Firecracker, gVisor 같은 경량 VM 런타임은 각 컨테이너(또는 Pod)에 전용 TAP 인터페이스를 생성하여 하드웨어 수준의 네트워크 격리를 제공합니다. CNI(Container Network Interface) 플러그인은 TAP 디바이스의 생성, IP 할당, 라우팅 설정을 자동화합니다.
Kata Containers TAP 설정
Kata Containers는 각 Pod에 대해 경량 VM을 생성하고,
TAP 디바이스를 통해 호스트 네트워크와 연결합니다.
kata-runtime은 Pod의 네트워크 네임스페이스 내에 TAP을 생성하고,
게스트 VM의 virtio-net 드라이버와 vhost-net을 통해 연결합니다.
# Kata Containers의 TAP 디바이스 확인
$ kubectl exec -it kata-pod -- ip link show
1: lo: <LOOPBACK,UP> ...
2: eth0: <BROADCAST,MULTICAST,UP> ... # Guest 내부 virtio-net
# 호스트에서 TAP 확인 (Pod netns 내부)
$ nsenter -t $(crictl inspect $CID | jq .info.pid) -n ip link show
1: lo: ...
2: tap0_kata: <BROADCAST,MULTICAST,UP> ... # TAP (microVM 연결)
3: veth_kata@if4: ... # veth (호스트 연결)
# Kata 네트워크 설정 확인
$ cat /etc/kata-containers/configuration.toml | grep -A5 '\[network\]'
[hypervisor.qemu]
default_bridges = 1
enable_vhost_user = true
vhost_user_store_path = "/var/run/kata-containers/vhost-user"
Firecracker TAP 설정
Firecracker는 AWS Lambda와 Fargate의 기반이 되는 경량 VMM으로, 각 마이크로VM에 TAP 디바이스를 직접 연결합니다. Firecracker는 자체 virtio-net 에뮬레이션을 사용하며, TAP fd를 직접 전달받아 패킷을 처리합니다.
# Firecracker용 TAP 생성
$ sudo ip tuntap add dev fc-tap0 mode tap
$ sudo ip addr add 172.16.0.1/24 dev fc-tap0
$ sudo ip link set fc-tap0 up
# iptables NAT 설정 (마이크로VM 외부 통신)
$ sudo iptables -t nat -A POSTROUTING -o eth0 \
-s 172.16.0.0/24 -j MASQUERADE
$ sudo iptables -A FORWARD -i fc-tap0 -o eth0 -j ACCEPT
$ sudo iptables -A FORWARD -i eth0 -o fc-tap0 \
-m state --state RELATED,ESTABLISHED -j ACCEPT
# Firecracker API로 네트워크 설정
$ curl --unix-socket /tmp/firecracker.socket -X PUT \
http://localhost/network-interfaces/eth0 \
-H "Content-Type: application/json" \
-d '{
"iface_id": "eth0",
"host_dev_name": "fc-tap0",
"guest_mac": "AA:FC:00:00:00:01"
}'
CNI 플러그인과 TAP 통합
| CNI 플러그인 | TAP 연결 방식 | 사용 런타임 | 특징 |
|---|---|---|---|
bridge | TAP → veth → Bridge | Kata, Firecracker | 표준 L2 브리지 연결 |
ptp | TAP → veth pair (point-to-point) | Kata | 브리지 없이 직접 라우팅 |
tc-redirect-peer | TAP → tc ingress/egress | Cilium + Kata | eBPF 기반 고성능 경로 |
macvlan | macvtap (TAP+MACVLAN) | Kata (passthrough) | 브리지 오버헤드 제거 |
host-device | 물리 NIC 직접 할당 | SR-IOV + Kata | 최대 성능 (VFIO) |
/* CNI 플러그인의 TAP 생성 로직 패턴 (의사 코드) */
/* 1. Pod 네트워크 네임스페이스에서 TAP 생성 */
netns_enter(pod_netns);
int tap_fd = tun_alloc("tap_kata0",
IFF_TAP | IFF_NO_PI | IFF_VNET_HDR | IFF_MULTI_QUEUE);
/* 2. vhost-net 연결 (Kata Containers) */
int vhost_fd = open("/dev/vhost-net", O_RDWR);
ioctl(vhost_fd, VHOST_SET_OWNER, 0);
ioctl(vhost_fd, VHOST_NET_SET_BACKEND, &backend);
/* 3. TAP에 오프로드 설정 */
unsigned int offload = TUN_F_CSUM | TUN_F_TSO4 | TUN_F_TSO6;
ioctl(tap_fd, TUNSETOFFLOAD, offload);
/* 4. veth pair 생성 (호스트 연결용) */
/* ip link add veth_pod type veth peer name veth_host */
/* ip link set veth_host netns host_netns */
/* 5. 브리지 또는 tc-redirect로 TAP-veth 연결 */
/* tc filter add dev tap_kata0 ingress bpf da obj redirect.o */
- vhost-net 활성화:
enable_vhost_user = true로 커널 경로 사용 - 멀티큐: vCPU 수에 맞춰 TAP 큐 수 설정
- TSO/GSO 활성화: 대량 전송 시 시스템 콜 횟수 감소
- tc-redirect-peer CNI: eBPF 기반 경로로 브리지 오버헤드 제거
Persistent TUN/TAP
Persistent TUN/TAP는 파일 디스크립터를 닫아도 네트워크 인터페이스가 유지되는 모드입니다.
TUNSETPERSIST ioctl 또는 ip tuntap 명령으로 설정하며,
서비스 재시작(Reboot) 시에도 인터페이스와 IP 설정을 유지할 수 있어 운영 환경에서 중요합니다.
Persistent 모드 라이프사이클
Persistent 커널 구현
/* TUNSETPERSIST ioctl 처리 — drivers/net/tun.c */
case TUNSETPERSIST:
/* 생성 시: persistent 플래그 설정 */
if (arg && !(tun->flags & IFF_PERSIST)) {
tun->flags |= IFF_PERSIST;
/* net_device 참조 카운트 증가 — close 시에도 유지 */
dev_hold(tun->dev);
tun_dev_set_persist(tun->dev);
}
/* 해제 시: persistent 플래그 제거 */
if (!arg && (tun->flags & IFF_PERSIST)) {
tun->flags &= ~IFF_PERSIST;
/* 참조 카운트 감소 — 연결된 fd가 없으면 삭제 */
dev_put(tun->dev);
}
break;
/* __tun_detach()에서의 persistent 처리 */
static void __tun_detach(struct tun_file *tfile, bool clean)
{
...
if (clean) {
if (tun->flags & IFF_PERSIST) {
/* persistent: 디바이스 유지, tun_file만 해제 */
rcu_assign_pointer(tfile->tun, NULL);
} else if (!tun->numqueues) {
/* non-persistent: 마지막 fd 종료 시 디바이스 삭제 */
tun_del_net(tun);
}
}
}
Persistent 디바이스 관리
# systemd-networkd를 이용한 persistent TAP 자동 생성
$ cat /etc/systemd/network/50-tap0.netdev
[NetDev]
Name=tap0
Kind=tap
[Tap]
User=qemu
Group=kvm
MultiQueue=yes
$ cat /etc/systemd/network/50-tap0.network
[Match]
Name=tap0
[Network]
Bridge=br0
# 적용
$ sudo systemctl restart systemd-networkd
# udev 규칙으로 TUN/TAP 권한 관리
$ cat /etc/udev/rules.d/99-tun.rules
KERNEL=="tun", GROUP="kvm", MODE="0660"
# 부팅 시 persistent TUN 자동 생성 스크립트
$ cat /etc/rc.local
ip tuntap add dev tun-vpn0 mode tun user vpn_user
ip addr add 10.8.0.1/24 dev tun-vpn0
ip link set tun-vpn0 up
ip route add 10.8.0.0/16 dev tun-vpn0
- Persistent 적합: 서비스 재시작 시 인터페이스 유지 필요, IP/라우팅 재설정 비용이 큰 경우, systemd-networkd 통합
- Non-persistent 적합: 임시 테스트, 자동 정리 필요, 동적 생성/삭제가 잦은 경우 (컨테이너 런타임)
패킷 흐름 추적 및 디버깅
TUN/TAP 디바이스의 패킷 흐름을 정확히 추적하는 것은 네트워크 문제 진단의 핵심입니다.
tcpdump, bpftrace, perf, ftrace를 활용한 체계적인 디버깅 방법론을 설명합니다.
tcpdump를 이용한 패킷 캡처
# TUN 인터페이스 패킷 캡처 (IP 레벨)
$ sudo tcpdump -i tun0 -n -e -vv -c 100
# TAP 인터페이스 패킷 캡처 (Ethernet 레벨)
$ sudo tcpdump -i tap0 -n -e -vv
# 특정 프로토콜만 캡처 (ICMP)
$ sudo tcpdump -i tun0 icmp -n
# pcap 파일로 저장 (Wireshark 분석용)
$ sudo tcpdump -i tap0 -w /tmp/tap0-capture.pcap -c 10000
# 브리지 양쪽 동시 캡처 (패킷 손실 지점 파악)
$ sudo tcpdump -i tap0 -n -c 50 &
$ sudo tcpdump -i br0 -n -c 50 &
$ sudo tcpdump -i eth0 -n -c 50 &
wait
bpftrace를 이용한 커널 내부 추적
# tun_get_user() 호출 빈도 및 패킷 크기 분포
$ sudo bpftrace -e '
kprobe:tun_get_user {
@call_count = count();
}
kretprobe:tun_get_user /retval > 0/ {
@pkt_sizes = hist(retval);
}
'
# tun_net_xmit() TX 경로 추적 (드롭 여부 포함)
$ sudo bpftrace -e '
kprobe:tun_net_xmit {
@tx_count = count();
@tx_by_cpu[cpu] = count();
}
kretprobe:tun_net_xmit /retval != 0/ {
@tx_drops = count();
printf("TX drop on CPU %d, ret=%d\n", cpu, retval);
}
'
# XDP 프로그램 실행 결과 추적
$ sudo bpftrace -e '
tracepoint:xdp:xdp_bulk_tx {
@xdp_actions[args->action] = count();
}
tracepoint:xdp:xdp_exception {
printf("XDP exception: dev=%s act=%d\n",
str(args->name), args->act);
}
'
# 큐별 패킷 분포 확인 (멀티큐 밸런스 점검)
$ sudo bpftrace -e '
kprobe:tun_select_queue {
@queue_dist = lhist(retval, 0, 16, 1);
}
'
perf를 이용한 성능 프로파일링
# TUN/TAP 관련 함수 CPU 사용 프로파일링
$ sudo perf record -g -a -e cycles -- sleep 10
$ sudo perf report --sort=dso,symbol | grep tun_
# 특정 프로세스(QEMU)의 TUN/TAP 핫 경로
$ sudo perf record -g -p $(pgrep qemu) -- sleep 30
$ sudo perf report
# Overhead Symbol
# 12.34% tun_net_xmit
# 8.56% tun_chr_read_iter
# 6.78% tun_get_user
# 4.12% tun_put_user
# 캐시 미스 분석 (메모리 병목 확인)
$ sudo perf stat -e cache-misses,cache-references,instructions \
-p $(pgrep qemu) -- sleep 10
# flame graph 생성
$ sudo perf script | ./stackcollapse-perf.pl | \
./flamegraph.pl > tuntap-flamegraph.svg
ftrace를 이용한 함수 추적
# TUN/TAP 드라이버 함수만 추적
$ cd /sys/kernel/debug/tracing
$ echo 0 > tracing_on
$ echo function_graph > current_tracer
$ echo 'tun_*' > set_ftrace_filter
$ echo 'vhost_*' >> set_ftrace_filter
$ echo 1 > tracing_on
# 패킷 한 개 전송 후 트레이스 확인
$ ping -c 1 10.0.0.2
$ echo 0 > tracing_on
$ cat trace
# CPU DURATION FUNCTION CALLS
# 0) 3.456 us | tun_net_xmit() {
# 0) 0.234 us | tun_flow_update();
# 0) 1.567 us | skb_queue_tail();
# 0) 0.123 us | tun_data_ready();
# 0) 5.380 us | }
# 트레이스 초기화
$ echo nop > current_tracer
$ echo > set_ftrace_filter
체계적 디버깅 체크리스트
| 단계 | 확인 항목 | 명령어 | 정상 상태 |
|---|---|---|---|
| 1. 인터페이스 | UP 상태 확인 | ip link show tun0 | UP,LOWER_UP 플래그 |
| 2. IP 설정 | IP 주소 할당 | ip addr show tun0 | 올바른 서브넷 |
| 3. 라우팅 | 경로 존재 | ip route get 10.0.0.2 | dev tun0 경유 |
| 4. 패킷 흐름 | TX/RX 카운터 | ip -s link show tun0 | 카운터 증가 |
| 5. 드롭 | 드롭 카운터 | ip -s link show tun0 | dropped = 0 |
| 6. 큐 상태 | 큐 길이 | tc -s qdisc show dev tun0 | backlog 낮음 |
| 7. fd 상태 | 열린 fd 확인 | lsof /dev/net/tun | 프로세스 연결됨 |
| 8. 오프로드 | 기능 일치 | ethtool -k tun0 | 양쪽 설정 일치 |
| 9. BPF 필터 | 필터 프로그램 | bpftool prog show | 의도한 프로그램 로드됨 |
| 10. 커널 로그 | 오류 메시지 | dmesg | grep -i tun | 에러 없음 |
- checksum 오프로드 불일치: tcpdump에서 "bad checksum" 경고가 나타나면
ethtool -K tun0 tx off로 캡처 시 비활성화 (실제 오류가 아닌 오프로드에 의한 미완성 체크섬) - GSO 패킷: tcpdump에서 MTU보다 큰 패킷이 보이면 GSO 활성 상태 (정상). 실제 세그먼테이션은 NIC 또는 스택 후단에서 수행
- NAPI 배치: 고부하 시 패킷이 묶여서 전달되므로, 지연 시간이 간헐적으로 증가할 수 있음 (NAPI budget 확인)
- 네임스페이스: TAP이 특정 네트워크 네임스페이스에 있으면
nsenter로 해당 네임스페이스에서 디버깅 필요
관련 문서
- VPP (FD.io) — 고성능 유저스페이스 패킷 처리 — FD.io VPP 벡터 패킷 처리, 그래프 노드 아키텍처, DPDK 통합, 플러그인, 커널
- 네트워크 보안 — xfrm/IPSec, WireGuard, Flooding 방어, Netlink
- IPVS L4 로드밸런싱 — Linux IPVS 아키텍처, 스케줄링 알고리즘
- Network Device 드라이버 (net_device) — net_device_ops, NAPI, RX/TX 링, ethtool