TUN/TAP 가상 네트워크 인터페이스
TUN/TAP는 userspace 애플리케이션이 가상 네트워크 인터페이스를 통해 패킷을 직접 송수신할 수 있게 하는 커널 드라이버입니다.
VPN, 네트워크 에뮬레이션, 가상화에서 핵심적으로 사용됩니다.
tun_struct/tun_file의 내부 필드 분석, XDP 프로그램 연동,
vhost-net 제로카피 전송 경로, 멀티큐 아키텍처와 스티어링 프로그램,
macvtap/macvlan과의 비교, virtio-net+TAP 통합, 오프로딩 제어,
패킷 전달 메커니즘의 커널 내부 함수 호출 체인, 성능 튜닝 파라미터, 디버깅 절차까지
운영 실무 관점으로 다룹니다.
핵심 요약
- TUN — IP 패킷(L3) 단위 가상 인터페이스. 라우팅 기반 VPN에 적합
- TAP — Ethernet 프레임(L2) 단위 가상 인터페이스. 브리지/가상화에 적합
- /dev/net/tun — userspace와 커널을 연결하는 문자 디바이스 (major 10, minor 200)
- tun_struct — 커널 내부에서 TUN/TAP 디바이스 하나를 대표하는 핵심 구조체
- tun_file — 파일 디스크립터(fd)별 큐 상태를 관리하는 구조체. 멀티큐의 핵심
- 주요 활용 — VPN, QEMU/KVM virtio-net, 컨테이너 네트워크, 패킷 테스트
단계별 이해
- 용도별 모드 선택
라우팅 기반 VPN은 TUN, 브리지/가상 스위치 연동은 TAP부터 검토합니다. - 디바이스 생성 확인
open()+TUNSETIFF로 인터페이스가 실제 생성되는지 먼저 검증합니다. - 패킷 경로 추적
userspace read/write와 커널 네트워크 스택 사이 흐름을 tcpdump/trace로 확인합니다. - 운영 옵션 고정
IFF_NO_PI, 멀티큐,IFF_VNET_HDR, 권한 모델을 서비스 요구사항에 맞춰 표준화합니다.
개요
TUN/TAP는 커널 공간과 userspace 간 패킷을 전달하는 가상 네트워크 디바이스입니다.
물리적 NIC가 하드웨어로부터 패킷을 수신하는 것처럼, TUN/TAP는 userspace 프로세스로부터 패킷을 수신합니다.
커널 소스에서 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 터널링 | 가상화 (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 헤더가 붙습니다.
| 오프셋 | 크기 | 필드 | 설명 |
|---|---|---|---|
| 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의 소켓 수신 큐에 저장됩니다.
Userspace는 read() / readv() 시스템 콜로 이 패킷을 가져갑니다.
/* 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--;
...
}
}
플로우 해시 테이블
TUN/TAP는 패킷의 src/dst 정보를 기반으로 플로우를 해시하여,
동일한 플로우의 패킷이 같은 큐로 전달되도록 합니다.
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)에서 락 없이 구조체에 접근할 수 있습니다.
/* 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(같은 인터페이스 재전송), 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에서 메모리 복사를 거쳐 오기 때문입니다.
vhost-net 제로카피
vhost-net은 커널 공간에서 TAP 디바이스와 virtio-net 링을 직접 연결하여 QEMU의 userspace 전환 오버헤드를 제거합니다.
제로카피(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에 구현된 커널 스레드로,
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()로
게스트 물리 메모리를 직접 참조하여 DMA scatter-gather I/O로 전달하는 방식입니다.
| 항목 | 일반 경로 | 제로카피 경로 |
|---|---|---|
| 메모리 접근 | memcpy_from_msg() 1회 | get_user_pages() + DMA |
| 패킷 완료 통지 | 즉시 used ring 갱신 | DMA 완료 후 콜백에서 갱신 |
| 대기 시간 | 짧음 (동기) | 길 수 있음 (비동기) |
| 최적 시나리오 | 소형 패킷 (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 기반 배치 수신 처리가 활성화됩니다.
고부하 환경에서 인터럽트 빈도를 줄이고 처리 효율을 높입니다.
/* 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 간 통신 차단 | 불가 | 완전 격리 요구 |
passthrough | 물리 NIC 직접 할당 (SR-IOV VF) | 불가 | 최대 성능 |
TAP vs macvtap 성능 비교
| 시나리오 | TAP + Bridge | macvtap (bridge) | 비고 |
|---|---|---|---|
| 처리량 (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 | 체크섬 오프로드 |
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 고정 | 캐시 효율 |
| 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 | 성능 프로파일링 | 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 전달 제한, 네임스페이스 격리 |
| 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을 마운트하면 컨테이너가 호스트 네트워크 스택에 패킷을 주입할 수 있습니다.
반드시 네트워크 네임스페이스와 함께 사용하고, 필요한 최소 권한만 부여하세요.
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 접근 감사 로그 수집
참고자료
- 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
- 네트워크 스택 — 패킷 처리 흐름
- Bridge/VLAN/Bonding — 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 명령으로 설정하며,
서비스 재시작 시에도 인터페이스와 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