MACVLAN / IPVLAN / veth / TUN/TAP — 가상 네트워크 인터페이스
Linux 커널 가상 네트워크 인터페이스 분석: MACVLAN(L2 분리), IPVLAN(L3/L3S), veth pair(네임스페이스(Namespace) 연결), TUN/TAP(유저스페이스 패킷(Packet) I/O)의 커널 내부 구현과 컨테이너/VM 네트워킹 활용을 다룹니다.
핵심 요약
- MACVLAN — 하나의 물리 NIC 위에 고유 MAC 주소를 가진 가상 인터페이스를 생성하여 L2 수준 격리를 제공합니다. bridge/vepa/private/passthru/source 5가지 모드를 지원합니다.
- IPVLAN — 동일한 MAC 주소를 공유하면서 IP 주소로 트래픽을 구분합니다. L2/L3/L3S 3가지 모드를 지원하며, MAC 주소 제한 환경(클라우드)에 적합합니다.
- veth pair — 항상 쌍으로 생성되어 네트워크 네임스페이스 간 통신을 연결하는 가상 이더넷 디바이스입니다. Docker/Kubernetes 컨테이너 네트워킹의 기본 빌딩 블록입니다.
- TUN/TAP — TUN(L3)과 TAP(L2) 디바이스를 통해 유저스페이스와 커널 간 직접 패킷 I/O를 제공합니다. VPN(OpenVPN), 가상화(QEMU/KVM)에 활용됩니다.
단계별 이해
- MACVLAN/IPVLAN 기초 파악
MACVLAN의 5가지 모드(bridge/vepa/private/passthru/source)와 IPVLAN의 3가지 모드(L2/L3/L3S)의 차이를 이해하고, 환경에 따른 선택 기준을 파악합니다. - veth pair와 네임스페이스 연결
veth pair의 생성·네임스페이스 이동·브리지 연결 과정을 학습하고,veth_xmit()의 패킷 전달 메커니즘을 파악합니다. - TUN/TAP 유저스페이스 패킷 I/O
TUN(L3)과 TAP(L2)의 차이,/dev/net/tun디바이스 열기와 ioctl 설정,read()/write()기반 패킷 교환 방식을 학습합니다. - 성능 최적화와 실전 활용
veth XDP, MACVLAN/IPVLAN 성능 비교, vhost-net 가속, 컨테이너 네트워킹 조합(Docker/Kubernetes)을 연습합니다.
MACVLAN / IPVLAN
MACVLAN은 하나의 물리 NIC 위에 각기 다른 MAC 주소를 가진 가상 인터페이스를 생성합니다. IPVLAN은 동일한 MAC 주소를 공유하면서 IP 주소로 트래픽을 구분합니다. 둘 다 컨테이너(Container)/VM 네트워킹에서 브리지(Bridge) 없이 고성능 네트워크 연결을 제공하는 경량 가상 NIC입니다.
MACVLAN 모드
| 모드 | MACVLAN 간 통신 | 외부 통신 | 설명 |
|---|---|---|---|
bridge | O (직접) | O | MACVLAN 간 내부 브리징 지원 |
vepa | O (외부 스위치 경유) | O | 모든 트래픽이 외부 스위치를 통과 |
private | X | O | MACVLAN 간 완전 격리(Isolation) |
passthru | - | O | 물리 NIC의 MAC을 직접 사용 (단일) |
source | 필터 기반 | O | 허용된 소스 MAC만 수신 |
# MACVLAN 생성 (bridge 모드)
ip link add macvlan0 link eth0 type macvlan mode bridge
ip addr add 192.168.1.100/24 dev macvlan0
ip link set macvlan0 up
# MACVLAN private 모드 (컨테이너 격리에 적합)
ip link add macvlan1 link eth0 type macvlan mode private
# IPVLAN L2 모드 (기본)
ip link add ipvlan0 link eth0 type ipvlan mode l2
ip addr add 192.168.1.200/24 dev ipvlan0
ip link set ipvlan0 up
# IPVLAN L3 모드 (라우팅 기반)
ip link add ipvlan1 link eth0 type ipvlan mode l3
# IPVLAN L3S 모드 (L3 + Netfilter/conntrack 통합)
ip link add ipvlan2 link eth0 type ipvlan mode l3s
MACVLAN 커널 자료 구조
MACVLAN의 핵심 자료 구조는 macvlan_port(물리 NIC당 하나)와 macvlan_dev(가상 인터페이스당 하나)입니다. macvlan_port는 하위 디바이스(lower device)에 등록된 rx_handler의 컨텍스트(Context)이며, 모든 MACVLAN 디바이스를 MAC 해시 테이블과 연결 리스트(Linked List)로 관리합니다.
/* drivers/net/macvlan.c — 물리 NIC(lower device)당 하나 생성 */
struct macvlan_port {
struct net_device *dev; /* lower device (예: eth0) */
struct hlist_head vlan_hash[MACVLAN_HASH_SIZE]; /* MAC→macvlan_dev 해시 */
struct list_head vlans; /* macvlan_dev 연결 리스트 */
struct sk_buff_head bc_queue; /* 브로드캐스트 큐 */
struct work_struct bc_work; /* 브로드캐스트 처리 워크 */
int count; /* 등록된 MACVLAN 수 */
struct hlist_head vlan_source_hash[MACVLAN_HASH_SIZE]; /* source 모드 해시 */
DECLARE_BITMAP( mc_filter, MACVLAN_MC_FILTER_SZ); /* 멀티캐스트 필터 */
unsigned char perm_addr[ETH_ALEN]; /* lower device 원본 MAC */
};
/* include/linux/if_macvlan.h — 가상 인터페이스당 하나 생성 */
struct macvlan_dev {
struct net_device *dev; /* 이 MACVLAN의 net_device */
struct list_head list; /* macvlan_port->vlans 링크 */
struct hlist_node hlist; /* MAC 해시 테이블 링크 */
struct macvlan_port *port; /* 소속 macvlan_port 참조 */
struct net_device *lowerdev; /* 하위 물리 디바이스 */
enum macvlan_mode mode; /* bridge/vepa/private/passthru/source */
int flags; /* MACVLAN_FLAG_NOPROMISC 등 */
unsigned int macaddr_count; /* source 모드: 허용 MAC 수 */
struct { /* per-CPU 통계 */
u64 rx_packets, rx_bytes;
u64 tx_packets, tx_bytes;
} __percpu *pcpu_stats;
netdev_features_t set_features; /* 기능 플래그 (GRO, checksum) */
};
MACVLAN source 모드 상세
source 모드는 허용된 소스(Source) MAC 주소 목록을 기반으로 수신 패킷을 필터링합니다. 특정 MAC 주소에서 온 패킷만 가상 인터페이스로 전달하므로, MAC 기반 접근 제어(Access Control)가 가능합니다.
# source 모드 MACVLAN 생성
ip link add macvlan-src link eth0 type macvlan mode source
# 허용할 소스 MAC 주소 추가
ip link set macvlan-src type macvlan macaddr add aa:bb:cc:11:22:33
ip link set macvlan-src type macvlan macaddr add aa:bb:cc:44:55:66
# 허용 MAC 목록 확인
ip -d link show macvlan-src
# MAC 제거
ip link set macvlan-src type macvlan macaddr del aa:bb:cc:44:55:66
# 활성화
ip addr add 192.168.1.200/24 dev macvlan-src
ip link set macvlan-src up
/* drivers/net/macvlan.c — source 모드 패킷 필터링 */
static bool macvlan_passthru(const struct macvlan_port *port);
/* macvlan_handle_frame()에서 source 모드 처리:
* 1. 소스 MAC으로 vlan_source_hash 조회
* 2. 매칭되는 macvlan_dev가 있으면 → 해당 디바이스로 전달
* 3. 매칭 없으면 → RX_HANDLER_PASS (물리 디바이스로 전달) */
static struct macvlan_source_entry *
macvlan_hash_lookup_source(const struct macvlan_port *port,
const unsigned char *addr)
{
struct macvlan_source_entry *entry;
u32 idx = macvlan_eth_hash(addr);
/* 소스 MAC 해시 테이블에서 엔트리 검색 */
hlist_for_each_entry_rcu(entry, &port->vlan_source_hash[idx], hlist) {
if (ether_addr_equal_64bits(entry->addr, addr))
return entry; /* entry->vlan이 수신할 macvlan_dev */
}
return NULL;
}
- MAC 기반 VLAN 할당: 특정 장비(예: IoT 센서)의 MAC을 등록하여 전용 가상 인터페이스로 격리합니다
- 보안 필터링: 허용된 MAC만 수신하므로 ARP 스푸핑(Spoofing) 방지에 활용합니다
- 네트워크 모니터링: 특정 장비 트래픽만 캡처하는 가상 인터페이스를 구성합니다
MACVLAN 패킷 수신 핸들러
/* drivers/net/macvlan.c — MACVLAN 패킷 수신 */
static rx_handler_result_t macvlan_handle_frame(
struct sk_buff **pskb)
{
struct macvlan_port *port;
struct macvlan_dev *vlan;
const struct ethhdr *eth = eth_hdr(skb);
port = macvlan_port_get_rcu(skb->dev);
/* 목적지 MAC으로 MACVLAN 디바이스 탐색 */
if (macvlan_passthru(port))
vlan = list_first_or_null_rcu(&port->vlans, ...);
else
vlan = macvlan_hash_lookup(port, eth->h_dest);
if (!vlan)
return RX_HANDLER_PASS; /* 물리 디바이스로 전달 */
/* MACVLAN 디바이스로 패킷 전달 */
skb->dev = vlan->dev;
skb->pkt_type = PACKET_HOST;
netif_rx(skb);
return RX_HANDLER_CONSUMED;
}
- MACVLAN: 각 컨테이너에 고유 MAC이 필요한 경우, 802.1Q VLAN과 결합할 때
- IPVLAN: MAC 주소 수 제한이 있는 환경 (일부 클라우드), Netfilter 규칙 공유 시 (L3S 모드)
veth pair (가상 이더넷 쌍)
veth는 항상 쌍(pair)으로 생성되는 가상 이더넷 디바이스입니다. 한쪽에 전송된 패킷은 반대쪽에서 수신됩니다. Docker, Kubernetes, LXC 등 컨테이너 네트워킹의 기본 빌딩 블록이며, 네트워크 네임스페이스 간 통신의 핵심입니다.
# veth 쌍 생성
ip link add veth0 type veth peer name veth1
# 한쪽을 네트워크 네임스페이스로 이동
ip netns add ns1
ip link set veth1 netns ns1
# 호스트 측 설정
ip addr add 10.0.0.1/24 dev veth0
ip link set veth0 up
# 네임스페이스 측 설정
ip netns exec ns1 ip addr add 10.0.0.2/24 dev veth1
ip netns exec ns1 ip link set veth1 up
ip netns exec ns1 ip link set lo up
# 브리지에 veth 연결 (컨테이너 → 외부 통신)
ip link set veth0 master br0
/* drivers/net/veth.c — veth 패킷 전송 */
static netdev_tx_t veth_xmit(struct sk_buff *skb,
struct net_device *dev)
{
struct veth_priv *rcv_priv, *priv = netdev_priv(dev);
struct net_device *rcv;
/* peer 디바이스 참조 획득 */
rcv = rcu_dereference(priv->peer);
if (unlikely(!rcv)) {
kfree_skb(skb);
goto drop;
}
/* XDP 프로그램이 있으면 실행 */
if (likely(veth_forward_skb(rcv, skb, rq, rcv_xdp) == NET_RX_SUCCESS)) {
/* 패킷을 peer 디바이스의 수신 큐에 전달 */
struct pcpu_lstats *stats = this_cpu_ptr(dev->lstats);
u64_stats_update_begin(&stats->syncp);
stats->bytes += length;
stats->packets++;
u64_stats_update_end(&stats->syncp);
}
return NETDEV_TX_OK;
}
veth_xdp_rcv() 경로를 통해 peer로부터 수신된 패킷에 XDP 프로그램을 적용할 수 있어, 컨테이너 네트워킹에서 고성능 패킷 처리가 가능합니다. 또한 GRO(Generic Receive Offload)도 지원합니다.
veth 커널 자료 구조와 생성 과정
veth pair는 veth_newlink()에서 생성됩니다. 각 veth 디바이스는 veth_priv 구조체로 관리되며, peer 포인터로 상대편을 참조합니다. 생성 시 양쪽 디바이스를 동시에 할당하고 교차 연결하는 것이 핵심입니다.
/* drivers/net/veth.c — veth 디바이스 개별 상태 */
struct veth_priv {
struct net_device __rcu *peer; /* 반대편 veth 디바이스 */
atomic64_t dropped; /* 드롭된 패킷 수 */
struct bpf_prog *_xdp_prog; /* XDP 프로그램 */
struct veth_rq *rq; /* per-queue RX 구조체 배열 */
unsigned int requested_headroom; /* XDP 헤드룸 */
};
/* per-queue RX 구조체 (NAPI + XDP 지원) */
struct veth_rq {
struct napi_struct xdp_napi; /* NAPI 인스턴스 */
struct napi_struct napi; /* 일반 NAPI */
struct net_device *dev; /* 소속 net_device */
struct bpf_prog __rcu *xdp_prog; /* per-queue XDP */
struct xdp_mem_info xdp_mem; /* XDP 메모리 정보 */
struct xdp_rxq_info xdp_rxq; /* XDP RX 큐 정보 */
struct sk_buff_head xdp_ring; /* XDP 패킷 큐 */
};
/* drivers/net/veth.c — veth pair 생성 (ip link add type veth) */
static int veth_newlink(struct net *src_net,
struct net_device *dev,
struct nlattr *tb[],
struct nlattr *data[],
struct netlink_ext_ack *extack)
{
struct net_device *peer;
struct veth_priv *priv;
struct net *net;
int err;
/* 1단계: peer 디바이스 할당 */
net = rtnl_link_get_net(src_net, tbp);
peer = rtnl_create_link(net, ifname, name_assign_type,
&veth_link_ops, tbp, extack);
/* 2단계: peer 디바이스 등록 */
err = register_netdevice(peer);
/* 3단계: 양쪽 veth_priv의 peer 포인터 교차 연결 */
priv = netdev_priv(dev);
rcu_assign_pointer(priv->peer, peer); /* dev→peer = peer */
priv = netdev_priv(peer);
rcu_assign_pointer(priv->peer, dev); /* peer→peer = dev */
/* 4단계: dev 디바이스 등록 (양쪽 모두 활성화) */
err = register_netdevice(dev);
return 0;
}
/* drivers/net/veth.c — veth 삭제: 한쪽 삭제 시 peer도 자동 해제 */
static void veth_dellink(struct net_device *dev,
struct list_head *head)
{
struct veth_priv *priv;
struct net_device *peer;
priv = netdev_priv(dev);
peer = rtnl_dereference(priv->peer);
/* peer가 존재하면 함께 등록 해제 */
if (peer) {
struct veth_priv *peer_priv = netdev_priv(peer);
RCU_INIT_POINTER(peer_priv->peer, NULL);
unregister_netdevice_queue(peer, head);
}
RCU_INIT_POINTER(priv->peer, NULL);
unregister_netdevice_queue(dev, head);
}
/* net_device_ops — veth 디바이스 오퍼레이션 */
static const struct net_device_ops veth_netdev_ops = {
.ndo_open = veth_open,
.ndo_stop = veth_close,
.ndo_start_xmit = veth_xmit, /* TX: peer로 전달 */
.ndo_get_stats64 = veth_get_stats64, /* per-CPU 통계 */
.ndo_set_rx_mode = veth_set_multicast_list,
.ndo_set_mac_address = eth_mac_addr,
.ndo_bpf = veth_xdp, /* XDP 프로그램 관리 */
.ndo_xdp_xmit = veth_ndo_xdp_xmit, /* XDP_REDIRECT 수신 */
.ndo_features_check = passthru_features_check,
};
- 원자적 생성/삭제: 양쪽 디바이스가 항상 쌍으로 생성·삭제됩니다. 한쪽을
ip link del하면 peer도 자동 제거됩니다 - 네임스페이스 이동:
ip link set veth1 netns ns1으로 한쪽을 다른 네임스페이스로 이동할 수 있습니다. 이동 후에도 peer 참조는 유효합니다 - RCU 보호: peer 포인터는 RCU로 보호되어 패킷 전송 중 peer가 삭제되어도 안전합니다
- NAPI 지원: 커널 5.0+에서 XDP가 활성화되면
veth_rq의 NAPI가 활성화되어 폴링 기반 수신이 가능합니다
TUN/TAP
TUN은 L3(IP) 수준, TAP은 L2(이더넷) 수준에서 유저스페이스와 커널 간 패킷 I/O를 제공합니다. VPN(OpenVPN, WireGuard), 가상화(QEMU/KVM), 네트워크 시뮬레이션에 사용됩니다.
| 항목 | TUN | TAP |
|---|---|---|
| 계층 | L3 (IP 패킷) | L2 (이더넷 프레임) |
| 용도 | 라우팅(Routing) 기반 VPN | 브리징, VM NIC |
| 디바이스 파일 | /dev/net/tun | |
| 생성 플래그 | IFF_TUN | IFF_TAP |
TUN/TAP 아키텍처
TUN/TAP 디바이스는 drivers/net/tun.c에서 구현됩니다. 유저스페이스 프로세스(Process)가 /dev/net/tun 캐릭터 디바이스를 열고 TUNSETIFF ioctl로 가상 인터페이스를 생성하면, 커널은 tun_struct와 연결된 net_device를 등록합니다. 이후 유저스페이스는 파일 디스크립터(File Descriptor)의 read()/write()를 통해 커널 네트워크 스택과 직접 패킷을 교환합니다.
TUN/TAP 디바이스 생성 라이프사이클
커널 자료 구조
TUN/TAP의 핵심 자료 구조는 tun_struct(디바이스 전체 관리)와 tun_file(각 큐/fd 관리)입니다. 멀티큐 지원을 위해 하나의 tun_struct에 여러 tun_file이 연결됩니다.
/* drivers/net/tun.c — 디바이스 전체를 관리하는 구조체 */
struct tun_struct {
struct tun_file __rcu *tfiles[MAX_TAP_QUEUES]; /* 큐별 tun_file 배열 */
unsigned int numqueues; /* 활성 큐 수 */
unsigned int flags; /* IFF_TUN / IFF_TAP 등 */
kuid_t owner; /* TUNSETOWNER로 설정 */
kgid_t group; /* TUNSETGROUP으로 설정 */
struct net_device *dev; /* 연결된 net_device */
struct net_device_stats stats; /* 패킷/바이트 통계 */
struct tap_filter txflt; /* TX 필터 (MAC 기반) */
int sndbuf; /* 소켓 송신 버퍼 크기 */
int vnet_hdr_sz; /* virtio-net 헤더 크기 */
};
/* drivers/net/tun.c — 큐(파일 디스크립터)별 관리 구조체 */
struct tun_file {
struct tun_struct __rcu *tun; /* 소속 tun_struct 역참조 */
struct socket socket; /* vhost-net 연동용 소켓 */
struct tun_page tpage; /* XDP용 페이지 풀 */
struct xdp_rxq_info xdp_rxq; /* XDP 수신 큐 정보 */
struct napi_struct napi; /* NAPI 폴링 (napi_gro_receive) */
int queue_index; /* 멀티큐 인덱스 */
struct sk_buff_head sk_receive_queue; /* 수신 패킷 큐 (TX→유저) */
};
MAX_TAP_QUEUES는 기본 256입니다. QEMU는 vCPU당 하나의 큐를 할당하여 병렬 패킷 처리를 수행합니다. 각 큐는 독립적인 tun_file과 sk_receive_queue를 가지므로 락 경합(Contention) 없이 동시 I/O가 가능합니다.
주요 플래그 및 ioctl
/dev/net/tun을 열고 TUNSETIFF ioctl을 호출할 때 ifreq.ifr_flags에 설정하는 플래그로 디바이스 동작을 제어합니다.
| 플래그 | 값 | 설명 |
|---|---|---|
IFF_TUN | 0x0001 | L3 (IP) TUN 디바이스 생성 |
IFF_TAP | 0x0002 | L2 (Ethernet) TAP 디바이스 생성 |
IFF_NO_PI | 0x1000 | PI(Packet Info) 헤더 생략 — 순수 패킷만 전달 |
IFF_VNET_HDR | 0x4000 | virtio-net 헤더 포함 (GSO/checksum offload) |
IFF_MULTI_QUEUE | 0x0100 | 멀티큐 모드 활성화 |
IFF_PERSIST | 0x0800 | fd 닫아도 디바이스 유지 |
IFF_NOFILTER | 0x1000 | 패킷 필터 비활성화 |
IFF_NO_PI를 설정하지 않으면 각 패킷 앞에 4바이트 struct tun_pi가 붙습니다 — flags(2바이트, 예: TUN_PKT_STRIP)와 proto(2바이트, ETH_P_IP 등). 대부분의 애플리케이션은 IFF_NO_PI를 설정하여 순수 패킷만 교환합니다.
디바이스 생성 후 추가 설정을 위한 ioctl 명령입니다.
| ioctl 명령 | 인자 | 설명 |
|---|---|---|
TUNSETIFF | struct ifreq * | 디바이스 생성/연결 (이름 + 플래그 설정) |
TUNSETOWNER | uid_t | 디바이스 소유자 UID 설정 |
TUNSETGROUP | gid_t | 디바이스 그룹 GID 설정 |
TUNSETPERSIST | int | 0: 비영구, 1: fd 닫아도 디바이스 유지 |
TUNSETOFFLOAD | unsigned long | offload 기능 설정 (TUN_F_CSUM, TUN_F_TSO4 등) |
TUNSETVNETHDRSZ | int | virtio-net 헤더 크기 설정 (기본 10 또는 12) |
/* 유저스페이스에서 TUN/TAP 디바이스 생성 */
#include <linux/if_tun.h>
#include <sys/ioctl.h>
int tun_alloc(char *dev, int flags)
{
struct ifreq ifr;
int fd, err;
/* /dev/net/tun 열기 */
fd = open("/dev/net/tun", O_RDWR);
if (fd < 0)
return fd;
memset(&ifr, 0, sizeof(ifr));
ifr.ifr_flags = flags; /* IFF_TUN 또는 IFF_TAP | IFF_NO_PI */
if (*dev)
strncpy(ifr.ifr_name, dev, IFNAMSIZ);
/* TUNSETIFF ioctl로 디바이스 생성 */
err = ioctl(fd, TUNSETIFF, (void *)&ifr);
if (err < 0) {
close(fd);
return err;
}
strcpy(dev, ifr.ifr_name);
return fd; /* read()/write()로 패킷 I/O */
}
/* 사용 예 */
char tun_name[IFNAMSIZ] = "tun0";
int tun_fd = tun_alloc(tun_name, IFF_TUN | IFF_NO_PI);
/* 패킷 읽기 (커널 → 유저스페이스) */
char buf[2048];
int nread = read(tun_fd, buf, sizeof(buf));
/* 패킷 쓰기 (유저스페이스 → 커널) */
write(tun_fd, packet, pkt_len);
# ip 명령으로 TUN/TAP 생성
ip tuntap add dev tun0 mode tun user $(whoami)
ip tuntap add dev tap0 mode tap user $(whoami)
# 멀티큐 TUN/TAP (QEMU vhost-net에 활용)
ip tuntap add dev tap0 mode tap multi_queue vnet_hdr
# 삭제
ip tuntap del dev tun0 mode tun
패킷 흐름
TUN/TAP의 패킷 흐름은 크게 세 가지 경로로 나뉩니다: 유저스페이스 → 커널(Write), 커널 → 유저스페이스(Read), 커널 네트워크 스택 → TUN/TAP(TX).
/* drivers/net/tun.c — Write 경로: 유저가 write() 시 커널로 패킷 주입 */
static ssize_t tun_chr_write_iter(
struct kiocb *iocb,
struct iov_iter *from)
{
struct tun_file *tfile = file->private_data;
struct tun_struct *tun = tun_get(tfile);
/* iov_iter → sk_buff 변환 후 네트워크 스택 진입 */
result = tun_get_user(tun, tfile, ...);
return result;
}
/* tun_get_user() 세부 로직:
* 1. IFF_VNET_HDR → virtio_net_hdr 파싱 (GSO 메타데이터)
* 2. !IFF_NO_PI → struct tun_pi 파싱 (proto 추출)
* 3. alloc_skb() + skb_copy_datagram_from_iter()
* 4. IFF_TUN → skb->protocol = tun_pi.proto (또는 IP 버전 감지)
* IFF_TAP → eth_type_trans() 호출
* 5. netif_rx() → 커널 네트워크 스택 진입
*/
/* drivers/net/tun.c — Read 경로: 커널에서 유저스페이스로 패킷 전달 */
static ssize_t tun_do_read(
struct tun_struct *tun,
struct tun_file *tfile,
struct iov_iter *to)
{
struct sk_buff *skb;
/* sk_receive_queue에서 패킷 대기/dequeue */
skb = skb_dequeue(&tfile->sk_receive_queue);
/* !IFF_NO_PI → tun_pi 헤더 먼저 복사 */
/* IFF_VNET_HDR → virtio_net_hdr 먼저 복사 */
/* skb_copy_datagram_iter() → 유저 버퍼에 패킷 복사 */
ret = tun_put_user(tun, tfile, skb, to);
consume_skb(skb);
return ret;
}
/* drivers/net/tun.c — TX 경로: 네트워크 스택이 TUN/TAP으로 패킷 전송 */
static netdev_tx_t tun_net_xmit(
struct sk_buff *skb,
struct net_device *dev)
{
struct tun_struct *tun = netdev_priv(dev);
struct tun_file *tfile;
/* 멀티큐: skb의 queue_mapping으로 tfile 선택 */
tfile = rcu_dereference(tun->tfiles[skb_get_queue_mapping(skb)]);
/* sndbuf 초과 시 드롭 */
if (skb_queue_len(&tfile->sk_receive_queue) >= dev->tx_queue_len)
goto drop;
/* sk_receive_queue에 enqueue → 유저스페이스 read() 대기 깨움 */
skb_queue_tail(&tfile->sk_receive_queue, skb);
wake_up_interruptible_poll(&tfile->socket.wq.wait, ...);
return NETDEV_TX_OK;
}
poll()/epoll()/select()를 완벽히 지원합니다. POLLIN은 sk_receive_queue에 패킷이 있을 때, POLLOUT은 sndbuf 여유가 있을 때 발생합니다. 고성능 VPN/가상화 애플리케이션은 epoll 기반 이벤트 루프(Event Loop)로 TUN/TAP fd를 관리합니다.
멀티큐 및 vhost-net
멀티큐 TUN/TAP은 가상화 환경에서 네트워크 처리량(Throughput)을 크게 향상시킵니다. IFF_MULTI_QUEUE 플래그로 생성한 디바이스에 여러 fd를 attach하면 각 큐가 독립적으로 패킷을 처리합니다.
/* 멀티큐 TUN/TAP 설정 — 큐마다 fd를 하나씩 열어 attach */
int tun_alloc_mq(char *dev, int queues, int *fds)
{
struct ifreq ifr;
int fd, i;
memset(&ifr, 0, sizeof(ifr));
strncpy(ifr.ifr_name, dev, IFNAMSIZ);
ifr.ifr_flags = IFF_TAP | IFF_NO_PI | IFF_VNET_HDR | IFF_MULTI_QUEUE;
for (i = 0; i < queues; i++) {
fd = open("/dev/net/tun", O_RDWR);
ioctl(fd, TUNSETIFF, &ifr); /* 동일 이름 → 큐 추가 */
fds[i] = fd;
}
return 0;
}
vhost-net은 TAP 디바이스의 I/O 경로를 최적화하는 커널 모듈입니다. 일반적인 TAP은 유저스페이스(QEMU)를 거쳐 패킷을 중계하지만, vhost-net은 커널 내부의 vhost 워커 스레드(Thread)가 virtio ring과 TAP 소켓(Socket)을 직접 연결하여 컨텍스트 스위칭(Context Switching)을 제거합니다.
# QEMU에서 vhost-net 활용 (멀티큐 TAP)
# 1. TAP 디바이스 생성
ip tuntap add dev tap0 mode tap multi_queue vnet_hdr user qemu
# 2. QEMU 실행 (4큐 + 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
# 3. 게스트 내부에서 멀티큐 활성화
ethtool -L eth0 combined 4
/dev/vhost-net 디바이스 파일과 vhost_net 모듈이 필요합니다.
고급 설정 및 디버깅(Debugging)
# Persistent TUN/TAP — fd 닫아도 유지
ip tuntap add dev tap0 mode tap
ip link set tap0 up
tunctl -p tap0 # 또는 ioctl(fd, TUNSETPERSIST, 1)
# 소유자/그룹 설정 (비루트 사용자 접근 허용)
ip tuntap add dev tap0 mode tap user nobody group kvm
# offload 기능 설정 (QEMU virtio-net 최적화)
ethtool -K tap0 tx-checksum-ip-generic on
ethtool -K tap0 tso on gso on
# tcpdump로 TUN/TAP 트래픽 캡처
tcpdump -i tap0 -nn -e -v
# sysfs를 통한 파라미터 확인
cat /sys/class/net/tap0/tun_flags
cat /sys/class/net/tap0/type # 1=이더넷(TAP), 65534=TUN
cat /sys/class/net/tap0/owner
cat /sys/class/net/tap0/group
# 통계 확인
ip -s link show tap0
cat /sys/class/net/tap0/statistics/tx_packets
cat /sys/class/net/tap0/statistics/rx_dropped
- NO-CARRIER: TUN/TAP fd를 열고 있는 프로세스가 없으면 인터페이스가
NO-CARRIER상태가 됩니다. persistent 모드에서도 fd를 닫으면 carrier가 내려갑니다.ip link show tap0으로 확인하세요. - MTU 불일치: TUN/TAP의 기본 MTU는 1500입니다. VPN 캡슐화(IPsec, GRE) 시 오버헤드(Overhead)를 고려하여
ip link set tun0 mtu 1400으로 조정하세요. - Permission denied:
/dev/net/tun은 기본root:root 0666이지만, 일부 배포판에서0660으로 제한됩니다.TUNSETOWNER/TUNSETGROUP또는 udev 규칙으로 해결합니다.
IPVLAN 모드별 동작 상세
IPVLAN은 3가지 모드를 지원하며, 각 모드는 패킷 처리 계층이 다릅니다. 모든 IPVLAN 인터페이스는 하위 디바이스(parent)의 MAC 주소를 공유합니다.
| 항목 | L2 | L3 | L3S |
|---|---|---|---|
| 패킷 처리 계층 | L2 (이더넷) | L3 (IP 라우팅) | L3 + Netfilter |
| ARP/NDP 처리 | 각 IPVLAN이 응답 | 커널 라우팅 테이블(Routing Table) 기반 | 커널 라우팅 테이블 기반 |
| 브로드캐스트/멀티캐스트 | 지원 | 미지원 (L3 only) | 미지원 |
| Netfilter (iptables/nftables) | 미지원 | 미지원 | 지원 (conntrack 포함) |
| IPVLAN 간 통신 | L2 브리징 | 라우팅 기반 | 라우팅 기반 |
| DHCP | 지원 (L2이므로) | 미지원 | 미지원 |
| 사용 사례 | 일반 컨테이너 | 고성능 라우팅 | Kubernetes, 서비스 메시 |
IPVLAN 커널 자료 구조
IPVLAN은 ipvl_port(물리 NIC당 하나), ipvl_dev(가상 인터페이스당 하나), ipvl_addr(IP 주소당 하나) 세 가지 핵심 구조체로 구성됩니다. MACVLAN과 달리 MAC이 아닌 IP 주소로 디바이스를 식별하므로, IP 주소 해시 테이블이 핵심입니다.
/* drivers/net/ipvlan/ipvlan.h — 물리 NIC당 하나 */
struct ipvl_port {
struct net_device *dev; /* parent device (예: eth0) */
struct hlist_head hlhead[IPVLAN_HASH_SIZE]; /* IP→ipvl_addr 해시 */
struct list_head ipvlans; /* ipvl_dev 연결 리스트 */
u16 mode; /* L2 / L3 / L3S */
u16 flags; /* IPVLAN_F_PRIVATE 등 */
struct sk_buff_head backlog; /* 큐잉된 패킷 */
int count; /* 등록된 IPVLAN 수 */
};
/* drivers/net/ipvlan/ipvlan.h — 가상 인터페이스당 하나 */
struct ipvl_dev {
struct net_device *dev; /* 이 IPVLAN의 net_device */
struct list_head pnode; /* ipvl_port->ipvlans 링크 */
struct ipvl_port *port; /* 소속 ipvl_port 참조 */
struct net_device *phy_dev; /* parent 물리 디바이스 */
struct list_head addrs; /* ipvl_addr 목록 (이 dev의 IP들) */
struct ipvl_pcpu_stats __percpu *pcpu_stats; /* per-CPU 통계 */
DECLARE_BITMAP( mac_filters, IPVLAN_MAC_FILTER_SIZE);
};
/* drivers/net/ipvlan/ipvlan.h — IP 주소당 하나 */
struct ipvl_addr {
struct ipvl_dev *master; /* 소속 ipvl_dev */
union {
struct in6_addr ip6; /* IPv6 주소 */
struct in_addr ip4; /* IPv4 주소 */
} ipu;
struct hlist_node hlnode; /* IP 해시 테이블 링크 */
struct list_head anode; /* ipvl_dev->addrs 링크 */
ip_addr_type atype; /* IPv4 / IPv6 */
};
IPVLAN TX 경로
IPVLAN의 TX 경로는 모드에 따라 다릅니다. L2 모드는 dev_forward_skb()로 parent에 전달하고, L3/L3S 모드는 ipvlan_process_v4_outbound()로 커널 라우팅 스택을 직접 호출합니다.
/* drivers/net/ipvlan/ipvlan_core.c — IPVLAN TX 경로 */
static netdev_tx_t ipvlan_start_xmit(struct sk_buff *skb,
struct net_device *dev)
{
struct ipvl_dev *ipvlan = netdev_priv(dev);
struct ipvl_port *port = ipvlan->port;
int skblen = skb->len;
/* 모드별 TX 분기 */
switch (port->mode) {
case IPVLAN_MODE_L2:
/* L2: dst MAC 확인 → 같은 port의 다른 IPVLAN이면 직접 전달
* 외부 목적지면 parent(lowerdev)로 전달 */
ipvlan_xmit_mode_l2(skb, dev);
break;
case IPVLAN_MODE_L3:
case IPVLAN_MODE_L3S:
/* L3/L3S: skb를 IP 라우팅 스택에 직접 주입
* L3: netfilter 우회 → ipvlan_process_outbound()
* L3S: NF_HOOK() 통과 → conntrack 적용 */
ipvlan_xmit_mode_l3(skb, dev);
break;
}
/* 통계 갱신 */
ipvlan_count_tx(ipvlan, skblen, true);
return NETDEV_TX_OK;
}
/* L3 모드 TX: 커널 라우팅 스택에 직접 진입 */
static int ipvlan_process_v4_outbound(struct sk_buff *skb)
{
const struct iphdr *ip4h = ip_hdr(skb);
struct net *net = dev_net(skb->dev);
struct rtable *rt;
struct flowi4 fl4;
int err;
/* dst IP로 라우팅 테이블 조회 */
memset(&fl4, 0, sizeof(fl4));
fl4.daddr = ip4h->daddr;
fl4.saddr = ip4h->saddr;
rt = ip_route_output_flow(net, &fl4, NULL);
if (IS_ERR(rt))
return PTR_ERR(rt);
/* 라우팅 결과에 따라 패킷 전송
* 같은 호스트의 다른 IPVLAN → 직접 전달
* 외부 → parent의 TX 큐로 전송 */
skb_dst_set(skb, &rt->dst);
err = ip_local_out(net, skb->sk, skb);
return err;
}
- L3:
ipvlan_process_v4_outbound()에서ip_local_out()을 직접 호출합니다. Netfilter(OUTPUT chain)를 우회하므로 iptables 규칙이 적용되지 않습니다 - L3S:
ip_local_out()호출 전에NF_HOOK(NFPROTO_IPV4, NF_INET_LOCAL_OUT, ...)를 통과합니다. conntrack이 연결을 추적하고 iptables/nftables 규칙이 적용됩니다 - 성능 차이: L3S는 Netfilter 통과 오버헤드로 L3 대비 약 5~10% 낮은 처리량을 보이지만, Kubernetes Service(kube-proxy) 연동이 가능합니다
IPVLAN RX 핸들러
/* drivers/net/ipvlan/ipvlan_core.c — IPVLAN RX 모드 분기 */
rx_handler_result_t ipvlan_handle_frame(struct sk_buff **pskb)
{
struct ipvl_port *port = ipvlan_port_get_rcu(skb->dev);
switch (port->mode) {
case IPVLAN_MODE_L2:
return ipvlan_handle_mode_l2(pskb, port);
/* dst MAC 확인 → 브로드캐스트면 모든 IPVLAN에 복제
* 유니캐스트면 IP로 IPVLAN 디바이스 탐색 → 전달 */
case IPVLAN_MODE_L3:
return ipvlan_handle_mode_l3(pskb, port);
/* L3 헤더만 확인 → dst IP로 IPVLAN 디바이스 탐색
* netfilter 통과 안 함 → 고성능 */
case IPVLAN_MODE_L3S:
return ipvlan_handle_mode_l3(pskb, port);
/* L3와 동일하지만 NF_HOOK()을 통과 → iptables/conntrack 적용
* skb->skb_iif를 IPVLAN 디바이스로 설정하여 per-device 규칙 적용 */
}
return RX_HANDLER_PASS;
}
# IPVLAN L3S로 컨테이너 격리 + iptables 사용
ip netns add container1
ip link add ipvl0 link eth0 type ipvlan mode l3s
ip link set ipvl0 netns container1
ip netns exec container1 ip addr add 192.168.1.10/32 dev ipvl0
ip netns exec container1 ip link set ipvl0 up
ip netns exec container1 ip route add default dev ipvl0
# 호스트에서 라우팅 추가
ip route add 192.168.1.10/32 dev eth0
# container1 내부에서 iptables 사용 가능 (L3S이므로)
ip netns exec container1 iptables -A INPUT -p tcp --dport 80 -j ACCEPT
veth XDP와 성능 최적화
커널 5.0부터 veth는 native XDP를 지원합니다. peer에서 전송한 패킷이 네트워크 스택에 진입하기 전에 XDP 프로그램을 실행할 수 있어, 컨테이너 간 고성능 패킷 처리(로드밸런싱, 필터링, 리다이렉트)가 가능합니다.
/* drivers/net/veth.c — veth XDP 수신 경로 */
static int veth_xdp_rcv(struct veth_rq *rq,
int budget,
struct veth_xdp_tx_bq *bq)
{
int i, done = 0;
for (i = 0; i < budget; i++) {
struct xdp_frame *frame = veth_xdp_rcv_one(rq, ...);
/* XDP 프로그램 실행 */
act = bpf_prog_run_xdp(xdp_prog, &xdp);
switch (act) {
case XDP_PASS:
/* 정상 — sk_buff로 변환 후 네트워크 스택 진입 */
napi_gro_receive(&rq->xdp_napi, skb);
break;
case XDP_TX:
/* peer로 다시 전송 (bounce back) */
veth_xdp_tx(rq, &xdp, bq);
break;
case XDP_REDIRECT:
/* 다른 인터페이스로 리다이렉트 (BPF map 기반) */
xdp_do_redirect(rq->dev, &xdp, xdp_prog);
break;
case XDP_DROP:
/* 패킷 드롭 — 네트워크 스택 진입 없이 즉시 해제 */
xdp_return_frame(frame);
break;
}
done++;
}
return done;
}
# veth에 XDP 프로그램 로드 (컨테이너 측 인터페이스에 attach)
ip link set veth1 xdpgeneric obj xdp_drop.o sec xdp
# native XDP (더 빠름 — veth 전용 드라이버 지원)
ip link set veth1 xdp obj xdp_prog.o sec xdp
# XDP 상태 확인
ip link show veth1 | grep xdp
# veth 성능 튜닝
# 1. GRO 활성화 (기본 on)
ethtool -K veth0 gro on
# 2. TSO 활성화
ethtool -K veth0 tso on
# 3. TX queue length 조정 (대량 트래픽 시)
ip link set veth0 txqueuelen 10000
# 4. 체크섬 오프로드 (veth 내부에서는 불필요하므로 off 가능)
ethtool -K veth0 tx-checksum-ip-generic off
- 일반 경로: ~3~5 Mpps (NAPI 미사용, softirq 기반)
- GRO 활성화: ~8~12 Mpps (대용량 패킷 병합)
- XDP: ~15~24 Mpps (커널 스택 우회, 제로카피)
- XDP_REDIRECT: 컨테이너 간 직접 전달로 브리지 오버헤드 제거
- Cilium, Calico 등 CNI 플러그인이 veth+XDP 조합을 활용
MACVLAN/IPVLAN L2/L3/L3S
MACVLAN과 IPVLAN은 컨테이너 네트워킹에서 브리지 오버헤드 없이 고성능 네트워크 연결을 제공합니다. 패킷 경로, 성능 특성, 컨테이너 런타임과의 통합 방법을 상세히 비교합니다.
MACVLAN 패킷 경로 상세
/* drivers/net/macvlan.c — MACVLAN TX 경로 */
static netdev_tx_t macvlan_start_xmit(struct sk_buff *skb,
struct net_device *dev)
{
struct macvlan_dev *vlan = netdev_priv(dev);
struct macvlan_port *port = vlan->port;
const struct ethhdr *eth = (void *)skb->data;
/* bridge 모드: dst MAC이 다른 MACVLAN이면 직접 전달 */
if (vlan->mode == MACVLAN_MODE_BRIDGE) {
struct macvlan_dev *dest;
dest = macvlan_hash_lookup(port, eth->h_dest);
if (dest && dest != vlan) {
/* 같은 호스트의 다른 MACVLAN → 물리 NIC을 거치지 않고 직접 전달 */
skb->dev = dest->dev;
dev_forward_skb(dest->dev, skb);
return NET_XMIT_SUCCESS;
}
}
/* vepa/private 모드: 항상 물리 NIC으로 전송 */
skb->dev = vlan->lowerdev;
return dev_queue_xmit(skb);
}
IPVLAN L2/L3/L3S 패킷 경로 비교
컨테이너에서의 MACVLAN/IPVLAN 활용
# Docker에서 MACVLAN 네트워크 생성
docker network create -d macvlan \
--subnet=192.168.1.0/24 \
--gateway=192.168.1.1 \
-o parent=eth0 \
-o macvlan_mode=bridge \
my_macvlan
docker run --network=my_macvlan --ip=192.168.1.100 nginx
# Docker에서 IPVLAN 네트워크 생성
docker network create -d ipvlan \
--subnet=192.168.1.0/24 \
--gateway=192.168.1.1 \
-o parent=eth0 \
-o ipvlan_mode=l3s \
my_ipvlan
# MACVLAN의 한계: 호스트 ↔ 컨테이너 직접 통신 불가
# 해결: 호스트에도 별도의 MACVLAN 인터페이스 생성
ip link add macvlan-host link eth0 type macvlan mode bridge
ip addr add 192.168.1.250/32 dev macvlan-host
ip link set macvlan-host up
ip route add 192.168.1.100/32 dev macvlan-host
# 성능 비교 (iperf3 기준, 일반적 수치)
# MACVLAN bridge: ~95% of line rate (최소 오버헤드)
# IPVLAN L3: ~97% of line rate (L3 직접 전달)
# IPVLAN L3S: ~93% (netfilter 통과 오버헤드)
# veth + bridge: ~85% (브리지 + netfilter 오버헤드)
MACVLAN/IPVLAN 성능 비교 상세
| 모드 | Throughput (10G 기준) | PPS (64B) | Latency | CPU 사용률 |
|---|---|---|---|---|
| MACVLAN bridge | 9.4 Gbps (~94%) | ~12 Mpps | <10us 추가 | 낮음 |
| MACVLAN passthru | 9.7 Gbps (~97%) | ~14 Mpps | <5us 추가 | 최소 |
| MACVLAN private | 9.4 Gbps (~94%) | ~12 Mpps | <10us 추가 | 낮음 |
| MACVLAN vepa | 9.0 Gbps (~90%) | ~11 Mpps | <15us 추가 | 중간 (헤어핀) |
| IPVLAN L2 | 9.2 Gbps (~92%) | ~11 Mpps | <12us 추가 | 낮음 |
| IPVLAN L3 | 9.6 Gbps (~96%) | ~13 Mpps | <8us 추가 | 최소 |
| IPVLAN L3S | 9.1 Gbps (~91%) | ~10 Mpps | <15us 추가 | 중간 (netfilter) |
| veth + bridge | 8.2 Gbps (~82%) | ~8 Mpps | <25us 추가 | 높음 |
- Calico IPVLAN: L3S 모드로 iptables/eBPF policy 적용 가능, DHCP 불필요 환경에 적합
- Multus MACVLAN: 보조 네트워크(스토리지, 관리)에 물리 네트워크 직접 연결 시 사용
- MACVLAN 한계: 호스트 ↔ 컨테이너 직접 통신 불가 (동일 parent에서 hairpin 미지원)
- IPVLAN L3 한계: broadcast/multicast 미지원으로 DHCP, mDNS 사용 불가
- MAC 주소 제한 환경: 클라우드 VM에서 MAC 수 제한이 있으면 IPVLAN 필수 (MAC 공유)
가상 NIC 성능 비교
각 가상 네트워크 인터페이스의 성능과 오버헤드를 비교합니다. 실제 성능은 하드웨어, 커널 버전, 워크로드에 따라 달라지지만, 상대적 순위와 특성은 일관됩니다.
| 인터페이스 | TX 오버헤드 | RX 오버헤드 | Throughput (상대) | Latency | XDP 지원 |
|---|---|---|---|---|---|
| MACVLAN | 매우 낮음 | MAC hash lookup | 95~98% | 최소 | O (passthru) |
| IPVLAN L3 | 낮음 | IP lookup | 93~97% | 최소 | O |
| veth + bridge | 중간 | bridge FDB lookup | 80~90% | 낮음 | O |
| veth + XDP | 낮음 | XDP native | 90~95% | 최소 | O (native) |
| TAP + vhost-net | 중간 | vhost worker | 70~85% | 중간 | X |
| TAP (userspace) | 높음 | 유저 read() | 40~60% | 높음 | X |
| TUN | 높음 | 유저 read() | 40~60% | 높음 | X |
기술 종합 비교
| 기술 | 목적 | 계층 | MAC 주소 | 네임스페이스 이동 | 주요 사용처 |
|---|---|---|---|---|---|
| MACVLAN | 가상 NIC (MAC 분리) | L2 | 고유 MAC/인터페이스 | 가능 | 컨테이너, VM NIC |
| IPVLAN | 가상 NIC (IP 분리) | L2/L3 | 공유 (parent MAC) | 가능 | 클라우드, MAC 제한 환경 |
| veth | 네임스페이스 연결 | L2 | 고유 MAC/쌍 | 가능 | 컨테이너 네트워킹 (Docker, K8s) |
| TUN | 유저스페이스 L3 I/O | L3 | 없음 | 가능 | VPN (OpenVPN, WireGuard) |
| TAP | 유저스페이스 L2 I/O | L2 | 고유 MAC | 가능 | VM NIC (QEMU/KVM) |
- Docker: veth pair + Linux Bridge + iptables NAT
- Kubernetes: veth pair + XDP(Cilium) 또는 IPVLAN L3S(Calico)
- VM (KVM): TAP + vhost-net + macvtap 또는 Bridge
- VPN 게이트웨이: TUN + Bonding(active-backup) + 라우팅
- 고성능 컨테이너: MACVLAN(bridge) 또는 IPVLAN(L3) — 브리지 오버헤드 없음
- 멀티테넌트 격리: MACVLAN(private) 또는 IPVLAN + 네트워크 네임스페이스
MACVLAN vs IPVLAN 패킷 경로 비교
MACVLAN과 IPVLAN은 동일한 목적(가상 NIC 생성)이지만, 패킷 경로가 근본적으로 다릅니다. MACVLAN은 목적지 MAC 주소로, IPVLAN은 목적지 IP 주소로 가상 인터페이스를 식별합니다. 아래 다이어그램은 TX/RX 양방향 경로를 비교합니다.
veth pair 네임스페이스 간 패킷 전달 상세
veth pair의 핵심은 veth_xmit()에서 peer 디바이스로 패킷을 직접 전달하는 메커니즘입니다. NAPI 폴링(Polling), XDP, GRO 등 최적화 경로를 포함한 전체 패킷 흐름을 분석합니다.
veth_poll() NAPI 폴링 경로
커널 5.0+에서 veth는 NAPI 기반 수신을 지원합니다. veth_poll()은 XDP 프로그램 실행과 GRO 병합을 통해 성능을 크게 향상시킵니다.
/* drivers/net/veth.c — veth NAPI 폴링 */
static int veth_poll(struct napi_struct *napi, int budget)
{
struct veth_rq *rq = container_of(napi, struct veth_rq, xdp_napi);
struct veth_xdp_tx_bq bq;
int done;
bq.count = 0;
/* XDP 프로그램이 있으면 veth_xdp_rcv()로 처리
* XDP_PASS: sk_buff로 변환 후 GRO
* XDP_DROP: 즉시 해제 (네트워크 스택 미진입)
* XDP_TX: peer로 반송
* XDP_REDIRECT: BPF 맵 기반 리다이렉트 */
done = veth_xdp_rcv(rq, budget, &bq);
/* XDP_TX로 반송된 패킷 일괄 전송 */
if (bq.count)
veth_xdp_flush_bq(rq, &bq);
/* 처리량이 budget 미만이면 NAPI 완료 */
if (done < budget && napi_complete_done(napi, done)) {
/* 큐에 패킷이 남아있으면 다시 스케줄 */
if (unlikely(!skb_queue_empty_lockless(&rq->xdp_ring)))
napi_schedule(napi);
}
return done;
}
/* veth_forward_skb() — peer로 패킷 전달 핵심 함수 */
static int veth_forward_skb(struct net_device *dev,
struct sk_buff *skb,
struct veth_rq *rq,
bool xdp)
{
/* NAPI + XDP 활성화 시: xdp_ring에 enqueue 후 NAPI 스케줄 */
if (xdp) {
skb_queue_tail(&rq->xdp_ring, skb);
napi_schedule(&rq->xdp_napi);
return NET_RX_SUCCESS;
}
/* 일반 경로: netif_rx()로 직접 전달 */
return __dev_forward_skb(dev, skb) ?: netif_rx(skb);
}
TUN/TAP 커널-사용자 공간 패킷 흐름 상세
TUN/TAP의 핵심 흐름은 크게 세 가지입니다: 유저 write() → 커널 주입(tun_chr_write_iter → tun_get_user), 커널 TX → 유저 read()(tun_net_xmit → sk_receive_queue → tun_do_read), 커널 내부 라우팅(tun_net_xmit이 패킷을 큐에 넣고 유저를 깨움). 아래 다이어그램은 양방향 경로를 상세히 보여줍니다.
컨테이너 네트워킹에서 가상 NIC 사용 구조
Docker, Kubernetes, Podman 등 컨테이너 런타임은 가상 NIC를 조합하여 네트워크를 구성합니다. 각 런타임의 기본 네트워크 모델과 가상 NIC 활용 구조를 비교합니다.
Docker/Podman MACVLAN 네트워크 설정
Docker와 Podman에서 MACVLAN 네트워크를 생성하면 컨테이너가 물리 네트워크에 직접 연결되어 브리지/NAT 오버헤드 없이 고성능 통신이 가능합니다.
# === Docker MACVLAN 네트워크 ===
# 1. MACVLAN 네트워크 생성 (bridge 모드)
docker network create -d macvlan \
--subnet=192.168.1.0/24 \
--gateway=192.168.1.1 \
--ip-range=192.168.1.128/25 \
-o parent=eth0 \
-o macvlan_mode=bridge \
net-macvlan
# 2. 컨테이너 실행
docker run -d --network=net-macvlan --ip=192.168.1.130 --name web nginx
docker run -d --network=net-macvlan --ip=192.168.1.131 --name app myapp
# 3. 컨테이너간 직접 통신 확인
docker exec web ping -c 3 192.168.1.131
# 4. 호스트 ↔ 컨테이너 통신 해결 (별도 MACVLAN 인터페이스)
ip link add macvlan-shim link eth0 type macvlan mode bridge
ip addr add 192.168.1.250/32 dev macvlan-shim
ip link set macvlan-shim up
ip route add 192.168.1.128/25 dev macvlan-shim
# === Docker IPVLAN 네트워크 ===
# IPVLAN L3S (Netfilter 지원, Kubernetes 호환)
docker network create -d ipvlan \
--subnet=10.100.0.0/16 \
--gateway=10.100.0.1 \
-o parent=eth0 \
-o ipvlan_mode=l3s \
net-ipvlan
docker run -d --network=net-ipvlan --ip=10.100.1.10 --name svc myservice
# === Podman MACVLAN (rootless 지원) ===
# Podman은 rootless 모드에서도 MACVLAN 가능 (slirp4netns 대신)
podman network create \
--driver macvlan \
--subnet 192.168.1.0/24 \
--gateway 192.168.1.1 \
-o parent=eth0 \
podman-macvlan
podman run -d --network podman-macvlan --ip 192.168.1.140 nginx
# === 802.1Q VLAN + MACVLAN (VLAN 분리) ===
# 물리 NIC에 VLAN 서브인터페이스 생성 후 parent로 지정
ip link add link eth0 name eth0.100 type vlan id 100
ip link set eth0.100 up
docker network create -d macvlan \
--subnet=10.100.0.0/24 \
--gateway=10.100.0.1 \
-o parent=eth0.100 \
net-vlan100
Kubernetes Multus + MACVLAN/IPVLAN 활용
Kubernetes에서 Multus CNI는 Pod에 여러 네트워크 인터페이스를 할당합니다. 기본 CNI(Cilium/Calico)로 클러스터 네트워크를 처리하고, Multus로 MACVLAN/IPVLAN 보조 네트워크를 추가합니다.
# === Multus NetworkAttachmentDefinition: MACVLAN ===
# /etc/cni/multus/net-conf.d/macvlan-storage.yaml
apiVersion: k8s.cni.cncf.io/v1
kind: NetworkAttachmentDefinition
metadata:
name: storage-net
spec:
config: '{
"cniVersion": "0.3.1",
"type": "macvlan",
"master": "eth1",
"mode": "bridge",
"ipam": {
"type": "host-local",
"subnet": "10.200.0.0/24",
"rangeStart": "10.200.0.100",
"rangeEnd": "10.200.0.200"
}
}'
# === Multus NetworkAttachmentDefinition: IPVLAN L3S ===
apiVersion: k8s.cni.cncf.io/v1
kind: NetworkAttachmentDefinition
metadata:
name: dataplane-net
spec:
config: '{
"cniVersion": "0.3.1",
"type": "ipvlan",
"master": "bond0",
"mode": "l3s",
"ipam": {
"type": "whereabouts",
"range": "10.244.0.0/16"
}
}'
# === Pod에 보조 네트워크 연결 ===
apiVersion: v1
kind: Pod
metadata:
name: db-pod
annotations:
k8s.v1.cni.cncf.io/networks: storage-net
spec:
containers:
- name: postgres
image: postgres:16
# Pod 내부: eth0(기본 CNI) + net1(storage-net MACVLAN)
# === SR-IOV + Multus (고성능) ===
# SR-IOV VF를 Pod에 직접 할당하여 line-rate 성능 달성
apiVersion: k8s.cni.cncf.io/v1
kind: NetworkAttachmentDefinition
metadata:
name: sriov-net
annotations:
k8s.v1.cni.cncf.io/resourceName: intel.com/sriov_net
spec:
config: '{
"cniVersion": "0.3.1",
"type": "sriov",
"vlan": 100,
"ipam": {"type": "host-local", "subnet": "10.10.0.0/24"}
}'
macvtap — MACVLAN + TAP 결합
macvtap은 MACVLAN과 TAP을 결합한 디바이스입니다. MACVLAN처럼 물리 NIC 위에 고유 MAC 주소를 가진 가상 인터페이스를 생성하면서, TAP처럼 /dev/tapN 캐릭터 디바이스를 통해 유저스페이스에서 직접 패킷 I/O가 가능합니다. QEMU/KVM의 VM NIC으로 사용하면 기존 TAP+Bridge 구조보다 단순하고 빠릅니다.
macvtap 커널 구현
macvtap은 drivers/net/macvtap.c에서 구현됩니다. MACVLAN의 net_device 위에 TAP의 캐릭터 디바이스 인터페이스를 올린 구조입니다. macvtap_open()이 /dev/tapN을 열 때 내부적으로 MACVLAN 디바이스와 연결됩니다.
/* drivers/net/macvtap.c — macvtap 핵심 구조 (TAP 인터페이스) */
/* macvtap은 macvlan_dev를 상속하고 TAP 기능을 추가합니다.
* TAP 캐릭터 디바이스 → macvtap_queue → macvlan_dev → lowerdev
*
* macvtap_queue: fd당 하나 (멀티큐 지원)
* - socket 구조체로 vhost-net과 연동
* - sk_receive_queue로 커널→유저 패킷 전달
*
* 패킷 흐름 (TX — 유저→커널):
* 1. write(fd) → macvtap_write_iter()
* 2. → macvtap_get_user() → skb 변환
* 3. → macvlan_start_xmit() → lowerdev로 전송
*
* 패킷 흐름 (RX — 커널→유저):
* 1. lowerdev RX → macvlan_handle_frame()
* 2. → macvtap_receive() → sk_receive_queue에 enqueue
* 3. read(fd) → macvtap_read_iter() → 유저에게 전달
*/
/* macvtap의 net_device_ops — MACVLAN 오퍼레이션 재사용 */
static const struct net_device_ops macvtap_netdev_ops = {
.ndo_init = macvtap_init,
.ndo_open = macvtap_open,
.ndo_stop = macvtap_stop,
.ndo_start_xmit = macvlan_start_xmit, /* MACVLAN TX 재사용 */
.ndo_set_rx_mode = macvlan_set_mac_filter, /* MACVLAN 필터 재사용 */
.ndo_get_stats64 = macvtap_get_stats64,
.ndo_features_check = passthru_features_check,
};
/* macvtap은 misc 디바이스가 아닌 cdev로 /dev/tapN 생성 */
/* N은 net_device의 ifindex와 동일 */
# === macvtap 디바이스 생성 ===
# 1. macvtap 인터페이스 생성 (bridge 모드)
ip link add link eth0 name macvtap0 type macvtap mode bridge
ip link set macvtap0 up
# 2. /dev/tapN 캐릭터 디바이스 확인 (N = ifindex)
ip link show macvtap0 | grep -o 'index [0-9]*' # ifindex 확인
ls -la /dev/tap* # 해당 ifindex의 /dev/tapN 존재 확인
# 3. QEMU에서 macvtap 사용
qemu-system-x86_64 \
-netdev tap,id=net0,fd=3,vhost=on \
-device virtio-net-pci,netdev=net0,mac=52:54:00:12:34:56 \
3<>/dev/tap$(cat /sys/class/net/macvtap0/ifindex)
# 4. libvirt에서 macvtap 사용 (가장 일반적)
# /etc/libvirt/qemu/vm.xml 에서:
# <interface type="direct">
# <source dev="eth0" mode="bridge"/>
# <model type="virtio"/>
# </interface>
# → libvirt가 자동으로 macvtap 생성·관리
# === macvtap 모드별 설정 ===
# bridge 모드 — VM 간 직접 통신 O, 호스트↔VM 직접 통신 X
ip link add link eth0 name mvt0 type macvtap mode bridge
# vepa 모드 — 모든 트래픽이 외부 스위치 경유 (IEEE 802.1Qbg)
ip link add link eth0 name mvt1 type macvtap mode vepa
# passthru 모드 — 물리 NIC 1:1 할당 (SR-IOV VF에 적합)
ip link add link eth0 name mvt2 type macvtap mode passthru
# private 모드 — VM 간 완전 격리
ip link add link eth0 name mvt3 type macvtap mode private
# 멀티큐 macvtap + vhost-net (고성능)
ip link add link eth0 name mvt-mq type macvtap mode bridge
# QEMU에서 queues=4,vhost=on으로 멀티큐 활성화
macvtap vs TAP+Bridge 성능 비교
| 항목 | TAP + Bridge | macvtap | macvtap + vhost-net |
|---|---|---|---|
| 패킷 경로 단계 | 3단계 (TAP → Bridge → NIC) | 2단계 (macvtap → NIC) | 2단계 (커널 직접) |
| Throughput (10G) | ~7.5 Gbps (~75%) | ~8.5 Gbps (~85%) | ~9.2 Gbps (~92%) |
| Latency 추가 | ~25~35us | ~15~20us | ~8~12us |
| CPU 사용률 | 높음 (Bridge FDB 룩업) | 중간 | 낮음 (vhost 워커) |
| 설정 복잡도 | 높음 (Bridge 생성·관리) | 낮음 (단일 명령) | 낮음 |
| VM 간 통신 | Bridge 내부 전달 | MACVLAN bridge 모드 | MACVLAN bridge 모드 |
| 호스트↔VM 통신 | 가능 (Bridge 경유) | 불가 (MACVLAN 제한) | 불가 |
- libvirt/KVM:
<interface type="direct">로 macvtap 자동 생성 — Bridge 설정 불필요, 가장 간편한 VM 네트워킹입니다 - SR-IOV + passthru: VF(Virtual Function)를 macvtap passthru로 VM에 할당하면 line-rate에 근접한 성능을 달성합니다
- 다수 VM 환경: bridge 모드로 VM 간 직접 통신이 가능합니다. Bridge를 별도 관리할 필요가 없습니다
- 호스트↔VM 통신이 필요한 경우: macvtap으로는 직접 통신이 불가하므로, 호스트에 별도 macvlan 인터페이스를 추가하거나 TAP+Bridge 방식을 사용합니다
WireGuard + TUN 인터페이스 연동
WireGuard는 커널 내장 VPN 모듈(drivers/net/wireguard/)로, 내부적으로 TUN과 유사한 가상 네트워크 인터페이스를 사용합니다. WireGuard 인터페이스는 wg0 등의 이름으로 생성되며, 커널 내에서 패킷 암호화/복호화를 처리합니다.
# === WireGuard 인터페이스 생성 및 설정 ===
# 1. 키 생성
wg genkey | tee privatekey | wg pubkey > publickey
# 2. 인터페이스 생성 (커널 네이티브)
ip link add wg0 type wireguard
ip addr add 10.8.0.1/24 dev wg0
wg set wg0 \
listen-port 51820 \
private-key ./privatekey \
peer "PEER_PUBLIC_KEY" \
endpoint "203.0.113.1:51820" \
allowed-ips "10.8.0.0/24" \
persistent-keepalive 25
ip link set wg0 up
# 3. MTU 설정 (IPv4: 1420, IPv6: 1400 권장)
ip link set wg0 mtu 1420
# 4. 라우팅 설정 (원격 서브넷 트래픽을 WireGuard 터널로)
ip route add 192.168.2.0/24 dev wg0
# 5. NAT/포워딩 (서버 역할)
sysctl -w net.ipv4.ip_forward=1
iptables -t nat -A POSTROUTING -s 10.8.0.0/24 -o eth0 -j MASQUERADE
# === WireGuard + systemd-networkd 영구 설정 ===
# /etc/systemd/network/90-wg0.netdev
[NetDev]
Name=wg0
Kind=wireguard
[WireGuard]
ListenPort=51820
PrivateKeyFile=/etc/wireguard/private.key
[WireGuardPeer]
PublicKey=PEER_PUBLIC_KEY
Endpoint=203.0.113.1:51820
AllowedIPs=10.8.0.0/24
PersistentKeepalive=25
# /etc/systemd/network/90-wg0.network
[Match]
Name=wg0
[Network]
Address=10.8.0.1/24
[Route]
Destination=192.168.2.0/24
# === OpenVPN (유저스페이스 TUN) vs WireGuard (커널 네이티브) 비교 ===
# OpenVPN: /dev/net/tun → 유저스페이스 read()/write() → 컨텍스트 스위칭
# WireGuard: 커널 내부 처리 → 소프트IRQ에서 직접 암호화/복호화
# 성능 차이: WireGuard ~3-4Gbps, OpenVPN ~500Mbps-1Gbps (동일 HW)
# === 상태 확인 ===
wg show wg0
ip -d link show wg0
ip route show dev wg0
- WireGuard: 커널 모듈 기반, ChaCha20/Poly1305 암호화, 컨텍스트 스위칭 없음, 최소 코드(~4,000줄)
- OpenVPN: 유저스페이스
/dev/net/tun기반, 패킷마다 유저↔커널 왕복, SSL/TLS 오버헤드 - 성능: WireGuard는 동일 하드웨어에서 OpenVPN 대비 3~5배 빠르며, 지연(Latency)도 크게 낮습니다
- 커널 통합: WireGuard는 Linux 5.6부터 메인라인 포함,
ip link add type wireguard로 바로 사용
가상 NIC 성능 최적화
가상 NIC의 성능을 최대화하기 위한 XDP, busy polling, GRO/TSO 등 최적화 기법을 정리합니다.
XDP (eXpress Data Path) 최적화
# === veth XDP 최적화 ===
# 1. native XDP를 veth에 로드 (드라이버 레벨, 최고 성능)
ip link set veth1 xdp obj xdp_redirect.o sec xdp
# 2. XDP_REDIRECT로 컨테이너 간 직접 전달 (브리지 우회)
# BPF 맵에 veth 인터페이스 인덱스를 등록하여
# 패킷을 직접 대상 veth로 리다이렉트
# → 브리지/라우팅 스택 전체 우회, ~15-24 Mpps 달성
# 3. XDP 상태 확인
ip link show veth1 | grep xdp
bpftool net show
bpftool prog show
# === Busy Polling (저지연 최적화) ===
# 소켓 레벨 busy polling으로 softirq 지연 제거
sysctl -w net.core.busy_poll=50 # 50us 동안 폴링
sysctl -w net.core.busy_read=50 # read() 시 50us 폴링
# 소켓 옵션으로 개별 설정
# setsockopt(fd, SOL_SOCKET, SO_BUSY_POLL, &timeout, sizeof(timeout));
# === GRO/TSO 최적화 ===
# veth/MACVLAN에서 GRO 활성화 (기본 on)
ethtool -K veth0 gro on tso on gso on
# MACVLAN에서 체크섬 오프로드
ethtool -K macvlan0 tx-checksum-ip-generic on
# TAP에서 offload 활성화 (QEMU virtio-net)
ethtool -K tap0 tx-checksum-ip-generic on tso on gso on
# === 커널 파라미터 튜닝 ===
# netdev_budget: NAPI 폴링 예산 (기본 300)
sysctl -w net.core.netdev_budget=600
# netdev_budget_usecs: NAPI 폴링 시간 제한 (기본 2000us)
sysctl -w net.core.netdev_budget_usecs=4000
# backlog 큐 크기 (기본 1000)
sysctl -w net.core.netdev_max_backlog=5000
# === TX 큐 길이 조정 ===
ip link set veth0 txqueuelen 10000 # 대량 트래픽 시 증가
ip link set macvlan0 txqueuelen 5000
성능 최적화 가이드
| 최적화 기법 | 적용 대상 | 효과 | 설정 |
|---|---|---|---|
| XDP native | veth, MACVLAN | ~15-24 Mpps | ip link set dev xdp obj prog.o |
| GRO | veth, TAP | 대용량 패킷 병합, 2~3x 처리량 | ethtool -K dev gro on |
| TSO/GSO | veth, TAP | TX 세그먼테이션 오프로드 | ethtool -K dev tso on gso on |
| Busy Polling | 모든 가상 NIC | 지연 50% 감소 | sysctl net.core.busy_poll=50 |
| vhost-net | TAP (KVM) | 2~5x 처리량 향상 | -netdev tap,vhost=on |
| 멀티큐 | TAP, veth | CPU 병렬 처리 | IFF_MULTI_QUEUE, ethtool -L dev combined N |
| TX 큐 확대 | 모든 가상 NIC | burst 트래픽 흡수 | ip link set dev txqueuelen 10000 |
| NAPI budget | 시스템 전체 | 폴링 효율 향상 | sysctl net.core.netdev_budget=600 |
네트워크 네임스페이스 + veth 수동 구성 실습
Docker/Kubernetes가 내부적으로 수행하는 네트워크 구성을 수동으로 재현하는 실습입니다. veth pair, 네트워크 네임스페이스, Bridge, NAT를 조합하여 컨테이너 네트워킹의 원리를 이해합니다.
실습 1: 기본 veth + 네임스페이스 통신
# === 2개 네임스페이스 직접 연결 ===
# 1. 네임스페이스 생성
ip netns add red
ip netns add blue
# 2. veth 쌍 생성
ip link add veth-red type veth peer name veth-blue
# 3. 각 네임스페이스에 배치
ip link set veth-red netns red
ip link set veth-blue netns blue
# 4. IP 설정
ip netns exec red ip addr add 10.0.0.1/24 dev veth-red
ip netns exec red ip link set veth-red up
ip netns exec red ip link set lo up
ip netns exec blue ip addr add 10.0.0.2/24 dev veth-blue
ip netns exec blue ip link set veth-blue up
ip netns exec blue ip link set lo up
# 5. 통신 테스트
ip netns exec red ping -c 3 10.0.0.2
# 6. 정리
ip netns del red
ip netns del blue
실습 2: Bridge + NAT (Docker 스타일 네트워킹)
# === Docker 네트워킹 수동 재현 ===
# 1. Bridge 생성
ip link add br0 type bridge
ip addr add 172.20.0.1/24 dev br0
ip link set br0 up
# 2. NAT 설정
sysctl -w net.ipv4.ip_forward=1
iptables -t nat -A POSTROUTING -s 172.20.0.0/24 ! -o br0 -j MASQUERADE
iptables -A FORWARD -i br0 -j ACCEPT
iptables -A FORWARD -o br0 -m conntrack --ctstate RELATED,ESTABLISHED -j ACCEPT
# 3. 컨테이너 1 생성
ip netns add c1
ip link add veth-c1-host type veth peer name eth0-c1
ip link set veth-c1-host master br0
ip link set veth-c1-host up
ip link set eth0-c1 netns c1
ip netns exec c1 bash -c "
ip link set lo up
ip link set eth0-c1 name eth0
ip addr add 172.20.0.10/24 dev eth0
ip link set eth0 up
ip route add default via 172.20.0.1
"
# 4. 컨테이너 2 생성
ip netns add c2
ip link add veth-c2-host type veth peer name eth0-c2
ip link set veth-c2-host master br0
ip link set veth-c2-host up
ip link set eth0-c2 netns c2
ip netns exec c2 bash -c "
ip link set lo up
ip link set eth0-c2 name eth0
ip addr add 172.20.0.11/24 dev eth0
ip link set eth0 up
ip route add default via 172.20.0.1
"
# 5. 테스트
ip netns exec c1 ping -c 2 172.20.0.11 # c1 → c2 (브리지 내부)
ip netns exec c1 ping -c 2 8.8.8.8 # c1 → 외부 (NAT 경유)
# 6. 포트 포워딩 (외부 → 컨테이너)
iptables -t nat -A PREROUTING -p tcp --dport 8080 -j DNAT --to 172.20.0.10:80
# 7. 정리
ip netns del c1 && ip netns del c2
ip link del br0
iptables -t nat -F
실습 3: MACVLAN 네임스페이스 격리
# === MACVLAN으로 물리 네트워크 직접 연결 ===
# 1. 네임스페이스 생성
ip netns add vm1
ip netns add vm2
# 2. MACVLAN 인터페이스 생성 (bridge 모드)
ip link add macvlan-vm1 link eth0 type macvlan mode bridge
ip link add macvlan-vm2 link eth0 type macvlan mode bridge
# 3. 네임스페이스에 배치
ip link set macvlan-vm1 netns vm1
ip link set macvlan-vm2 netns vm2
# 4. IP 설정 (물리 네트워크 대역 사용)
ip netns exec vm1 bash -c "
ip link set lo up
ip link set macvlan-vm1 name eth0
ip addr add 192.168.1.50/24 dev eth0
ip link set eth0 up
ip route add default via 192.168.1.1
"
ip netns exec vm2 bash -c "
ip link set lo up
ip link set macvlan-vm2 name eth0
ip addr add 192.168.1.51/24 dev eth0
ip link set eth0 up
ip route add default via 192.168.1.1
"
# 5. vm1 ↔ vm2 통신 (MACVLAN bridge 모드 → 직접 전달)
ip netns exec vm1 ping -c 3 192.168.1.51
# 6. vm1 ↔ 외부 통신 (물리 NIC 경유)
ip netns exec vm1 ping -c 3 8.8.8.8
# 7. MAC 주소 확인 (각 MACVLAN마다 고유 MAC)
ip netns exec vm1 ip link show eth0
ip netns exec vm2 ip link show eth0
# 정리
ip netns del vm1 && ip netns del vm2
- 실습 1 (veth 직접 연결): 두 네임스페이스 간 1:1 통신, 가장 단순한 구조
- 실습 2 (Bridge+NAT): Docker 기본 네트워킹과 동일한 구조 — 다수 컨테이너, 외부 통신, 포트 포워딩
- 실습 3 (MACVLAN): 물리 네트워크에 직접 연결 — NAT 없이 외부 통신, 고성능, 각 컨테이너 고유 MAC
- 실습 후 반드시
ip netns del로 네임스페이스를 정리하세요. 네임스페이스 삭제 시 내부 인터페이스도 자동 정리됩니다.
참고자료
- 커널 공식 문서: IPVLAN — IPVLAN 드라이버 공식 문서입니다
- drivers/net/macvlan.c — 커널 MACVLAN 구현 소스 코드입니다
- LWN: Network namespaces and veth (2012) — 네트워크 네임스페이스와 veth 구현에 대한 LWN 기사입니다
- man ip-link(8) — macvlan/ipvlan/veth/tun/tap 인터페이스 생성 매뉴얼 페이지입니다