L3 Master Device와 VRF 심화

L3 Master Device는 리눅스 커널의 네트워크 격리 프레임워크로, VRF(Virtual Routing and Forwarding)를 구현하는 핵심 기반입니다. 네트워크 네임스페이스 없이도 독립된 라우팅 테이블과 FIB 규칙을 인터페이스 단위로 분리할 수 있어, 멀티테넌트 환경, ISP PE(Provider Edge) 라우터, 데이터센터 오버레이 네트워크 등 대규모 라우팅 인프라에서 필수적으로 활용됩니다. l3mdev_ops 인터페이스 구조, VRF 디바이스의 내부 구현, FIB 규칙 자동 생성, 패킷 수신/송신 경로의 l3 훅, 소켓 바인딩 모델, FRR/BGP/OSPF 연동, MPLS/EVPN 고급 기능, 디버깅 기법까지 전 영역을 심층적으로 다룹니다.

관련 표준: RFC 4364 (BGP/MPLS IP VPN), RFC 2547 (BGP/MPLS VPN), RFC 8299 (YANG VRF Model) -- 커널 VRF 구현은 이 표준들의 라우팅 격리 원리를 리눅스 네트워크 스택에 적용합니다. 종합 목록은 참고자료 -- 표준 & 규격 섹션을 참고하세요.

L3 Master Device 개요

L3 Master Device란?

L3 Master Device는 리눅스 커널 4.4에서 도입된 네트워크 계층(Layer 3) 격리 프레임워크입니다. 하나의 네트워크 네임스페이스 안에서 여러 개의 독립적인 라우팅 도메인을 생성할 수 있게 해주며, 각 도메인은 자체 라우팅 테이블, FIB 규칙, 이웃 테이블을 갖습니다.

전통적인 네트워크 격리는 네트워크 네임스페이스(netns)를 통해 이루어졌으나, 네임스페이스는 완전한 스택 복제를 수반하므로 수천 개의 라우팅 인스턴스가 필요한 ISP/엔터프라이즈 환경에서는 메모리와 관리 오버헤드가 과도합니다. L3 Master Device는 네트워크 스택의 L3 계층만 선택적으로 격리하여 이 문제를 해결합니다.

핵심 설계 원칙

원칙설명구현 메커니즘
경량 격리 L3 계층만 분리, L2는 공유 l3mdev_ops
테이블 기반 분리 라우팅 테이블 번호로 도메인 구분 vrf_table, FIB 규칙
슬레이브 모델 물리/가상 인터페이스를 VRF에 종속 ndo_add_slave
투명한 통합 기존 라우팅/소켓 API 그대로 활용 FIB 규칙 자동 생성, SO_BINDTODEVICE
확장성 수천 개 VRF 인스턴스 지원 인스턴스당 최소 메모리 소비

L3 Master Device의 위치

커널 소스 트리에서 L3 Master Device 관련 코드는 다음 위치에 있습니다.

파일역할
include/net/l3mdev.h l3mdev_ops 정의, 인라인 헬퍼 함수
net/l3mdev/l3mdev.c l3mdev 코어 로직, FIB 규칙 연동
drivers/net/vrf.c VRF 디바이스 드라이버 구현
include/net/vrf.h VRF 헤더, 인라인 헬퍼
net/ipv4/fib_rules.c IPv4 FIB 규칙, l3mdev 통합
net/ipv6/fib6_rules.c IPv6 FIB 규칙, l3mdev 통합

VRF vs 네트워크 네임스페이스 요약 비교

항목VRF (L3 Master Device)네트워크 네임스페이스
격리 수준 L3 (라우팅 테이블, FIB) 전체 네트워크 스택
인스턴스당 메모리 수 KB (라우팅 테이블 + 구조체) 수 MB (전체 스택 복제)
스케일 수천 개 (PE 라우터 시나리오) 수백 개 (컨테이너 시나리오)
L2 공유 가능 (동일 이더넷 브리지) 불가 (veth 필요)
관리 도구 ip link, ip vrf ip netns
방화벽 격리 공유 (netfilter 공유) 완전 독립
라우팅 데몬 단일 데몬 (FRR VRF 지원) 인스턴스당 별도 데몬
사용 시나리오 선택 가이드: 라우팅만 분리하면 되고 동일 호스트에서 수백~수천 개의 라우팅 인스턴스가 필요하다면 VRF, 네트워크 스택 전체(소켓, netfilter, 인터페이스)를 완전히 격리해야 하면 네트워크 네임스페이스를 사용합니다. 두 기술을 조합(VRF + netns)하는 하이브리드 구성도 가능합니다.

VRF (Virtual Routing and Forwarding) 개념

VRF의 정의와 역사

VRF(Virtual Routing and Forwarding)는 단일 물리 라우터 위에 여러 개의 독립적인 라우팅 테이블을 유지하는 기술입니다. Cisco IOS에서 처음 상용화되었고, RFC 4364(BGP/MPLS IP VPN)에서 표준화되었습니다. 리눅스에서는 커널 4.3(초기 프로토타입)부터 4.8(안정화)까지 Cumulus Networks 엔지니어들이 주도하여 구현했습니다.

VRF의 핵심 아이디어는 간단합니다: 네트워크 인터페이스를 그룹으로 묶고, 각 그룹에 독립적인 라우팅 테이블을 할당합니다. 인터페이스 A가 VRF "red"에 속하면, A로 들어오는 패킷은 "red"의 라우팅 테이블만 참조하여 포워딩됩니다. VRF "blue"에 속한 인터페이스 B로 들어오는 패킷은 "blue"의 테이블만 참조합니다.

전통적인 네트워킹 VRF 개념

네트워킹 장비에서 VRF는 다음 요소로 구성됩니다.

VRF 구성 요소역할리눅스 대응
VRF 인스턴스 라우팅 도메인의 논리적 컨테이너 net_device (type vrf)
RD (Route Distinguisher) BGP에서 VRF 경로 구분 FRR 설정 레벨
RT (Route Target) VRF 간 경로 임포트/익스포트 FRR 설정 레벨
인터페이스 바인딩 물리 포트 -> VRF 매핑 ip link set dev eth0 master vrf-red
라우팅 테이블 VRF별 독립 FIB ip route show table N

멀티테넌트 시나리오

클라우드 환경이나 ISP PE 라우터에서 VRF가 왜 필수적인지 구체적 시나리오를 통해 이해합니다.

시나리오: ISP PE 라우터
고객사 A와 B가 동일한 사설 IP 대역(10.0.0.0/24)을 사용합니다. PE 라우터는 두 고객의 트래픽을 동시에 처리해야 하므로, 고객 A의 10.0.0.0/24와 고객 B의 10.0.0.0/24를 구분해야 합니다. VRF를 사용하면 고객 A의 인터페이스는 VRF "cust-a"에, 고객 B는 VRF "cust-b"에 할당하여 동일 IP 대역이 충돌 없이 공존합니다.
VRF 개념: 멀티테넌트 라우팅 격리 리눅스 라우터 (단일 네트워크 네임스페이스) VRF "cust-a" (table 100) eth1 eth2 라우팅 테이블 100 10.0.0.0/24 -> eth1 VRF "cust-b" (table 200) eth3 eth4 라우팅 테이블 200 10.0.0.0/24 -> eth3 글로벌 테이블 (main/default) eth0 (mgmt) 0.0.0.0/0 기본경로 고객 A: 10.0.0.0/24 고객 B: 10.0.0.0/24 동일 IP 대역 10.0.0.0/24가 VRF별로 격리되어 충돌 없음 각 VRF는 독립된 FIB를 사용하여 라우팅 결정

VRF의 핵심 이점

VRF 발전 타임라인

커널 버전변경사항중요도
4.3 VRF 디바이스 초기 구현 (drivers/net/vrf.c) 기초
4.4 l3mdev 프레임워크 도입, l3mdev_ops 인터페이스 핵심
4.8 TCP/UDP 소켓의 VRF 바인딩 지원 (sysctl tcp_l3mdev_accept) 핵심
4.10 VRF FIB 규칙 자동 생성 편의
4.14 IPv6 VRF 완전 지원 핵심
5.1 udp_l3mdev_accept sysctl 추가 편의
5.3 VRF + 멀티캐스트 지원 기능
5.8 MPLS + VRF 통합 개선 기능
5.15 VRF strict mode, 향상된 소켓 바인딩 핵심
6.1+ VRF + SRv6 연동, 성능 최적화 고급

l3mdev 커널 아키텍처

아키텍처 개요

l3mdev 프레임워크는 리눅스 네트워크 스택의 L3(IP) 계층에 훅 포인트를 삽입하여, 패킷이 라우팅 결정을 내리기 전에 적절한 라우팅 테이블로 리다이렉트하는 메커니즘입니다. 이 프레임워크는 세 가지 핵심 구성 요소로 이루어집니다.

구성 요소역할코드 위치
l3mdev_ops 마스터 디바이스의 L3 훅 인터페이스 include/net/l3mdev.h
FIB 규칙 통합 l3mdev 규칙으로 테이블 선택 자동화 net/core/fib_rules.c
VRF 디바이스 드라이버 l3mdev_ops의 VRF 구현체 drivers/net/vrf.c
l3mdev 커널 아키텍처 사용자 공간 ip link / ip vrf FRRouting 소켓 애플리케이션 커널 공간 소켓 계층 (SO_BINDTODEVICE / VRF 바인딩) tcp_l3mdev_accept, udp_l3mdev_accept sysctl IP 계층 (L3) l3mdev_l3_rcv() 수신 경로 훅 l3mdev_l3_out() 송신 경로 훅 l3mdev_fib_table() FIB 테이블 선택 FIB 규칙 엔진 (ip rule) l3mdev 규칙 (pref 1000) -> VRF 테이블 자동 선택 -> main -> default table 100 (VRF red) 10.0.0.0/24 -> eth1 table 200 (VRF blue) 10.0.0.0/24 -> eth3 main / default 0.0.0.0/0 -> gw net_device 계층 (L2) eth0(global) | eth1,eth2(VRF red) | eth3,eth4(VRF blue) 물리 NIC / 가상 인터페이스

l3mdev 프레임워크의 동작 흐름

패킷이 네트워크 인터페이스에 도착하면, IP 수신 경로(ip_rcv)는 해당 인터페이스가 l3mdev 마스터에 종속되어 있는지 확인합니다. 종속 관계가 있으면 l3mdev_l3_rcv() 훅을 호출하여 패킷의 skb->dev를 마스터 VRF 디바이스로 변경하고, FIB 조회 시 해당 VRF의 라우팅 테이블을 사용하도록 유도합니다.

송신 경로에서는 ip_route_output()이 FIB 규칙을 순회할 때 l3mdev 규칙에 의해 올바른 VRF 테이블이 선택됩니다. 소켓이 VRF에 바인딩되어 있으면 해당 VRF의 마스터 인덱스가 라우팅 키에 포함되어 정확한 테이블 매칭이 이루어집니다.

l3mdev와 net_device 관계

l3mdev 마스터 디바이스는 net_device의 특수한 형태입니다. 일반 네트워크 디바이스와 달리 실제 패킷을 물리적으로 전송하지 않고, 종속된 슬레이브 디바이스들의 L3 트래픽을 관리하는 "논리적 컨테이너" 역할을 합니다.

/* net_device에서 l3mdev 마스터 관계 확인 */
static inline int l3mdev_master_ifindex(struct net_device *dev)
{
    /* 디바이스가 L3 마스터 자체인 경우 */
    if (netif_is_l3_master(dev))
        return dev->ifindex;

    /* 슬레이브인 경우 마스터의 ifindex 반환 */
    if (netif_is_l3_slave(dev))
        return dev->flags & IFF_L3MDEV_SLAVE ?
               dev->master->ifindex : 0;

    return 0;
}
코드 설명: l3mdev_master_ifindex

l3mdev_master_ifindex()는 주어진 net_device가 속한 L3 마스터 디바이스의 인터페이스 인덱스를 반환합니다. 디바이스 자체가 L3 마스터(예: VRF 디바이스)이면 자신의 ifindex를 반환하고, 슬레이브(예: VRF에 종속된 eth0)이면 마스터의 ifindex를 반환합니다. 이 값은 FIB 조회, 소켓 바인딩, 라우팅 결정 등에서 VRF 식별자로 사용됩니다.

l3mdev_ops 인터페이스

구조체 정의

l3mdev_ops는 L3 마스터 디바이스가 구현해야 하는 콜백 인터페이스입니다. VRF 디바이스는 이 인터페이스를 구현하여 커널 네트워크 스택에 L3 격리 기능을 제공합니다.

/* include/net/l3mdev.h */
struct l3mdev_ops {
    /* 디바이스가 속한 FIB 테이블 번호 반환 */
    u32   (*l3mdev_fib_table)(const struct net_device *dev);

    /* 수신 패킷에 대한 L3 입력 훅
     * ip_rcv()에서 호출되어 패킷의 skb->dev를 마스터로 변경
     * 반환: 처리된 skb 또는 NULL (드롭) */
    struct sk_buff * (*l3mdev_l3_rcv)(struct net_device *dev,
                                    struct sk_buff *skb,
                                    u16 proto);

    /* 송신 패킷에 대한 L3 출력 훅
     * ip_route_output()에서 호출
     * 반환: 처리된 skb 또는 NULL (드롭) */
    struct sk_buff * (*l3mdev_l3_out)(struct net_device *dev,
                                    struct sock *sk,
                                    struct sk_buff *skb,
                                    u16 proto);

    /* 링크-로컬 스코프 주소 조회
     * 슬레이브 디바이스에서 link-local 주소 해석 시 사용 */
    struct dst_entry * (*l3mdev_link_scope_lookup)(
                            const struct net_device *dev,
                            struct flowi6 *fl6);
};
코드 설명: struct l3mdev_ops 각 콜백의 역할

l3mdev_fib_table: 디바이스가 사용하는 FIB 라우팅 테이블 번호를 반환합니다. VRF 디바이스의 경우 생성 시 지정된 vrf_table 값을 반환합니다.

l3mdev_l3_rcv: 패킷 수신 시 IP 계층 진입 직전에 호출됩니다. 주요 역할은 skb->dev를 슬레이브에서 마스터(VRF 디바이스)로 변경하여, 이후 라우팅 조회가 VRF의 테이블을 사용하도록 만드는 것입니다.

l3mdev_l3_out: 패킷 송신 시 라우팅 결정 직후에 호출됩니다. 출력 인터페이스가 VRF에 속한 경우, 적절한 처리를 수행합니다.

l3mdev_link_scope_lookup: IPv6 링크-로컬 주소(fe80::/10) 조회 시 사용됩니다. VRF 환경에서 링크-로컬 주소의 스코프가 올바르게 해석되도록 보장합니다.

l3mdev 인라인 헬퍼 함수

커널 곳곳에서 l3mdev 상태를 확인하기 위해 다양한 인라인 헬퍼 함수가 제공됩니다.

/* 디바이스가 L3 마스터인지 확인 */
static inline bool netif_is_l3_master(const struct net_device *dev)
{
    return dev->priv_flags & IFF_L3MDEV_MASTER;
}

/* 디바이스가 L3 슬레이브인지 확인 */
static inline bool netif_is_l3_slave(const struct net_device *dev)
{
    return dev->priv_flags & IFF_L3MDEV_SLAVE;
}

/* 디바이스에서 FIB 테이블 번호 얻기 */
static inline u32 l3mdev_fib_table_rcu(const struct net_device *dev)
{
    u32 tb_id = 0;

    if (netif_is_l3_master(dev))
        tb_id = dev->l3mdev_ops->l3mdev_fib_table(dev);
    else if (netif_is_l3_slave(dev)) {
        struct net_device *master;
        master = netdev_master_upper_dev_get_rcu(dev);
        if (master && master->l3mdev_ops)
            tb_id = master->l3mdev_ops->l3mdev_fib_table(master);
    }

    return tb_id;
}

/* 수신 경로 l3 훅 호출 */
static inline struct sk_buff *l3mdev_l3_rcv(struct sk_buff *skb, u16 proto)
{
    struct net_device *master;

    if (netif_is_l3_slave(skb->dev)) {
        master = netdev_master_upper_dev_get_rcu(skb->dev);
        if (master && master->l3mdev_ops->l3mdev_l3_rcv)
            skb = master->l3mdev_ops->l3mdev_l3_rcv(master, skb, proto);
    }

    return skb;
}
코드 설명: l3mdev 헬퍼 함수 동작

netif_is_l3_master(): IFF_L3MDEV_MASTER 플래그로 L3 마스터 여부를 판별합니다. VRF 디바이스가 등록될 때 이 플래그가 설정됩니다.

l3mdev_fib_table_rcu(): RCU 읽기 측에서 안전하게 호출할 수 있는 FIB 테이블 조회 함수입니다. 디바이스가 마스터이면 직접 l3mdev_fib_table 콜백을 호출하고, 슬레이브이면 netdev_master_upper_dev_get_rcu()로 마스터를 찾아 테이블 번호를 얻습니다.

l3mdev_l3_rcv(): 패킷 수신 경로에서 ip_rcv_finish()가 호출하는 핵심 훅입니다. 슬레이브 디바이스로 들어온 패킷에 대해 마스터의 l3mdev_l3_rcv 콜백을 호출하여 패킷이 올바른 VRF 컨텍스트에서 처리되도록 합니다.

l3mdev_ops 등록 과정

VRF 디바이스 드라이버는 net_device_opsl3mdev_ops를 모두 등록합니다. 디바이스가 등록될 때 커널은 l3mdev_ops 포인터가 설정되어 있으면 IFF_L3MDEV_MASTER 플래그를 자동으로 설정합니다.

/* drivers/net/vrf.c에서 l3mdev_ops 등록 */
static const struct l3mdev_ops vrf_l3mdev_ops = {
    .l3mdev_fib_table       = vrf_fib_table,
    .l3mdev_l3_rcv          = vrf_l3_rcv,
    .l3mdev_l3_out          = vrf_l3_out,
    .l3mdev_link_scope_lookup = vrf_link_scope_lookup,
};

static void vrf_setup(struct net_device *dev)
{
    /* 이더넷 기본 초기화 */
    ether_setup(dev);

    /* l3mdev 오퍼레이션 설정 */
    dev->l3mdev_ops = &vrf_l3mdev_ops;
    dev->netdev_ops = &vrf_netdev_ops;

    /* VRF는 이더헤더가 필요 없음 */
    dev->hard_header_len = 0;
    dev->addr_len        = 0;
    dev->type            = ARPHRD_NONE;

    /* VRF 디바이스 전용 플래그 */
    dev->priv_flags |= IFF_NO_QUEUE | IFF_NO_RX_HANDLER;
    dev->needs_free_netdev = true;
}

VRF 디바이스 구현

struct net_vrf 자료구조

VRF 디바이스의 프라이빗 데이터 구조체인 net_vrf는 VRF 인스턴스의 핵심 상태를 유지합니다.

/* drivers/net/vrf.c */
struct net_vrf {
    u32                  tb_id;        /* 라우팅 테이블 ID */

    /* IPv4 관련 */
    struct fib_lookup_arg {
        struct fib_result *res;
        int               flags;
    } fl4_lookup;

    /* IPv6 관련 */
    struct rt6_info      *rt6;          /* VRF의 IPv6 라우트 엔트리 */
    struct rt6_info      *rt6_local;    /* 로컬 라우트 */

    /* 디바이스 통계 */
    struct pcpu_dstats __percpu *stats;
};
코드 설명: net_vrf 구조체 필드

tb_id: 이 VRF 인스턴스가 사용하는 라우팅 테이블 번호입니다. VRF 생성 시 ip link add ... table N으로 지정한 값이 저장됩니다.

rt6/rt6_local: IPv6 라우팅에서 VRF 디바이스 자체에 대한 라우트 엔트리입니다. VRF 디바이스가 UP 될 때 생성되며, VRF를 통한 IPv6 패킷 포워딩에 사용됩니다.

stats: Per-CPU 통계를 위한 카운터입니다. VRF를 통과하는 패킷 수, 바이트 수, 드롭 수 등을 추적합니다.

VRF 디바이스 생성: vrf_newlink()

사용자가 ip link add vrf-red type vrf table 100 명령을 실행하면, RTNL(Routing Netlink)을 통해 vrf_newlink()가 호출됩니다.

static int vrf_newlink(struct net *src_net,
                       struct net_device *dev,
                       struct nlattr *tb[],
                       struct nlattr *data[],
                       struct netlink_ext_ack *extack)
{
    struct net_vrf *vrf = netdev_priv(dev);
    bool *add_fib_rules;
    struct net *net;
    int err;

    /* table 속성 검증 */
    if (!data || !data[IFLA_VRF_TABLE]) {
        NL_SET_ERR_MSG(extack, "VRF table id is missing");
        return -EINVAL;
    }

    /* 테이블 번호 추출 */
    vrf->tb_id = nla_get_u32(data[IFLA_VRF_TABLE]);
    if (vrf->tb_id == RT_TABLE_UNSPEC) {
        NL_SET_ERR_MSG(extack, "Invalid VRF table id");
        return -EINVAL;
    }

    /* 디바이스 등록 */
    err = register_netdevice(dev);
    if (err)
        goto out;

    /* 최초 VRF 디바이스 생성 시 FIB 규칙 자동 추가 */
    net = dev_net(dev);
    add_fib_rules = net_generic(net, vrf_net_id);
    if (*add_fib_rules) {
        err = vrf_add_fib_rules(dev);
        if (err) {
            unregister_netdevice(dev);
            goto out;
        }
        *add_fib_rules = false;
    }

out:
    return err;
}
코드 설명: VRF 디바이스 생성 과정

1. 테이블 속성 검증: IFLA_VRF_TABLE Netlink 속성에서 라우팅 테이블 번호를 추출합니다. 테이블 번호는 RT_TABLE_UNSPEC(0)이 아닌 유효한 값이어야 합니다.

2. 디바이스 등록: register_netdevice()로 VRF 디바이스를 커널에 등록합니다. 이 시점에서 vrf_setup()에서 설정한 l3mdev_ops가 활성화됩니다.

3. FIB 규칙 자동 추가: 네임스페이스 내 최초 VRF 생성 시 l3mdev FIB 규칙을 IPv4와 IPv6 모두에 추가합니다. 이 규칙은 "이 패킷이 VRF에 속한 인터페이스에서 왔으면, 해당 VRF의 테이블을 사용하라"는 의미입니다.

슬레이브 인터페이스 종속(Enslaving)

물리 또는 가상 인터페이스를 VRF에 종속시키는 과정입니다. ip link set dev eth1 master vrf-red 명령이 ndo_add_slave()를 호출합니다.

static int vrf_add_slave(struct net_device *dev,
                         struct net_device *port_dev,
                         struct netlink_ext_ack *extack)
{
    /* 이미 다른 마스터에 종속된 경우 거부 */
    if (netdev_master_upper_dev_get(port_dev)) {
        NL_SET_ERR_MSG(extack, "Device is already enslaved");
        return -EBUSY;
    }

    /* L3 마스터를 슬레이브로 종속시키는 것은 불가 */
    if (netif_is_l3_master(port_dev)) {
        NL_SET_ERR_MSG(extack, "Can not enslave an L3 master device");
        return -EINVAL;
    }

    /* upper/lower 관계 설정 */
    return vrf_add_slave_common(dev, port_dev, extack);
}

static int vrf_add_slave_common(struct net_device *dev,
                                struct net_device *port_dev,
                                struct netlink_ext_ack *extack)
{
    int ret;

    /* upper device 관계 설정 (port_dev -> dev) */
    ret = netdev_master_upper_dev_link(port_dev, dev, NULL, NULL, extack);
    if (ret < 0)
        return ret;

    /* 슬레이브 플래그 설정 */
    port_dev->priv_flags |= IFF_L3MDEV_SLAVE;

    /* 슬레이브의 기존 주소를 VRF 테이블로 이동 */
    vrf_rt6_release(dev, port_dev);
    vrf_ip_addr_move(dev, port_dev);

    /* NETDEV_CHANGEUPPER 이벤트 발생 (FRR 등에서 감지) */
    call_netdevice_notifiers(NETDEV_CHANGEUPPER, port_dev);

    return 0;
}
VRF 디바이스 내부 구조 VRF net_device "vrf-red" l3mdev_ops = &vrf_l3mdev_ops IFF_L3MDEV_MASTER net_vrf.tb_id = 100 stats (per-cpu) eth1 (슬레이브) IFF_L3MDEV_SLAVE 10.1.0.1/24 eth2 (슬레이브) IFF_L3MDEV_SLAVE 10.2.0.1/24 vlan100 (슬레이브) IFF_L3MDEV_SLAVE 172.16.0.1/24 라우팅 테이블 100 (VRF red 전용) 모든 슬레이브의 트래픽이 table 100을 통해 라우팅

VRF net_device_ops

VRF 디바이스는 일반 네트워크 디바이스와 다른 특수한 net_device_ops를 사용합니다.

static const struct net_device_ops vrf_netdev_ops = {
    .ndo_init           = vrf_dev_init,
    .ndo_uninit         = vrf_dev_uninit,
    .ndo_start_xmit     = vrf_xmit,
    .ndo_set_mac_address = eth_mac_addr,
    .ndo_get_stats64    = vrf_get_stats64,
    .ndo_add_slave      = vrf_add_slave,
    .ndo_del_slave      = vrf_del_slave,
};
VRF의 xmit 경로: vrf_xmit()는 패킷이 VRF 디바이스 자체로 전송될 때 호출됩니다. VRF 디바이스는 직접 물리 전송을 하지 않으므로, 패킷을 다시 라우팅하여 적절한 슬레이브 인터페이스를 통해 나가도록 합니다. 이 과정에서 VRF의 라우팅 테이블이 참조됩니다.

라우팅 테이블 분리

VRF별 라우팅 테이블 원리

리눅스 커널은 최대 4,294,967,295개(32비트)의 라우팅 테이블을 지원합니다. 각 VRF 인스턴스는 고유한 테이블 번호를 할당받아, 해당 테이블에만 경로를 저장하고 조회합니다. 이 메커니즘은 FIB(Forwarding Information Base) 규칙 시스템과 연동됩니다.

VRF 생성 시 FIB 규칙 자동 추가

네임스페이스 내 첫 번째 VRF 디바이스가 생성될 때, 커널은 자동으로 l3mdev FIB 규칙을 추가합니다. 이 규칙은 패킷의 입력 인터페이스가 VRF 슬레이브인 경우, 해당 VRF의 테이블을 먼저 조회하도록 합니다.

static int vrf_add_fib_rules(const struct net_device *dev)
{
    struct net *net = dev_net(dev);
    int err;

    /* IPv4 l3mdev 규칙 추가 (우선순위 1000) */
    err = vrf_fib_rule(dev, AF_INET, true);
    if (err < 0)
        goto out;

    /* IPv6 l3mdev 규칙 추가 (우선순위 1000) */
    err = vrf_fib_rule(dev, AF_INET6, true);
    if (err < 0)
        goto ipv6_err;

    return 0;

ipv6_err:
    vrf_fib_rule(dev, AF_INET, false);
out:
    return err;
}

FIB 규칙 동작 흐름

VRF가 활성화된 시스템에서 FIB 규칙의 평가 순서를 확인합니다.

# FIB 규칙 확인
$ ip rule show
0:      from all lookup local
1000:   from all lookup [l3mdev-table]
32766:  from all lookup main
32767:  from all lookup default
우선순위규칙역할
0 lookup local 로컬 주소 확인 (루프백, 자기 주소)
1000 lookup [l3mdev-table] VRF 슬레이브 인터페이스면 해당 VRF 테이블 사용
32766 lookup main 글로벌 라우팅 테이블 (기본 경로)
32767 lookup default 기본 폴백 테이블
FIB 규칙과 VRF 라우팅 테이블 선택 흐름 패킷 수신 규칙 0: local 자기 주소 확인 규칙 1000: l3mdev-table VRF 슬레이브? VRF 테이블 100 VRF 테이블 200 규칙 32766: main 글로벌 라우팅 규칙 32767: default 폴백 라우팅 결정 완료 YES (VRF red) YES (VRF blue) NO l3mdev-table 규칙은 인터페이스의 VRF 귀속에 따라 자동으로 올바른 테이블을 선택

VRF별 라우팅 확인

# VRF "red"의 라우팅 테이블 확인 (table 100)
$ ip route show table 100
10.1.0.0/24 dev eth1 proto kernel scope link src 10.1.0.1
10.2.0.0/24 dev eth2 proto kernel scope link src 10.2.0.1
default via 10.1.0.254 dev eth1

# VRF "blue"의 라우팅 테이블 확인 (table 200)
$ ip route show table 200
10.1.0.0/24 dev eth3 proto kernel scope link src 10.1.0.1
192.168.0.0/24 dev eth4 proto kernel scope link src 192.168.0.1
default via 10.1.0.254 dev eth3

# VRF 단축 명령 (ip vrf 사용)
$ ip route show vrf vrf-red
$ ip route show vrf vrf-blue

# VRF 내에서 특정 목적지 경로 조회
$ ip route get 8.8.8.8 vrf vrf-red
8.8.8.8 via 10.1.0.254 dev eth1 table 100 src 10.1.0.1

# VRF별 라우트 추가
$ ip route add 192.168.100.0/24 via 10.1.0.2 table 100
$ ip route add 192.168.100.0/24 via 10.3.0.2 table 200
테이블 번호와 VRF 이름: ip route show vrf vrf-redip route show table 100은 동일한 결과를 보여줍니다. VRF 이름은 테이블 번호의 별칭(alias)으로 동작하며, /etc/iproute2/rt_tables에 수동으로 매핑을 추가할 수도 있습니다.

패킷 수신/송신 흐름

수신 경로: l3mdev_l3_rcv() 훅

패킷이 VRF 슬레이브 인터페이스에 도착하면, IP 수신 경로에서 l3mdev_l3_rcv() 훅이 호출됩니다. 이 훅은 패킷의 skb->dev를 VRF 마스터 디바이스로 변경하고, 이후 FIB 조회가 VRF의 라우팅 테이블을 사용하도록 보장합니다.

/* drivers/net/vrf.c - VRF의 l3_rcv 구현 */
static struct sk_buff *vrf_l3_rcv(struct net_device *vrf_dev,
                                   struct sk_buff *skb,
                                   u16 proto)
{
    switch (proto) {
    case AF_INET:
        return vrf_ip_rcv(vrf_dev, skb);
    case AF_INET6:
        return vrf_ip6_rcv(vrf_dev, skb);
    }
    return skb;
}

static struct sk_buff *vrf_ip_rcv(struct net_device *vrf_dev,
                                   struct sk_buff *skb)
{
    /* skb->dev를 VRF 마스터로 변경
     * 이후 ip_rcv_finish()의 FIB 조회가 VRF 테이블 사용 */
    skb->dev = vrf_dev;

    /* VRF 디바이스 통계 업데이트 */
    skb->skb_iif = vrf_dev->ifindex;
    IP_UPD_PO_STATS(dev_net(vrf_dev), IPSTATS_MIB_IN, skb->len);

    /* Netfilter PRE_ROUTING 훅 재진입
     * VRF 디바이스 컨텍스트에서 netfilter 규칙 평가 */
    if (!vrf_rcv_finish(skb))
        return NULL;

    return skb;
}

송신 경로: l3mdev_l3_out() 훅과 라우팅

애플리케이션이 소켓을 통해 패킷을 전송할 때, ip_route_output()은 FIB 규칙을 순회하며 올바른 라우팅 테이블을 선택합니다. 소켓이 VRF에 바인딩되어 있으면 l3mdev_master_ifindex_by_index()로 VRF 마스터를 찾고, FIB 조회 키에 출력 인터페이스 인덱스를 설정합니다.

/* drivers/net/vrf.c - VRF의 l3_out 구현 */
static struct sk_buff *vrf_l3_out(struct net_device *vrf_dev,
                                   struct sock *sk,
                                   struct sk_buff *skb,
                                   u16 proto)
{
    switch (proto) {
    case AF_INET:
        return vrf_ip_out(vrf_dev, sk, skb);
    case AF_INET6:
        return vrf_ip6_out(vrf_dev, sk, skb);
    }
    return skb;
}

static struct sk_buff *vrf_ip_out(struct net_device *vrf_dev,
                                   struct sock *sk,
                                   struct sk_buff *skb)
{
    /* Netfilter LOCAL_OUT 훅을 VRF 컨텍스트에서 재평가 */
    nf_reset_ct(skb);

    /* VRF 디바이스 통계 업데이트 */
    skb->dev = vrf_dev;
    IP_UPD_PO_STATS(dev_net(vrf_dev), IPSTATS_MIB_OUT, skb->len);

    return skb;
}
VRF 패킷 수신/송신 상세 흐름 수신 경로 (RX) NIC 수신 (eth1) NAPI -> netif_receive_skb() ip_rcv() 진입 l3mdev_l3_rcv() skb->dev = vrf_dev ip_rcv_finish() FIB 조회 (VRF 테이블) fib_lookup() -> table 100 ip_local_deliver() / ip_forward() 소켓 전달 또는 포워딩 송신 경로 (TX) 애플리케이션 sendmsg() 소켓 VRF 바인딩 확인 ip_route_output() oif = vrf master ifindex FIB 규칙: l3mdev-table VRF 테이블 선택 FIB 조회 -> nexthop 결정 l3mdev_l3_out() VRF 통계 업데이트 ip_output() -> Netfilter 이웃 탐색 (ARP/NDP) NIC 전송 (eth1) 양방향 모두 l3mdev 훅이 VRF 컨텍스트에서 올바른 라우팅 테이블을 선택하도록 보장

ip_rcv()에서 l3mdev_l3_rcv() 호출 위치

커널 소스에서 l3mdev 훅이 호출되는 정확한 위치를 확인합니다.

/* net/ipv4/ip_input.c - ip_rcv_finish_core() 내부 */
static int ip_rcv_finish_core(struct net *net,
                              struct sock *sk,
                              struct sk_buff *skb,
                              struct net_device *dev)
{
    const struct iphdr *iph = ip_hdr(skb);
    int err;

    /* l3mdev 훅: 슬레이브 디바이스면 마스터로 전환 */
    skb = l3mdev_ip_rcv(skb);
    if (!skb)
        return NET_RX_SUCCESS;

    /* 라우팅 테이블 조회 (VRF 테이블 사용) */
    if (!skb_valid_dst(skb)) {
        err = ip_route_input_noref(skb, iph->daddr, iph->saddr,
                                   iph->tos, dev);
        if (unlikely(err))
            goto drop;
    }

    /* ... 이후 ip_local_deliver() 또는 ip_forward() */
}

소켓 바인딩과 VRF

SO_BINDTODEVICE와 VRF

애플리케이션이 특정 VRF에서 동작하려면 소켓을 VRF 디바이스에 바인딩해야 합니다. 이를 위해 SO_BINDTODEVICE 소켓 옵션을 사용합니다.

/* VRF에 소켓 바인딩하는 C 예제 */
#include <sys/socket.h>
#include <netinet/in.h>
#include <string.h>

int create_vrf_socket(const char *vrf_name)
{
    int sockfd;
    struct sockaddr_in addr;

    /* TCP 소켓 생성 */
    sockfd = socket(AF_INET, SOCK_STREAM, 0);
    if (sockfd < 0)
        return -1;

    /* VRF 디바이스에 바인딩 */
    if (setsockopt(sockfd, SOL_SOCKET, SO_BINDTODEVICE,
                  vrf_name, strlen(vrf_name) + 1) < 0) {
        close(sockfd);
        return -1;
    }

    /* 주소 바인딩 */
    memset(&addr, 0, sizeof(addr));
    addr.sin_family = AF_INET;
    addr.sin_addr.s_addr = htonl(INADDR_ANY);
    addr.sin_port = htons(8080);

    if (bind(sockfd, (struct sockaddr *)&addr, sizeof(addr)) < 0) {
        close(sockfd);
        return -1;
    }

    return sockfd;
}

/* 사용 예 */
int main(void)
{
    int vrf_red_sock = create_vrf_socket("vrf-red");
    int vrf_blue_sock = create_vrf_socket("vrf-blue");

    /* 동일 포트 8080이 두 VRF에서 독립적으로 리스닝 */
    listen(vrf_red_sock, 128);
    listen(vrf_blue_sock, 128);
    /* ... */
}
소켓 VRF 바인딩 모델 App A (VRF red) SO_BINDTODEVICE "vrf-red" listen :8080 App B (VRF blue) SO_BINDTODEVICE "vrf-blue" listen :8080 App C (글로벌) 바인딩 없음 listen :9090 소켓 계층: inet_sk_bound_dev_eq() 매칭 sk->sk_bound_dev_if == dif || sk->sk_bound_dev_if == sdif vrf-red (ifindex=5) vrf-blue (ifindex=6) eth1 (red) eth2 (red) eth3 (blue) eth4 (blue) eth0 (글로벌) pkt :8080 pkt :8080 -> App A 전달 -> App B 전달 동일 포트, VRF별 독립 리스닝

tcp_l3mdev_accept / udp_l3mdev_accept sysctl

VRF에 바인딩되지 않은 "글로벌" 리스너 소켓이 VRF 인터페이스에서 들어오는 연결을 수락할지 결정하는 sysctl 파라미터입니다.

sysctl 파라미터기본값설명
net.ipv4.tcp_l3mdev_accept 0 1이면 글로벌 TCP 리스너가 VRF 패킷도 수락
net.ipv4.udp_l3mdev_accept 0 1이면 글로벌 UDP 소켓이 VRF 패킷도 수신
net.ipv4.raw_l3mdev_accept 1 1이면 글로벌 RAW 소켓이 VRF 패킷도 수신
# 글로벌 TCP 리스너가 모든 VRF에서 연결 수락
sysctl -w net.ipv4.tcp_l3mdev_accept=1

# 글로벌 UDP 리스너가 모든 VRF에서 패킷 수신
sysctl -w net.ipv4.udp_l3mdev_accept=1
보안 고려사항: tcp_l3mdev_accept=1을 설정하면 글로벌 리스너가 모든 VRF의 트래픽을 수신합니다. 멀티테넌트 환경에서는 이 설정이 보안 경계를 무너뜨릴 수 있으므로, VRF별 전용 리스너를 사용하고 이 sysctl은 0으로 유지하는 것이 권장됩니다.

ip vrf exec: VRF 컨텍스트에서 명령 실행

ip vrf exec 명령은 지정된 VRF 컨텍스트에서 프로세스를 실행합니다. 내부적으로 SO_BINDTODEVICE를 자동 설정하여 프로세스의 모든 소켓이 해당 VRF에서 동작하도록 합니다.

# VRF "red"에서 ping 실행
$ ip vrf exec vrf-red ping 8.8.8.8

# VRF "blue"에서 SSH 서버 시작
$ ip vrf exec vrf-blue /usr/sbin/sshd -D

# VRF "red"에서 curl 실행
$ ip vrf exec vrf-red curl http://10.1.0.100:80

# VRF "red"에서 서비스 시작
$ ip vrf exec vrf-red systemctl start nginx

# VRF에서 ss로 소켓 확인
$ ip vrf exec vrf-red ss -tlnp

커널 내부: 소켓과 VRF 연결 메커니즘

/* net/core/sock.c - SO_BINDTODEVICE 처리 */
static int sock_bindtodevice(struct sock *sk, int ifindex)
{
    struct net *net = sock_net(sk);
    struct net_device *dev;

    /* VRF 디바이스 조회 */
    dev = dev_get_by_index(net, ifindex);
    if (!dev)
        return -ENODEV;

    /* 소켓에 바인딩 인덱스 설정 */
    sk->sk_bound_dev_if = dev->ifindex;

    /* L3 마스터인 경우 추가 처리 */
    if (netif_is_l3_master(dev))
        sk->sk_bound_dev_if = dev->ifindex;

    dev_put(dev);
    return 0;
}

/* 소켓 조회 시 VRF 매칭 */
static inline bool inet_sk_bound_dev_eq(struct net *net,
                                        int bound_dev_if,
                                        int dif, int sdif)
{
    /* sdif: 슬레이브 디바이스의 ifindex
     * dif: 수신 디바이스 ifindex
     * bound_dev_if: 소켓이 바인딩된 디바이스 */

    /* 바인딩 없으면 모든 인터페이스 매칭 */
    if (!bound_dev_if)
        return true;

    /* 직접 매칭 또는 VRF 마스터 매칭 */
    return bound_dev_if == dif || bound_dev_if == sdif;
}

FIB 규칙과 정책 라우팅

FIB 규칙 개요

FIB(Forwarding Information Base) 규칙은 리눅스 정책 라우팅의 핵심입니다. ip rule 명령으로 관리하며, 패킷의 속성(소스 IP, 입력 인터페이스, fwmark 등)에 따라 어떤 라우팅 테이블을 참조할지 결정합니다. VRF는 이 규칙 시스템을 활용하여 인터페이스 기반 테이블 분리를 구현합니다.

l3mdev FIB 규칙의 구현

/* net/core/fib_rules.c - l3mdev 규칙 매칭 */
static int fib_rule_match(struct fib_rule *rule,
                          struct fib_rules_ops *ops,
                          struct flowi *fl,
                          int flags,
                          struct fib_lookup_arg *arg)
{
    int ret = 0;

    /* l3mdev 규칙인 경우 */
    if (rule->l3mdev && !rule->l3mdev_failed) {
        /* 입력 인터페이스의 VRF 테이블을 자동 선택 */
        ret = l3mdev_fib_rule_match(rule, fl, arg);
        if (ret)
            goto out;
    }

    /* 일반 규칙 매칭 (소스/목적지/fwmark 등) */
    if (rule->iifindex && (rule->iifindex != fl->flowi_iif))
        goto out;

    /* ... */
out:
    return ret;
}

/* l3mdev FIB 규칙 매칭 로직 */
int l3mdev_fib_rule_match(struct fib_rule *rule,
                          struct flowi *fl,
                          struct fib_lookup_arg *arg)
{
    struct net *net = rule->fr_net;
    struct net_device *dev;
    u32 tb_id;

    /* 입력/출력 인터페이스에서 VRF 마스터 찾기 */
    rcu_read_lock();

    /* 입력 인터페이스 기반 VRF 탐색 */
    if (fl->flowi_iif) {
        dev = dev_get_by_index_rcu(net, fl->flowi_iif);
        if (dev) {
            tb_id = l3mdev_fib_table_rcu(dev);
            if (tb_id) {
                arg->table = tb_id;
                rcu_read_unlock();
                return 1;
            }
        }
    }

    /* 출력 인터페이스 기반 VRF 탐색 */
    if (fl->flowi_oif) {
        dev = dev_get_by_index_rcu(net, fl->flowi_oif);
        if (dev) {
            tb_id = l3mdev_fib_table_rcu(dev);
            if (tb_id) {
                arg->table = tb_id;
                rcu_read_unlock();
                return 1;
            }
        }
    }

    rcu_read_unlock();
    return 0;
}

커스텀 FIB 규칙과 VRF 조합

# VRF에 추가 정책 라우팅 규칙 설정

# VRF red에서 특정 소스 IP의 트래픽을 별도 테이블로 라우팅
ip rule add from 10.1.0.0/24 iif eth1 table 150 prio 500

# fwmark 기반 VRF 간 라우팅 (route leaking)
ip rule add fwmark 0x100 table 200 prio 900

# VRF 내 특정 목적지에 대한 정책
ip rule add to 192.168.0.0/16 iif vrf-red table 100 prio 800

# 규칙 확인
ip rule show
ip -6 rule show
l3mdev 규칙 우선순위: l3mdev 규칙의 기본 우선순위는 1000입니다. 커스텀 규칙을 추가할 때 우선순위를 적절히 설정하여 l3mdev 규칙보다 먼저(숫자가 작으면) 또는 나중에(숫자가 크면) 평가되도록 조정할 수 있습니다. 일반적으로 VRF 특화 규칙은 500~999 범위에 배치합니다.

이웃 탐색과 ARP/NDP

VRF에서의 이웃 탐색

VRF 환경에서 ARP(IPv4)와 NDP(IPv6) 이웃 탐색은 각 VRF 인터페이스에 독립적으로 동작합니다. 이웃 테이블(neigh_table)은 네트워크 네임스페이스 단위로 관리되므로, 동일 네임스페이스 내 VRF들은 이웃 테이블을 공유합니다. 그러나 이웃 엔트리는 인터페이스별로 구분되므로, VRF 간 이웃 충돌은 발생하지 않습니다.

항목동작VRF 영향
ARP 요청 슬레이브 인터페이스를 통해 전송 VRF별 독립 (인터페이스 구분)
ARP 응답 슬레이브 인터페이스에서 수신 해당 VRF 이웃 테이블에 기록
Gratuitous ARP VRF 인터페이스 UP 시 전송 VRF별 독립
NDP Router Solicitation 슬레이브 인터페이스를 통해 전송 VRF별 독립
NDP Neighbor Advertisement 슬레이브 인터페이스에서 수신 해당 VRF 이웃 테이블에 기록
Proxy ARP VRF 내 인터페이스 간 가능 proxy_arp per-interface 설정

VRF별 이웃 테이블 확인

# VRF "red" 인터페이스의 이웃 확인
$ ip neigh show dev eth1
10.1.0.2 lladdr 00:11:22:33:44:55 REACHABLE

# VRF "blue" 인터페이스의 이웃 (같은 IP, 다른 MAC)
$ ip neigh show dev eth3
10.1.0.2 lladdr aa:bb:cc:dd:ee:ff REACHABLE

# 전체 이웃 테이블 (VRF 관련 정보 포함)
$ ip -s neigh show

# 정적 이웃 추가 (VRF 내 특정 인터페이스)
$ ip neigh add 10.1.0.100 lladdr 00:11:22:33:44:66 dev eth1

VRF의 arp_filter / arp_announce 동작

VRF 환경에서 arp_filterarp_announce sysctl은 슬레이브 인터페이스 단위로 적용됩니다. VRF가 라우팅을 격리하므로 arp_filter=1(라우팅 테이블 기반 ARP 응답 필터링)은 VRF의 라우팅 테이블을 참조하여 동작합니다.

# VRF 슬레이브 인터페이스별 ARP 설정
sysctl -w net.ipv4.conf.eth1.arp_filter=1
sysctl -w net.ipv4.conf.eth1.arp_announce=2
sysctl -w net.ipv4.conf.eth1.arp_ignore=1

# Proxy ARP 활성화 (VRF 내 서브넷 간 통신)
sysctl -w net.ipv4.conf.eth1.proxy_arp=1

네트워크 네임스페이스와 비교

상세 비교표

비교 항목VRF (L3 Master Device)네트워크 네임스페이스 (netns)
격리 수준 L3 라우팅만 분리 전체 네트워크 스택 완전 격리
인터페이스 공유 물리 인터페이스 직접 사용 가능 veth 쌍 또는 macvlan 필요
Netfilter 격리 공유 (동일 iptables/nftables 규칙) 완전 독립 규칙 세트
소켓 네임스페이스 공유 (동일 포트 번호 충돌 가능) 완전 독립
라우팅 데몬 단일 FRR 인스턴스가 모든 VRF 관리 네임스페이스당 별도 데몬 인스턴스
인스턴스 수 수천 개 (PE 라우터 시나리오) 수백 개 (커널 메모리 제한)
메모리 오버헤드 인스턴스당 수 KB 인스턴스당 수 MB
생성/삭제 속도 매우 빠름 (ms 단위) 비교적 느림 (전체 스택 초기화)
인터페이스 이동 간단 (ip link set master) 복잡 (ip link set netns)
모니터링 단일 뷰에서 모든 VRF 관측 네임스페이스별 개별 접근 필요
BGP/OSPF 지원 FRR VRF-aware 명령으로 통합 네임스페이스별 별도 설정
MPLS/SRv6 네이티브 지원 지원 (추가 설정 필요)
대표 사용처 ISP PE 라우터, 멀티테넌트 네트워킹 컨테이너, 완전 격리 필요 환경
VRF vs 네트워크 네임스페이스 아키텍처 비교 VRF 모델 단일 네트워크 네임스페이스 공유: Netfilter / 소켓 / L2 VRF red table 100 eth1, eth2 FIB 독립 VRF blue table 200 eth3, eth4 FIB 독립 물리 NIC: eth1, eth2, eth3, eth4 (직접 사용) 장점: 경량, 빠른 생성, 단일 데몬 한계: 방화벽 공유, 소켓 네임스페이스 공유 적합: ISP PE, 멀티테넌트 라우팅 네트워크 네임스페이스 모델 netns "red" 완전 독립 스택 독립 netfilter 독립 라우팅 독립 소켓 veth0 netns "blue" 완전 독립 스택 독립 netfilter 독립 라우팅 독립 소켓 veth1 veth 쌍을 통한 호스트 연결 (간접) 물리 NIC: 호스트 네임스페이스에서 관리 장점: 완전 격리, 보안 경계 명확 한계: 무거움, 관리 복잡, 스케일 제한 적합: 컨테이너, 보안 격리 필요 시

하이브리드 구성: VRF + netns

실무에서는 VRF와 네트워크 네임스페이스를 조합하여 사용하기도 합니다. 예를 들어, 데이터 플레인은 VRF로 격리하고, 관리 플레인은 별도 네임스페이스에 배치하는 구성이 가능합니다.

# 하이브리드 구성 예: 관리 네임스페이스 + 데이터 VRF

# 1. 관리 네임스페이스 생성
ip netns add mgmt

# 2. 관리 인터페이스를 관리 네임스페이스로 이동
ip link set eth0 netns mgmt
ip netns exec mgmt ip addr add 192.168.1.1/24 dev eth0
ip netns exec mgmt ip link set eth0 up

# 3. 데이터 플레인 VRF 생성 (기본 네임스페이스)
ip link add vrf-cust-a type vrf table 100
ip link set vrf-cust-a up
ip link set eth1 master vrf-cust-a
ip addr add 10.0.0.1/24 dev eth1

ip link add vrf-cust-b type vrf table 200
ip link set vrf-cust-b up
ip link set eth2 master vrf-cust-b
ip addr add 10.0.0.1/24 dev eth2

FRR/BGP/OSPF 연동

FRRouting VRF 지원

FRRouting(FRR)은 리눅스 VRF를 네이티브로 지원하는 오픈소스 라우팅 스위트입니다. 단일 FRR 인스턴스가 모든 VRF의 BGP, OSPF, IS-IS 등을 관리할 수 있으며, VRF 간 라우트 리킹(Route Leaking)도 지원합니다.

FRR VRF 기본 설정

# /etc/frr/daemons - VRF 관련 데몬 활성화
bgpd=yes
ospfd=yes
zebra=yes

# /etc/frr/frr.conf - VRF 설정 예
!
vrf vrf-red
 vni 100
exit-vrf
!
vrf vrf-blue
 vni 200
exit-vrf
!
router bgp 65001
 !
 address-family ipv4 unicast
  network 10.0.0.0/24
 exit-address-family
!
router bgp 65001 vrf vrf-red
 bgp router-id 10.1.0.1
 neighbor 10.1.0.2 remote-as 65002
 !
 address-family ipv4 unicast
  redistribute connected
  redistribute static
 exit-address-family
!
router bgp 65001 vrf vrf-blue
 bgp router-id 10.2.0.1
 neighbor 10.2.0.2 remote-as 65003
 !
 address-family ipv4 unicast
  redistribute connected
  redistribute static
 exit-address-family
!

VRF별 BGP 인스턴스

FRR에서 VRF별 BGP 인스턴스는 router bgp <ASN> vrf <VRF명> 블록으로 정의합니다. 각 인스턴스는 독립적인 피어링, 경로 정책, 필터를 가집니다.

# vtysh에서 VRF BGP 상태 확인
vtysh -c "show bgp vrf vrf-red summary"
vtysh -c "show bgp vrf vrf-red ipv4 unicast"
vtysh -c "show bgp vrf all summary"

# VRF별 OSPF 상태
vtysh -c "show ip ospf vrf vrf-red neighbor"
vtysh -c "show ip ospf vrf vrf-red route"

# VRF별 라우팅 테이블 (zebra)
vtysh -c "show ip route vrf vrf-red"
vtysh -c "show ip route vrf all"

VRF 간 라우트 리킹 (Route Leaking)

VRF 간 선택적으로 경로를 공유해야 하는 경우가 있습니다. 예를 들어, 공통 서비스 네트워크(DNS, NTP)를 모든 VRF에서 접근 가능하게 만들거나, 특정 VRF 간 통신을 허용하는 경우입니다.

# FRR에서 VRF 간 라우트 리킹 설정
!
router bgp 65001 vrf vrf-red
 address-family ipv4 unicast
  ! 글로벌 테이블에서 특정 경로를 vrf-red로 임포트
  import vrf default
  import vrf route-map IMPORT-DEFAULT-TO-RED
 exit-address-family
!
router bgp 65001 vrf vrf-blue
 address-family ipv4 unicast
  ! vrf-red에서 특정 경로를 vrf-blue로 임포트
  import vrf vrf-red
  import vrf route-map IMPORT-RED-TO-BLUE
 exit-address-family
!
! 라우트 맵으로 리킹할 경로 필터링
route-map IMPORT-DEFAULT-TO-RED permit 10
 match ip address prefix-list COMMON-SERVICES
!
ip prefix-list COMMON-SERVICES seq 10 permit 8.8.8.0/24
ip prefix-list COMMON-SERVICES seq 20 permit 1.1.1.0/24
!

OSPF VRF 설정

# FRR OSPF VRF 설정
!
router ospf vrf vrf-red
 ospf router-id 10.1.0.1
 network 10.1.0.0/24 area 0.0.0.0
 network 10.2.0.0/24 area 0.0.0.1
 redistribute bgp
!
router ospf vrf vrf-blue
 ospf router-id 10.2.0.1
 network 10.3.0.0/24 area 0.0.0.0
 redistribute connected
!

실전 구성 예제

기본 VRF 생성과 인터페이스 할당

#!/bin/bash
# VRF 기본 구성 스크립트

# 1. VRF 디바이스 생성
ip link add vrf-red type vrf table 100
ip link add vrf-blue type vrf table 200

# 2. VRF 디바이스 활성화
ip link set vrf-red up
ip link set vrf-blue up

# 3. 인터페이스를 VRF에 종속
ip link set eth1 master vrf-red
ip link set eth2 master vrf-red
ip link set eth3 master vrf-blue
ip link set eth4 master vrf-blue

# 4. 인터페이스에 IP 주소 할당
ip addr add 10.1.0.1/24 dev eth1
ip addr add 10.2.0.1/24 dev eth2
ip addr add 10.1.0.1/24 dev eth3    # 다른 VRF이므로 동일 IP 가능
ip addr add 172.16.0.1/24 dev eth4

# 5. VRF별 기본 경로 추가
ip route add default via 10.1.0.254 table 100
ip route add default via 10.1.0.254 table 200

# 6. 인터페이스 활성화
ip link set eth1 up
ip link set eth2 up
ip link set eth3 up
ip link set eth4 up

# 7. 확인
ip vrf show
ip route show vrf vrf-red
ip route show vrf vrf-blue

멀티테넌트 구성 예제

#!/bin/bash
# ISP PE 라우터 멀티테넌트 VRF 구성
# 고객 A, B, C 각각 독립 라우팅 도메인

# VLAN 인터페이스 생성
ip link add link eth0 name eth0.100 type vlan id 100
ip link add link eth0 name eth0.200 type vlan id 200
ip link add link eth0 name eth0.300 type vlan id 300

# VRF 생성
ip link add vrf-cust-a type vrf table 1001
ip link add vrf-cust-b type vrf table 1002
ip link add vrf-cust-c type vrf table 1003

# VRF 활성화
ip link set vrf-cust-a up
ip link set vrf-cust-b up
ip link set vrf-cust-c up

# VLAN을 VRF에 종속
ip link set eth0.100 master vrf-cust-a
ip link set eth0.200 master vrf-cust-b
ip link set eth0.300 master vrf-cust-c

# IP 주소 할당 (각 고객이 동일 사설 대역 사용 가능)
ip addr add 10.0.0.1/24 dev eth0.100
ip addr add 10.0.0.1/24 dev eth0.200
ip addr add 10.0.0.1/24 dev eth0.300

# VLAN 활성화
ip link set eth0.100 up
ip link set eth0.200 up
ip link set eth0.300 up

# 고객별 기본 게이트웨이
ip route add default via 10.0.0.254 table 1001
ip route add default via 10.0.0.254 table 1002
ip route add default via 10.0.0.254 table 1003

# IP 포워딩 활성화
sysctl -w net.ipv4.ip_forward=1

# VRF별 포워딩 확인
sysctl -w net.ipv4.conf.all.forwarding=1

# 상태 확인
echo "=== VRF 목록 ==="
ip vrf show

echo "=== 고객 A 라우팅 ==="
ip route show vrf vrf-cust-a

echo "=== 고객 B 라우팅 ==="
ip route show vrf vrf-cust-b

echo "=== 고객 C 라우팅 ==="
ip route show vrf vrf-cust-c

VRF + VXLAN 오버레이 구성

# EVPN/VXLAN + VRF 구성 (L3VNI)

# VRF 생성
ip link add vrf-tenant1 type vrf table 1001
ip link set vrf-tenant1 up

# L3 VNI (VXLAN) 생성
ip link add vxlan1001 type vxlan id 1001 \
    local 10.0.0.1 dstport 4789 nolearning
ip link set vxlan1001 master vrf-tenant1
ip link set vxlan1001 up

# Bridge 생성 (L2 VNI)
ip link add br100 type bridge
ip link set br100 master vrf-tenant1
ip link set br100 up

# L2 VXLAN 생성
ip link add vxlan100 type vxlan id 100 \
    local 10.0.0.1 dstport 4789 nolearning
ip link set vxlan100 master br100
ip link set vxlan100 up

# SVI (Switch Virtual Interface) 구성
ip addr add 192.168.100.1/24 dev br100

systemd 서비스와 VRF 통합

# /etc/systemd/system/nginx-vrf-red.service
[Unit]
Description=Nginx in VRF red
After=network.target

[Service]
Type=forking
ExecStart=/usr/bin/ip vrf exec vrf-red /usr/sbin/nginx -c /etc/nginx/nginx-vrf-red.conf
ExecReload=/bin/kill -s HUP $MAINPID
PrivateTmp=true

[Install]
WantedBy=multi-user.target

고급 기능

VRF Route Leaking 심화

VRF 간 라우트 리킹은 선택적으로 경로를 공유하는 메커니즘입니다. 커널 수준에서는 ip rule + fwmark 조합 또는 FRR의 BGP import/export 기능으로 구현합니다.

# 커널 수준 라우트 리킹: ip rule + fwmark

# vrf-red에서 특정 목적지는 vrf-blue 테이블 사용
ip rule add from all iif vrf-red to 192.168.200.0/24 table 200 prio 500

# nftables를 이용한 fwmark 기반 리킹
nft add rule inet filter forward \
    iifname "vrf-red" ip daddr 192.168.200.0/24 \
    meta mark set 0x200

ip rule add fwmark 0x200 table 200 prio 500

# 양방향 리킹 (vrf-blue -> vrf-red 응답 경로)
ip rule add from 192.168.200.0/24 iif vrf-blue to 10.1.0.0/24 table 100 prio 500

MPLS + VRF

MPLS(Multiprotocol Label Switching)와 VRF를 조합하면 PE 라우터에서 MPLS VPN을 구현할 수 있습니다. MPLS 레이블이 VRF의 라우팅 테이블과 연결됩니다.

# MPLS + VRF 구성

# MPLS 모듈 로드
modprobe mpls_router
modprobe mpls_iptunnel

# MPLS 인터페이스 활성화
sysctl -w net.mpls.platform_labels=1048575
sysctl -w net.mpls.conf.eth0.input=1

# VRF에 MPLS 레이블 바인딩
ip -f mpls route add 100 dev vrf-red
ip -f mpls route add 200 dev vrf-blue

# MPLS 캡슐화 경로 (VRF 내부)
ip route add 192.168.100.0/24 encap mpls 100 \
    via inet 10.0.0.2 dev eth0 table 1001

EVPN + VRF (VXLAN L3)

EVPN(Ethernet VPN)은 BGP를 사용하여 VXLAN 터널의 MAC/IP 학습을 제어 플레인에서 처리하는 기술입니다. VRF와 결합하면 L3 VXLAN(L3VNI)을 통해 서로 다른 서브넷 간 라우팅을 VRF 단위로 수행합니다.

# FRR EVPN + VRF 설정
!
vrf vrf-tenant1
 vni 1001
exit-vrf
!
router bgp 65001
 address-family l2vpn evpn
  neighbor 10.0.0.2 activate
  advertise-all-vni
 exit-address-family
!
router bgp 65001 vrf vrf-tenant1
 address-family ipv4 unicast
  redistribute connected
 exit-address-family
 address-family l2vpn evpn
  advertise ipv4 unicast
 exit-address-family
!

Unnumbered 인터페이스와 VRF

Unnumbered 인터페이스(IP 주소 없이 동작)는 BGP unnumbered 피어링에서 자주 사용됩니다. VRF 환경에서도 unnumbered 인터페이스를 지원하며, IPv6 링크-로컬 주소를 활용합니다.

# Unnumbered BGP 피어링 (VRF 내)
!
router bgp 65001 vrf vrf-red
 neighbor eth1 interface remote-as external
 !
 address-family ipv4 unicast
  neighbor eth1 activate
  redistribute connected
 exit-address-family
!

VRF Strict Mode

커널 5.15부터 도입된 VRF strict mode는 VRF에 종속되지 않은 인터페이스로의 패킷 유출을 방지합니다.

# VRF strict mode 활성화
sysctl -w net.vrf.strict_mode=1

# strict mode에서는:
# - 동일 테이블 번호를 가진 VRF가 2개 이상 존재할 수 없음
# - VRF 바인딩되지 않은 인터페이스로의 경로 유출 차단
# - 보안 강화 (멀티테넌트 환경 권장)

디버깅과 모니터링

VRF 상태 확인 명령

# VRF 디바이스 목록
$ ip vrf show
Name              Table
-----------------------
vrf-red           100
vrf-blue          200

# VRF 디바이스 상세 정보
$ ip -d link show type vrf
5: vrf-red: <NOARP,MASTER,UP,LOWER_UP> mtu 65575 qdisc noqueue state UP mode DEFAULT
    link/ether 00:00:00:00:00:00 brd ff:ff:ff:ff:ff:ff promiscuity 0
    vrf table 100 addrgenmode eui64 numtxqueues 1 numrxqueues 1

# 특정 VRF의 슬레이브 인터페이스 확인
$ ip link show master vrf-red
3: eth1: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 ...
4: eth2: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 ...

# VRF별 IP 주소
$ ip addr show vrf vrf-red

라우팅 디버깅

# VRF별 라우팅 테이블
$ ip route show vrf vrf-red
$ ip route show table 100

# 특정 목적지까지의 경로 추적 (VRF 컨텍스트)
$ ip route get 8.8.8.8 vrf vrf-red
$ ip route get fibmatch 10.0.0.0/24 vrf vrf-red

# FIB 규칙 확인
$ ip rule show
$ ip -6 rule show

# FIB trie 덤프 (디버깅용)
$ cat /proc/net/fib_trie
$ cat /proc/net/fib_triestat

# VRF별 이웃 테이블
$ ip neigh show dev eth1
$ ip neigh show dev eth3

tcpdump을 이용한 VRF 패킷 캡처

# 방법 1: 슬레이브 인터페이스에서 직접 캡처
$ tcpdump -i eth1 -nn

# 방법 2: VRF 디바이스에서 캡처 (VRF를 통과하는 모든 트래픽)
$ tcpdump -i vrf-red -nn

# 방법 3: ip vrf exec를 통한 캡처
$ ip vrf exec vrf-red tcpdump -i any -nn

# 방법 4: 특정 VRF의 TCP 연결만 캡처
$ ip vrf exec vrf-red tcpdump -i eth1 tcp port 80 -nn

# 방법 5: tshark으로 VRF 정보 포함 캡처
$ ip vrf exec vrf-red tshark -i eth1 -f "host 10.1.0.2"

커널 트레이싱

# ftrace로 l3mdev 훅 추적
$ echo 1 > /sys/kernel/debug/tracing/events/net/net_dev_xmit/enable
$ echo 1 > /sys/kernel/debug/tracing/events/fib/fib_table_lookup/enable
$ cat /sys/kernel/debug/tracing/trace_pipe

# perf로 VRF 관련 함수 프로파일링
$ perf probe --add 'vrf_l3_rcv'
$ perf probe --add 'vrf_ip_rcv'
$ perf record -e probe:vrf_l3_rcv -e probe:vrf_ip_rcv -a -- sleep 10
$ perf report

# bpftrace로 VRF 패킷 추적
$ bpftrace -e 'kprobe:vrf_l3_rcv {
    printf("VRF l3_rcv: dev=%s skb=%p\n",
           str(((struct net_device *)arg0)->name), arg1);
}'

# dropwatch로 VRF 관련 패킷 드롭 탐지
$ dropwatch -l kas

일반적인 문제와 해결

문제증상해결 방법
VRF에서 ping 실패 Network unreachable ip route show vrf <vrf>로 기본 경로 확인, ip vrf exec 사용
TCP 연결 거부 VRF 리스너에 연결 불가 SO_BINDTODEVICE 확인, tcp_l3mdev_accept sysctl 확인
ARP 해석 실패 이웃 엔트리 FAILED 슬레이브 인터페이스 UP 확인, arp_filter/arp_announce 확인
라우트 리킹 불가 VRF 간 통신 불가 ip rule에 교차 테이블 규칙 추가, FRR import vrf 설정
FIB 규칙 누락 l3mdev-table 규칙 없음 VRF 삭제 후 재생성, 또는 수동 규칙 추가
VRF strict mode 위반 동일 테이블 VRF 생성 거부 테이블 번호 고유성 확인, net.vrf.strict_mode 비활성화 검토
IPv6 링크-로컬 문제 VRF 내 IPv6 통신 실패 accept_ra, autoconf sysctl 확인, l3mdev_link_scope_lookup 동작 확인
주의: VRF 디바이스의 loopback 주소
VRF 디바이스 자체에 loopback 주소를 할당하면 VRF 내에서 해당 주소로의 통신이 가능합니다. 이는 BGP router-id나 서비스 주소로 활용됩니다. 단, VRF 디바이스의 주소는 해당 VRF의 라우팅 테이블에만 존재하므로, 다른 VRF에서 접근하려면 라우트 리킹이 필요합니다.

커널 설정

필수 Kconfig 옵션

옵션의존성설명
CONFIG_NET_VRF CONFIG_IP_MULTIPLE_TABLES
CONFIG_NET_L3_MASTER_DEV
VRF 디바이스 드라이버 (drivers/net/vrf.c)
CONFIG_NET_L3_MASTER_DEV CONFIG_INET l3mdev 프레임워크 (net/l3mdev/)
CONFIG_IP_MULTIPLE_TABLES CONFIG_INET IPv4 다중 라우팅 테이블 (정책 라우팅)
CONFIG_IPV6_MULTIPLE_TABLES CONFIG_IPV6 IPv6 다중 라우팅 테이블
CONFIG_IP_ROUTE_MULTIPATH CONFIG_IP_MULTIPLE_TABLES ECMP (선택적, 권장)
CONFIG_MPLS - MPLS + VRF 기능 (선택적)
CONFIG_NET_FIB_RULES 자동 선택 FIB 규칙 프레임워크

커널 설정 확인 방법

# 현재 커널의 VRF 지원 확인
$ zcat /proc/config.gz | grep -E "NET_VRF|L3_MASTER|MULTIPLE_TABLES|FIB_RULES"
CONFIG_NET_L3_MASTER_DEV=y
CONFIG_NET_VRF=m
CONFIG_IP_MULTIPLE_TABLES=y
CONFIG_IPV6_MULTIPLE_TABLES=y
CONFIG_NET_FIB_RULES=y

# 또는 /boot/config 파일 확인
$ grep -E "NET_VRF|L3_MASTER" /boot/config-$(uname -r)

# VRF 모듈 로드 확인
$ lsmod | grep vrf
vrf                    32768  0

# VRF 모듈 수동 로드
$ modprobe vrf

주요 sysctl 파라미터

sysctl기본값설명
net.ipv4.tcp_l3mdev_accept 0 글로벌 TCP 리스너가 VRF 패킷 수락 여부
net.ipv4.udp_l3mdev_accept 0 글로벌 UDP 소켓이 VRF 패킷 수신 여부
net.ipv4.raw_l3mdev_accept 1 글로벌 RAW 소켓이 VRF 패킷 수신 여부
net.vrf.strict_mode 0 1이면 VRF 테이블 번호 고유성 강제
net.ipv4.ip_forward 0 1이어야 VRF 간 포워딩 동작
net.ipv4.conf.all.rp_filter 0/1/2 역경로 필터링 (VRF 환경에서는 0 또는 2 권장)
net.ipv6.conf.all.forwarding 0 IPv6 VRF 포워딩 활성화
# VRF 운영에 권장되는 sysctl 설정
# /etc/sysctl.d/99-vrf.conf

# IP 포워딩 활성화
net.ipv4.ip_forward = 1
net.ipv6.conf.all.forwarding = 1

# 글로벌 리스너가 VRF 패킷을 수락하지 않도록 (보안)
net.ipv4.tcp_l3mdev_accept = 0
net.ipv4.udp_l3mdev_accept = 0

# VRF strict mode (테이블 번호 고유성 강제)
net.vrf.strict_mode = 1

# 역경로 필터링 (VRF 비대칭 라우팅 허용)
net.ipv4.conf.all.rp_filter = 0
net.ipv4.conf.default.rp_filter = 0

# VRF 디바이스에 대한 RP 필터 비활성화
# (VRF는 논리적 디바이스이므로 RP 필터가 오동작할 수 있음)
rp_filter 주의: VRF 환경에서 rp_filter=1(strict)을 사용하면 비대칭 라우팅으로 인해 패킷이 드롭될 수 있습니다. 특히 VRF 간 라우트 리킹을 사용하는 경우 rp_filter=0 또는 rp_filter=2(loose)로 설정해야 합니다.

커널 모듈 자동 로드 설정

# /etc/modules-load.d/vrf.conf
vrf

# 또는 modprobe 설정
# /etc/modprobe.d/vrf.conf
# 특별한 옵션 없음 (기본 설정으로 충분)

VRF 성능 튜닝

튜닝 항목설정효과
FIB 테이블 크기 적절한 테이블 번호 범위 사용 FIB trie 효율성 유지
이웃 테이블 크기 net.ipv4.neigh.default.gc_thresh3 다수 VRF 사용 시 이웃 캐시 부족 방지
ARP 캐시 타임아웃 net.ipv4.neigh.default.gc_stale_time VRF별 빈번한 ARP 재해석 방지
FIB 규칙 수 최소한의 커스텀 규칙 사용 규칙 순회 오버헤드 감소
VRF 개수 필요한 만큼만 생성 메모리 사용량 최적화

참고자료

커널 문서

커널 소스 코드

RFC 및 표준

FRRouting

관련 커널 커밋

블로그 및 프레젠테이션

필수 관련 문서: 참고 문서: