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, 컨테이너 네트워크, 패킷 테스트

단계별 이해

  1. 용도별 모드 선택
    라우팅 기반 VPN은 TUN, 브리지/가상 스위치 연동은 TAP부터 검토합니다.
  2. 디바이스 생성 확인
    open() + TUNSETIFF로 인터페이스가 실제 생성되는지 먼저 검증합니다.
  3. 패킷 경로 추적
    userspace read/write와 커널 네트워크 스택 사이 흐름을 tcpdump/trace로 확인합니다.
  4. 운영 옵션 고정
    IFF_NO_PI, 멀티큐, IFF_VNET_HDR, 권한 모델을 서비스 요구사항에 맞춰 표준화합니다.
관련 문서: 네트워크 스택 (패킷 처리), sk_buff (패킷 버퍼), 가상화 (KVM) (QEMU 통합), Bridge/VLAN/Bonding (가상 네트워크), BPF/XDP (패킷 필터링), Network Device 드라이버 (net_device)

개요

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 헤더가 붙습니다.

오프셋크기필드설명
02 bytesflagsTUN_PKT_STRIP 등 플래그
22 bytesprotoEthernet 프로토콜 타입 (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를 기본 사용합니다.

아키텍처

Userspace VPN Client OpenVPN, WireGuard QEMU/KVM virtio-net + vhost-net Container Runtime Docker, Podman Packet Test Tool scapy, custom app Kernel/User Boundary (syscall: read/write/ioctl) /dev/net/tun (misc device, major 10, minor 200) tun_fops: open, read_iter, write_iter, poll, ioctl, release TUN/TAP Driver (drivers/net/tun.c) tun_struct (net_device) tun_file[] (per-queue) XDP / BPF filter tun_net_xmit() | tun_get_user() | tun_put_user() Network Stack (TCP/IP, Routing, Netfilter, Bridge) netif_rx() / netif_receive_skb() / dev_queue_xmit() Physical NIC (eth0, ens3, ...)

디바이스 생성

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 헤더 없이 순수 패킷만 전달 */

커널 내부 생성 흐름

open("/dev/net/tun") tun_chr_open() ioctl(TUNSETIFF) tun_set_iff() alloc_netdev_mqs() net_device + tun_struct 할당 tun_net_init() net_device_ops 설정 register_netdevice() sysfs/procfs 등록 tun_attach() tun_file <-> tun_struct 연결

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_TUNIFF_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");
}

패킷 흐름 상세

RX 경로 (Userspace write → 커널) write(fd, pkt, len) tun_chr_write_iter() tun_get_user() XDP 프로그램 실행 netif_receive_skb() IP/TCP/UDP 스택 TX 경로 (커널 → Userspace read) Application (소켓 send) TCP/IP 스택 dev_queue_xmit() tun_net_xmit() sk_receive_queue 삽입 tun_chr_read_iter() read(fd, buf, len)

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_devicepriv 영역에 내장됩니다.

/* 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 수신 큐 정보 */
};
tun_struct net_device *dev tfiles[MAX_TAP_QUEUES] xdp_prog, steering_prog flags, numqueues, flows[] tun_file[0] fd = 3 queue_index = 0 napi_struct tx_ring xdp_rxq tun_file[1] fd = 4 queue_index = 1 napi_struct tx_ring xdp_rxq tun_file[2] fd = 5 queue_index = 2 napi_struct tx_ring xdp_rxq ... tun_file[N-1] __rcu *tfiles[]

ioctl 인터페이스

ioctl설명인자
TUNSETIFF0x400454ca인터페이스 생성/연결struct ifreq
TUNSETPERSIST0x400454cbpersistent 모드 설정int (0/1)
TUNSETOWNER0x400454cc소유자 UID 설정uid_t
TUNSETGROUP0x400454ce소유자 GID 설정gid_t
TUNSETOFFLOAD0x400454d0오프로드 플래그 설정unsigned int
TUNSETVNETHDRSZ0x400454d8vnet 헤더 크기 설정int
TUNSETQUEUE0x400454d9큐 활성화/비활성화struct ifreq
TUNSETSNDBUF0x400454d4송신 버퍼 크기 설정int
TUNGETFEATURES0x800454cf지원 기능 조회unsigned int *
TUNSETFILTEREBPF0x800454e1eBPF 필터 설정int (fd)
TUNSETSTEEREBPF0x800454e0eBPF 스티어링 설정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_devicepriv 영역에 내장되며, alloc_netdev_mqs()를 통해 할당됩니다. 아래에서는 각 필드 그룹의 역할과 커널 내부 사용 패턴을 심층 분석합니다.

tun_struct (drivers/net/tun.c) 큐 관리 그룹 tfiles[MAX_TAP_QUEUES] numqueues / numdisabled disabled (list_head) sndbuf (송신 버퍼 크기) lock (mutex) BPF/XDP 그룹 xdp_prog (XDP 프로그램) steering_prog (큐 선택) filter_prog (패킷 필터) fprog (cBPF 필터) filter_attached (bool) 디바이스 속성 그룹 dev (net_device *) flags (IFF_TUN/TAP 등) owner (kuid_t) / group (kgid_t) set_features (오프로드) vnet_hdr_sz / align 플로우 관리 그룹 flows[TUN_NUM_FLOW_ENTRIES] flow_count / ageing_time flow_gc_timer (GC 타이머) hash_link (글로벌 해시) net_device_ops 연결 ndo_start_xmit = tun_net_xmit ndo_select_queue = tun_select_queue ndo_bpf = tun_xdp ndo_xdp_xmit = tun_xdp_xmit ethtool_ops get_drvinfo = tun_get_drvinfo get_msglevel / set_msglevel get_link = ethtool_op_get_link get_ts_info tun_struct 라이프사이클 할당: alloc_netdev_mqs() → tun_net_init() → register_netdevice() 연결: tun_attach() (tun_file <-> tun_struct RCU 연결) 해제: tun_detach_all() → unregister_netdevice() → free_netdev()

큐 관리 필드 상세

tfiles[] 배열은 RCU로 보호되며, 활성 큐와 비활성 큐를 독립적으로 관리합니다. numqueuesnumdisabled의 합이 총 연결된 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_structtun_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 모드를 모두 지원합니다.

write(fd, pkt) tun_get_user() XDP 프로그램 실행 XDP_PASS XDP_DROP XDP_TX XDP_REDIRECT netif_receive_skb() → 스택 패킷 폐기 같은 인터페이스로 재전송 다른 인터페이스로 리다이렉트

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 액션 요약: 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);
}
성능 팁: TUN/TAP의 XDP는 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으로 전달합니다.

Guest VM virtio-net 드라이버 virtqueue (TX/RX) Guest Memory QEMU (Userspace) 제어 경로만 처리 vhost-net (Kernel Thread) 데이터 경로: virtqueue <-> TAP 직접 연결 TAP (tap0) tun_sendmsg / tun_recvmsg Network Stack / Bridge Physical NIC zero-copy

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-netvhost-net + zero-copy
데이터 경로Guest → QEMU → TAPGuest → vhost-net → TAPGuest → vhost-net → TAP (복사 없음)
컨텍스트 전환Guest → Host User → KernelGuest → Kernel만Guest → Kernel만
메모리 복사2회1회0회
성능 (64B 패킷)기준약 +30~50%약 +50~80%
설정기본vhost=onvhost=on + 커널 지원
실무 팁: vhost-net 제로카피는 대용량 패킷(TSO/GSO 활성)에서 특히 효과적입니다. 소형 패킷 위주 워크로드에서는 일반 vhost-net과 차이가 크지 않을 수 있습니다.

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 ...
주의: vhost-net 제로카피는 실험적(experimental) 기능입니다. 패킷 전송 완료 전에 게스트가 해당 메모리를 재사용하면 데이터 손상이 발생할 수 있으며, virtio used ring 갱신이 지연되어 게스트의 TX 큐가 포화될 수 있습니다. 운영 환경에서는 충분한 벤치마크 후 활성화하세요.

Multi-Queue TUN/TAP

Multi-queue는 여러 CPU 코어에서 병렬로 패킷을 처리하여 성능을 향상시킵니다. 하나의 TUN/TAP 디바이스에 최대 MAX_TAP_QUEUES(256)개의 큐를 생성할 수 있으며, 각 큐는 독립적인 파일 디스크립터와 tun_file 구조체를 갖습니다.

CPU 0 (fd=3) CPU 1 (fd=4) CPU 2 (fd=5) CPU 3 (fd=6) tun_file[0..3] (per-queue) tun_select_queue() eBPF steering / flow hash / automq tun_struct (net_device: tap0) Network Stack

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;
}
큐 선택 우선순위: (1) eBPF 스티어링 프로그램 → (2) Flow hash 기반 자동 분배 → (3) 현재 CPU 기반 선택. 스티어링 프로그램이 설정되면 항상 최우선 적용됩니다.

멀티큐 커널 구현 상세

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_NAPIIFF_NAPI_FRAGS를 함께 활성화하면 vhost-net의 scatter-gather 패킷도 NAPI 경로에서 처리되어 성능이 향상됩니다. QEMU 6.0 이상에서 자동으로 설정합니다.

macvtap 비교

macvtap은 MACVLAN과 TAP을 결합한 가상 디바이스로, TAP과 유사한 용도이지만 다른 아키텍처를 가집니다.

항목TAPmacvtapmacvlan
계층L2 (가상 NIC)L2 (가상 NIC)L2 (MAC 기반)
물리 NIC 필요아니오예 (상위 인터페이스 필수)
브리지 필요예 (외부 연결 시)아니오 (자체 브리지)아니오
userspace 접근/dev/net/tun/dev/tapN없음 (커널만)
QEMU 지원예 (-netdev tap)예 (-netdev tap,fd=...)불가
성능 (VM 네트워크)중간높음 (브리지 생략)해당 없음
호스트-VM 통신브리지 통해 가능VEPA 모드만 가능해당 없음
SR-IOV 호환아니오패스스루 모드아니오
TAP + Bridge QEMU (VM) tap0 Bridge (br0) Physical NIC (eth0) macvtap (브리지 불필요) QEMU (VM) macvtap0 (TAP+MACVLAN) Physical NIC (eth0) 직접 연결
# 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 호환 스위치 환경
privatemacvtap 간 통신 차단불가완전 격리 요구
passthrough물리 NIC 직접 할당 (SR-IOV VF)불가최대 성능

TAP vs macvtap 성능 비교

시나리오TAP + Bridgemacvtap (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 디바이스가 이를 물리 네트워크로 전달합니다.

Guest Kernel TCP/IP Stack virtio-net 드라이버 Virtqueue TX vring (avail/used) RX vring (avail/used) Shared Memory Host Kernel vhost-net vhost_worker thread handle_tx / handle_rx TAP (tap0) tun_sendmsg() tun_recvmsg() Bridge (br0) / OVS Physical NIC Driver Hardware NIC virtqueue

virtio-net 기능 제어

virtio-net 기능호스트 TAP 대응설명
VIRTIO_NET_F_CSUMTUN_F_CSUM체크섬 오프로드
VIRTIO_NET_F_GSOIFF_VNET_HDRGSO 지원
VIRTIO_NET_F_HOST_TSO4TUN_F_TSO4호스트 TCP Segmentation (IPv4)
VIRTIO_NET_F_HOST_TSO6TUN_F_TSO6호스트 TCP Segmentation (IPv6)
VIRTIO_NET_F_HOST_UFOTUN_F_UFO호스트 UDP Fragmentation
VIRTIO_NET_F_MRG_RXBUFvnet_hdr_sz 조정병합 수신 버퍼
VIRTIO_NET_F_MQIFF_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 기능 협상 흐름

Guest (virtio-net) VIRTIO_NET_F_CSUM 요청 VIRTIO_NET_F_HOST_TSO4 요청 VIRTIO_NET_F_MQ 요청 VIRTIO_NET_F_MRG_RXBUF 요청 VIRTIO_NET_F_CTRL_VQ 요청 Host (TAP + QEMU) TUN_F_CSUM (TUNSETOFFLOAD) TUN_F_TSO4 (TUNSETOFFLOAD) IFF_MULTI_QUEUE (TUNSETIFF) TUNSETVNETHDRSZ (v1 크기) IFF_VNET_HDR (TUNSETIFF) QEMU 기능 협상 TUNGETFEATURES 조회 Guest/Host 교집합 계산 TUNSETOFFLOAD 설정

오프로딩

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
성능 팁: TSO/GSO를 활성화하면 대량 전송 시 최대 64KB 크기의 슈퍼 패킷을 한 번에 처리할 수 있어 시스템 콜 횟수가 크게 줄어듭니다. VPN 시나리오에서는 MTU를 적절히 설정하여 터널 오버헤드를 고려하세요.

오프로딩 커널 구현

TUNSETOFFLOAD ioctl은 tun_set_offload()를 호출하여 net_devicefeatures 비트마스크를 설정합니다. 이 비트마스크에 따라 네트워크 스택은 해당 기능을 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_hdrgso_typegso_size 필드를 통해 userspace가 실제 세그먼테이션을 수행하거나, 다시 커널에 위임할 수 있습니다.

TCP 64KB 전송 sendmsg(sock, 64KB) TCP 스택 (GSO) 1개 skb (64KB, gso_size=1460) tun_net_xmit() 1개 슈퍼 패킷 큐 삽입 Userspace read() vnet_hdr + 64KB 데이터 TCP 스택 (GSO 없음) 44개 skb (각 1460B) tun_net_xmit() x44 44개 패킷 큐 삽입 read() x44 44번 시스템 콜 필요 GSO 활성: 1회 read()로 64KB GSO 비활성: 44회 read() 필요 GSO 활성화 시 시스템 콜 횟수 약 44배 감소 (64KB TCP 전송 기준)
주의: GRO(Generic Receive Offload)는 RX 경로(userspace write → 커널)에서 적용됩니다. 여러 개의 작은 패킷을 하나의 큰 패킷으로 합쳐서 네트워크 스택 처리 효율을 높입니다. TUN/TAP에서 GRO를 비활성화하면(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);
필터 vs 스티어링:
  • 필터 (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);
성능 팁: 고성능 TUN/TAP 애플리케이션에서는 다음 패턴을 권장합니다:
  • 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 txqueuelen5001000~5000대량 전송 시 증가
큐 수IFF_MULTI_QUEUE1CPU 수CPU 병렬 처리
TSO/GSOTUNSETOFFLOADoffon대량 전송 최적화
VNET_HDRIFF_VNET_HDRoffon (VM)오프로드 정보 전달
NAPIIFF_NAPIoffon (고부하)배치 수신 처리

시스템 레벨 튜닝

# 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 pollingsysctl 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_cpusTX CPU 고정캐시 효율
GRO flush timeoutsysctl net.core.gro_flush_timeout=2000GRO 배치 크기 증가처리량 최적화
NAPI weight드라이버 코드 수정배치 크기 조정고부하 서버
SO_BUSY_POLLsetsockopt(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 offip 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
bpftoolBPF/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/typeR인터페이스 타입 (ARPHRD_NONE=65534, ARPHRD_ETHER=1)
/sys/class/net/tun0/mtuR/WMTU 크기
/sys/class/net/tun0/flagsR인터페이스 플래그 (IFF_UP 등)
/sys/class/net/tun0/operstateR동작 상태 (up/down/unknown)
/sys/class/net/tun0/tx_queue_lenR/WTX 큐 길이
/sys/class/net/tun0/queues/R큐별 상태 디렉터리
/sys/class/net/tun0/statistics/R패킷/바이트 통계
/sys/class/net/tun0/tun_flagsRTUN/TAP 전용 플래그 (IFF_TUN 등)
/sys/class/net/tun0/ownerR디바이스 소유자 UID (-1=미설정)
/sys/class/net/tun0/groupR디바이스 소유자 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 접근 감사 로그 수집

참고자료

다음 학습:

컨테이너 네트워킹에서의 TAP

TAP 디바이스는 마이크로VM 기반 컨테이너 런타임에서 핵심 네트워크 인터페이스로 사용됩니다. Kata Containers, Firecracker, gVisor 같은 경량 VM 런타임은 각 컨테이너(또는 Pod)에 전용 TAP 인터페이스를 생성하여 하드웨어 수준의 네트워크 격리를 제공합니다. CNI(Container Network Interface) 플러그인은 TAP 디바이스의 생성, IP 할당, 라우팅 설정을 자동화합니다.

Kata Container / Firecracker microVM 컨테이너 프로세스 Guest virtio-net Virtqueue TX/RX vring Shared Memory Host Kernel (Pod netns) vhost-net worker TAP (tap_kata0) CNI Plugin (tc-redirect-peer, bridge, ptp) veth pair / Bridge / Host 라우팅 테이블 Physical NIC / Overlay Network (VXLAN, Geneve) Kubernetes Pod 네트워크 흐름 Pod (microVM) → TAP → CNI (veth/bridge) → Host → Overlay → Remote Pod

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 연결 방식사용 런타임특징
bridgeTAP → veth → BridgeKata, Firecracker표준 L2 브리지 연결
ptpTAP → veth pair (point-to-point)Kata브리지 없이 직접 라우팅
tc-redirect-peerTAP → tc ingress/egressCilium + KataeBPF 기반 고성능 경로
macvlanmacvtap (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 */
성능 팁: Kata Containers에서 최대 네트워크 성능을 달성하려면:
  • 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 모드 라이프사이클

1. 생성 ip tuntap add + TUNSETPERSIST 2. 설정 IP, 라우팅, 오프로드 3. 연결 앱이 open + TUNSETIFF 4. 분리 close(fd) 인터페이스 유지 (재연결 가능) 비-Persistent: close(fd) 시 인터페이스 즉시 삭제 삭제: ip tuntap del dev tun0 mode tun (또는 TUNSETPERSIST 0)

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 vs Non-persistent 선택 기준:
  • 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 tun0UP,LOWER_UP 플래그
2. IP 설정IP 주소 할당ip addr show tun0올바른 서브넷
3. 라우팅경로 존재ip route get 10.0.0.2dev tun0 경유
4. 패킷 흐름TX/RX 카운터ip -s link show tun0카운터 증가
5. 드롭드롭 카운터ip -s link show tun0dropped = 0
6. 큐 상태큐 길이tc -s qdisc show dev tun0backlog 낮음
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로 해당 네임스페이스에서 디버깅 필요
필수 관련 문서: 참고 문서: