라우팅 (Routing Subsystem)
Linux 커널 라우팅 서브시스템을 실제 조회 경로 기준으로 정리합니다. flowi 입력, RPDB(ip rule), local/main/default 및 커스텀 테이블, LC-trie/FIB6 prefix tree, nexthop 객체, neighbour(ARP/NDP) 해석, VRF l3mdev, ECMP, SRv6, NAT/mark에 의한 재평가 지점을 하나의 데이터 경로로 연결해 설명합니다.
skb에서 추출한 selector가 규칙, 테이블, nexthop, neighbour, 출력 디바이스까지 연쇄적으로 영향을 주는 구조이므로 IP 계층과 설정 전달 경로를 먼저 이해해야 합니다.
핵심 요약
- RPDB —
ip rule우선순위 목록이 어떤 라우팅 테이블을 볼지 먼저 결정합니다. - 기본 테이블 —
local(255),main(254),default(253)는 커널이 기본으로 사용하는 예약 테이블입니다. - Longest Prefix Match — 같은 테이블 안에서는 가장 구체적인 접두사가 이깁니다.
- nexthop 과 neighbour — 경로가 "다음으로 어디 보낼지"를 고르면, neighbour가 실제 L2 주소를 해결합니다.
- dst_entry — 최종 결과는
dst_entry/rtable에 담겨 출력 경로, PMTU, redirect 예외와 함께 재사용됩니다.
단계별 이해
- selector 확인
목적지뿐 아니라 소스 주소, mark, iif/oif, TOS, L4 포트까지 어떤 값이 조회에 들어가는지 확인합니다. - 규칙과 테이블 분리
ip rule이 고른 테이블과, 그 테이블 안에서 LPM이 고른 엔트리를 따로 봅니다. - route 의미 해석
type,scope,proto,metric이 실제 동작에 어떤 의미인지 구분합니다. - neighbour 까지 추적
라우트가 맞아도 ARP/NDP가 실패하면 출력이 막히므로ip neigh까지 확인합니다. - 실측으로 검증
ip route get,ip monitor,tcpdump,tracepoint로 실제 경로를 검증합니다.
- 전체 조감도 (개요, 파이프라인) — 라우팅이 무엇이고, 패킷이 거치는 7단계 경로
- FIB 핵심 (구조체, LC-trie, 조회 경로) — 커널이 경로를 저장하고 찾는 방법
- FIB 결과 처리 (fib_result→dst_entry, 콜백, 참조 카운팅) — 조회 결과가 패킷에 어떻게 적용되는지
- FIB 지원 시스템 (통지, nexthop 객체, 메모리, HW offload) — 운영에 필요한 보조 메커니즘
- FIB 특수 주제 (flowi 상세, 예외 캐시, 멀티캐스트/MPLS, 네임스페이스) — 필요할 때 참조
- 사용자 관점 (테이블 관리, Policy Routing) —
ip route/ip rule실전 사용법 - 프로토콜 확장 (IPv6, ECMP) — IPv4 지식을 IPv6와 다중 경로로 확장
- Last Mile (Neighbour, VRF, Netfilter) — 경로 결정 후 실제 전송까지의 마지막 단계
- 운영 (캐시/최적화, 성능 튜닝, 디버깅, SRv6) — 실무에서 필요한 도구와 기법
라우팅 서브시스템 개요
이 섹션에서는 라우팅 서브시스템의 전체 구성 요소를 조감도로 살펴봅니다. 각 요소의 상세한 내부 구조와 동작은 이후 섹션에서 하나씩 다룹니다.
Linux 라우팅 서브시스템은 목적지 주소 하나만 보고 다음 홉을 고르는 단순 표 조회가 아닙니다. 실제 hot path는 flowi4/flowi6 selector 생성, fib_rules_lookup()에 의한 테이블 선택, 각 테이블 내부의 Longest Prefix Match, route type/scope/proto 해석, nexthop 또는 nexthop group 선택, neighbour(ARP/NDP) 해석, dst_entry 생성 순으로 이어집니다.
패킷 흐름에서 라우팅의 위치
라우팅 결정은 네트워크 스택에서 두 지점에서 발생합니다:
| 경로 | 라우팅 함수 | 시점 | 설명 |
|---|---|---|---|
| 수신 경로 (RX) | ip_rcv() → ip_route_input_noref() | NF_INET_PRE_ROUTING 뒤 | 로컬 배달(RTN_LOCAL)인지 포워딩인지 결정 |
| 송신 경로 (TX) | ip_route_output_flow() | 소켓에서 패킷 생성 직후 | 출력 인터페이스, 선택 소스 주소, nexthop 결정 |
| OUTPUT 재평가 | ip_route_me_harder() 계열 재조회 | NF_INET_LOCAL_OUT에서 mark/DNAT 변경 시 | 이미 계산된 dst를 무효화하고 새 결과를 다시 계산 |
| 출력 직전 해석 | dst_neigh_output() 계열 | egress 직전 | ARP/NDP로 L2 목적지를 확인하고 실제 전송 큐로 전달 |
RPDB에서 출력 디바이스까지
운영 관점에서 가장 중요한 질문은 "이 패킷이 어느 테이블을 봤고, 왜 그 nexthop이 선택됐으며, 실제로 어느 L2 이웃으로 나가느냐"입니다. 라우팅 장애는 이 다섯 단계 중 하나에서 끊어집니다.
- selector 생성 — 커널은
skb또는 소켓 정보에서 목적지, 소스, mark, iif/oif, L4 포트 등을 추출해flowi를 만듭니다. - RPDB 스캔 —
fib_rules_lookup()가 priority 오름차순으로 규칙을 훑어 어떤 테이블을 볼지 결정합니다. - 테이블 내부 조회 — 선택된 테이블 안에서 가장 긴 접두사와 route semantics(
type,scope,metric,prefsrc)를 평가합니다. - nexthop 및 neighbour 해석 — route가 참조하는 게이트웨이, 출력 디바이스, ECMP 그룹을 정한 뒤 ARP/NDP로 실제 L2 목적지를 확인합니다.
- dst 생성 및 출력 — 최종 결과는
dst_entry/rtable에 연결되고, PMTU/redirect 예외와 함께 출력 경로에 전달됩니다.
# 조회를 단계별로 확인하는 기본 세트
ip rule show
ip route show table local
ip -details route show table all
# 실제 조회 결과 (규칙 + 테이블 + 선택 소스 주소 반영)
ip route get 203.0.113.10 from 192.0.2.10 iif eth1 mark 0x10
# 어떤 FIB 엔트리가 맞았는지 직접 확인
ip route get fibmatch 203.0.113.10
ip route get은 "최종적으로 선택된 결과"를 보여주고, fibmatch는 "어느 FIB 엔트리가 일치했는지"를 보여줍니다.
둘을 함께 봐야 rule, source address 선택, nexthop group, 예외 캐시의 영향을 분리해서 이해할 수 있습니다.
FIB 내부 구조
FIB(Forwarding Information Base)는 ip route 명령으로 보는 라우팅 테이블을, 커널이 매우 빠르게 조회할 수 있도록 내부적으로 저장하는 방식입니다. 패킷이 들어올 때마다 "이 목적지 IP는 어디로 보내야 하나?"를 수십~수백 나노초 안에 결정해야 하므로, 단순한 리스트가 아닌 특별한 트리 자료구조를 사용합니다.
10-0-1-5를 찾는다고 생각해 보세요. 처음부터 한 줄씩 훑지 않습니다. 먼저 "10"으로 시작하는 탭을 펼치고, 그 안에서 "10-0" 탭, 다시 "10-0-1" 탭으로 좁혀갑니다. 정확히 일치하는 항목이 없으면 가장 구체적인 탭의 정보를 사용합니다. 이것이 LPM(Longest Prefix Match)이고, LC-trie는 이 색인 탭을 효율적으로 관리하는 커널의 자료구조입니다.
FIB는 단순히 "목적지→게이트웨이" 매핑이 아니라, 접두사(prefix) 기반 트리 자료구조 위에 경로 메타데이터(type, scope, metric), nexthop 객체, 이벤트 통지 체계, HW offload 인터페이스까지 포함하는 복합 서브시스템입니다. Linux IPv4는 LC-trie(Level-Compressed trie), IPv6는 fib6_node 기반 prefix tree를 사용하여 최적의 LPM 성능을 달성합니다.
FIB의 역할과 위치
커널 네트워크 스택에서 FIB는 세 가지 핵심 역할을 수행합니다:
| 역할 | 설명 | 관련 함수 |
|---|---|---|
| 경로 저장 | 사용자 공간(ip route add), 라우팅 데몬(BGP/OSPF), 커널 자동 생성 경로를 prefix tree에 저장 | fib_table_insert(), fib_create_info() |
| LPM 조회 | 목적지 주소에 가장 구체적으로 일치하는 접두사를 찾아 nexthop과 출력 디바이스를 반환 | fib_table_lookup(), fib_lookup() |
| 이벤트 전파 | 경로 추가/삭제/변경을 구독자(switchdev, TC, BPF, netlink)에게 통지 | fib_notify(), call_fib_notifiers() |
FIB 자료구조 계층
FIB의 핵심은 prefix(접두사)와 route(경로) 정보를 분리한 설계입니다. 하나의 접두사(예: 10.0.0.0/24)에 여러 경로(fib_alias)가 붙을 수 있고, 서로 다른 접두사가 동일한 nexthop 정보(fib_info)를 공유할 수 있습니다. 이 분리 덕분에 대규모 라우팅 테이블에서도 메모리 효율이 유지됩니다.
10.0.0.0/24에 대해 TOS가 다르거나 type이 다른(unicast vs local) 복수의 경로가 존재할 수 있습니다. fib_alias는 이 복수 경로 각각을 나타내며, fa_tos, fa_type, fa_scope로 구별됩니다. 이 설계 덕분에 ip route add 10.0.0.0/24 via 192.168.1.1 tos 0x10과 같은 TOS 기반 경로 분리가 자연스럽게 가능합니다.
핵심 자료구조
fib_table= 전화번호부 한 권 (main, local 등 여러 권이 있음)key_vector= 전화번호부의 색인 탭 (trie 노드 — 비트별로 분기)fib_alias= 같은 전화번호에 달린 개별 연락처 카드 (같은 IP 접두사에 여러 경로)fib_info= 실제 배송 정보 (게이트웨이 주소, 출력 인터페이스, 메트릭)fib_nh= 택배 기사 한 명 (게이트웨이 + 디바이스 조합)fib_result= 조회 결과 봉투 (일치한 접두사, 선택된 nexthop, 경로 유형)flowi4= 조회 요청서 (목적지 주소, 소스 주소, TOS, mark 등)
/* net/ipv4/fib_semantics.c, net/ipv4/fib_trie.c */
/* fib_table: 하나의 라우팅 테이블 (예: main=254, local=255) */
struct fib_table {
struct hlist_node tb_hlist; /* 테이블 해시 리스트 */
u32 tb_id; /* 테이블 ID (254=main, 255=local) */
int tb_num_default; /* default 경로 수 */
struct rcu_head rcu;
unsigned long __data[]; /* trie 루트 (struct trie) */
};
/* fib_info: 경로(route)의 메타데이터 (여러 fib_alias가 공유 가능) */
struct fib_info {
struct hlist_node fib_hash;
int fib_treeref; /* 참조 카운트 */
u32 fib_flags;
unsigned char fib_scope; /* RT_SCOPE_UNIVERSE, LINK, HOST */
unsigned char fib_type; /* RTN_UNICAST, LOCAL, BROADCAST, ... */
u32 fib_priority; /* 경로 메트릭 (낮을수록 우선) */
struct nexthop *nh; /* nexthop 객체 (5.x+) */
int fib_nhs; /* nexthop 수 (multipath) */
struct fib_nh fib_nh[]; /* nexthop 배열 (레거시) */
};
/* fib_nh: 개별 nexthop (게이트웨이 + 출력 디바이스) */
struct fib_nh {
struct fib_nh_common nh_common;
struct net_device *fib_nh_dev; /* 출력 디바이스 */
__be32 fib_nh_gw4; /* IPv4 게이트웨이 */
int fib_nh_weight; /* ECMP 가중치 */
u32 fib_nh_oif; /* 출력 인터페이스 인덱스 */
u8 fib_nh_scope;
};
/* fib_alias: 같은 prefix에 대한 개별 경로 엔트리 */
struct fib_alias {
struct hlist_node fa_list; /* leaf의 fib_alias 체인 */
struct fib_info *fa_info; /* 공유 가능한 경로 메타데이터 */
dscp_t fa_dscp; /* DSCP/TOS 선택자 (6.x+) */
u8 fa_type; /* RTN_UNICAST, RTN_LOCAL, ... */
u8 fa_state; /* FA_S_ACCESSED 등 */
u8 fa_slen; /* suffix length (trie 최적화) */
u32 tb_id; /* 소속 테이블 ID */
s16 fa_default; /* default route 인덱스 */
struct rcu_head rcu;
};
/* fib_result: FIB 조회 결과를 담는 구조체 */
struct fib_result {
__be32 prefix; /* 일치한 접두사 */
unsigned char prefixlen; /* 접두사 길이 (0~32) */
unsigned char nh_sel; /* 선택된 nexthop 인덱스 */
unsigned char type; /* RTN_UNICAST 등 */
unsigned char scope; /* RT_SCOPE_UNIVERSE 등 */
u32 tclassid; /* TC 분류 ID */
struct fib_info *fi; /* 경로 메타데이터 */
struct fib_table *table; /* 조회에 사용된 테이블 */
struct fib_nh_common *nhc; /* 선택된 nexthop */
};
/* flowi4: 라우팅 조회 입력 키 (selector) */
struct flowi4 {
struct flowi_common __fl_common;
__be32 saddr; /* 소스 주소 */
__be32 daddr; /* 목적지 주소 */
/* flowi_common 포함:
* flowi4_oif — 출력 인터페이스 인덱스
* flowi4_iif — 입력 인터페이스 인덱스
* flowi4_mark — fwmark (RPDB selector)
* flowi4_tos — TOS/DSCP
* flowi4_scope — RT_SCOPE_UNIVERSE 등
* flowi4_proto — L4 프로토콜 (TCP/UDP)
* fl4_sport — L4 소스 포트
* fl4_dport — L4 목적지 포트
*/
};
fib_table→ 하나의 라우팅 테이블.__data[]뒤에struct trie(rootkey_vector)가 위치key_vector→ LC-trie 노드. internal 노드는 자식 배열(tnode[])을, leaf 노드는fib_alias리스트를 가짐fib_alias→ 같은 prefix에 대한 개별 route. TOS/type이 다르면 같은 leaf에 여러 alias가 붙음fib_info→ nexthop 세트와 공통 메타데이터. 여러 alias가 같은fib_info를 참조 가능 (refcount)fib_nh→ 개별 nexthop. 게이트웨이 주소, 출력 디바이스, ECMP weight 보유fib_result→ 조회 결과. 일치한 prefix, 선택된 nexthop, type/scope 등을 반환flowi4→ 조회 입력 키. 소스/목적지 주소, mark, iif, TOS, L4 포트 포함
fib_alias와 fib_info 공유 메커니즘
192.168.1.1)를 가리킨다면? 경로마다 게이트웨이 정보를 복사해서 10만 개 저장하면 메모리 낭비입니다. 대신 게이트웨이 정보(fib_info)를 딱 하나만 만들고, 10만 개의 경로 카드(fib_alias)가 그것을 "참조"합니다. 마치 도서관에서 같은 책 10만 권을 사는 대신, 1권의 책에 10만 장의 대출 카드를 만드는 것과 같습니다.
FIB의 메모리 효율은 fib_info 공유에 크게 의존합니다. 예를 들어 BGP 라우터에서 10만 개의 접두사가 모두 같은 게이트웨이를 가리킨다면, 10만 개의 fib_alias가 하나의 fib_info를 공유합니다. fib_info는 전역 해시 테이블(fib_info_hash)에 등록되어 중복 생성이 방지됩니다.
/* net/ipv4/fib_semantics.c — fib_info 생성/공유 로직 */
/* fib_create_info(): 새 route 추가 시 호출
* 1. 요청된 nexthop 집합으로 fib_info 후보 생성
* 2. fib_info_hash에서 동일 속성(gw, dev, scope, flags)의 기존 fi 검색
* 3. 발견되면 refcount++ 후 기존 fi 반환 (새 fi 해제)
* 4. 없으면 새 fi를 해시에 등록하고 반환
*/
struct fib_info *fib_create_info(struct fib_config *cfg) {
struct fib_info *fi, *ofi;
fi = kzalloc(struct_size(fi, fib_nh, nhs), GFP_KERNEL);
/* nexthop 초기화 ... */
ofi = fib_find_info(fi); /* 기존 동일 fib_info 검색 */
if (ofi) {
fi->fib_dead = 1;
fib_info_put(fi); /* 새로 만든 fi 해제 */
ofi->fib_treeref++; /* 기존 fi 참조 증가 */
return ofi;
}
/* 새 fi를 해시에 등록 */
hlist_add_head(&fi->fib_hash, &fib_info_hash[...]);
return fi;
}
/* fib_info 해제: refcount가 0이 되면 실제 해제 */
void fib_info_put(struct fib_info *fi) {
if (refcount_dec_and_test(&fi->fib_clntref))
free_fib_info(fi);
}
| 시나리오 | fib_alias 수 | fib_info 수 | fib_nh 수 | 설명 |
|---|---|---|---|---|
| 단일 default route | 1 | 1 | 1 | 가장 단순한 구성 |
| 같은 gw로 10개 prefix | 10 | 1 | 1 | fib_info 공유로 메모리 절약 |
| ECMP 2경로 × 5 prefix | 5 | 1 | 2 | 같은 nexthop 세트면 fi 공유 |
| BGP full table (100K prefix) | ~100K | 수십~수백 | 수십 | 대부분 소수 fi를 공유 |
| 같은 prefix에 TOS 0 + TOS 0x10 | 2 (같은 leaf) | 2 (다를 수 있음) | 각 1 | TOS별 다른 경로 → 별도 alias |
LC-trie 자료구조
앞에서 FIB가 경로를 저장하는 "전화번호부"라고 배웠습니다. LC-trie는 그 전화번호부의 색인 구조 — 원하는 페이지를 빠르게 찾아가는 방법입니다.
LC-trie는 path-compressed trie에 level compression을 추가한 구조로, 메모리 효율과 조회 속도를 모두 최적화합니다:
/* net/ipv4/fib_trie.c */
/* trie 노드: internal node와 leaf가 같은 구조체 */
struct key_vector {
t_key key; /* 접두사 키 */
unsigned char pos; /* 이 노드에서 검사 시작 비트 위치 */
unsigned char bits; /* 이 노드에서 검사할 비트 수 */
unsigned char slen; /* suffix length (최적화) */
union {
struct hlist_head leaf; /* leaf: fib_alias 리스트 */
struct key_vector *tnode[]; /* internal: 자식 배열 */
};
};
/* LPM (Longest Prefix Match) 알고리즘:
* 1. root에서 시작, 목적지 IP의 비트를 순차적으로 검사
* 2. 각 internal 노드에서 bits만큼의 비트로 자식 인덱스 결정
* 3. leaf 도달 시 접두사 일치 확인
* 4. 일치하지 않으면 부모로 backtrack하여 더 짧은 접두사 탐색
* 5. 가장 긴 일치 접두사 반환
*
* 시간 복잡도: O(W) where W = 주소 길이(32비트)
* 실제: level compression 덕분에 대부분 O(log n) 이하
*/
# FIB trie 내부 구조 확인
cat /proc/net/fib_trie
# Main:
# +-- 0.0.0.0/0 3 0 0
# +-- 0.0.0.0/4 2 0 0
# +-- 10.0.0.0/24 2 0 0
# |-- 10.0.0.0
# /24 host LOCAL
# +-- 10.0.1.0/24 2 0 0
# |-- 10.0.1.0
# /24 link UNICAST
# FIB 통계
cat /proc/net/fib_triestat
# Basic info: size of leaf/tnode, Max depth, Prefixes, ...
LC-trie 시각적 이해
LC-trie의 핵심은 두 가지 압축입니다: path compression(중간에 분기가 없는 경로를 건너뜀)과 level compression(연속 1비트 분기를 한 번에 여러 비트로 확장). 아래 다이어그램은 4개의 경로가 LC-trie에 저장되는 모습을 보여줍니다.
10.0.1.5를 위 LC-trie에서 실제로 조회하는 과정을 한 단계씩 보여줍니다. 각 노드에서 어떤 비트를 검사하고, 어떤 자식으로 내려가는지 따라가 보세요.
pos— 이 노드에서 검사를 시작할 비트 위치 (0=MSB). path compression의 결과bits— 이 노드에서 한번에 검사할 비트 수. level compression의 결과.bits=n이면2^n개 자식slen— suffix length. backtrack 최적화에 사용. 이 값이 0이면 이 leaf 이하에 더 이상 검사할 접두사가 없음key— 이 노드의 접두사 키 (32비트 IP를 정수로 표현)
LC-trie 삽입, 삭제, 리밸런싱
LC-trie는 정적 구조가 아니라 경로 추가/삭제 시 동적으로 변합니다. 커널은 fib_insert_node()와 fib_remove()로 trie를 수정하며, 자식 수 변화에 따라 level compression 수준을 자동 조정합니다.
/* net/ipv4/fib_trie.c — trie 수정 핵심 로직 */
/* 경로 삽입 과정:
* 1. 목적지 IP의 비트를 따라 trie를 내려감
* 2. 적절한 위치에 new leaf 또는 new internal node 삽입
* 3. path compression 재계산 (pos 조정)
* 4. level compression 재계산 (bits 조정)
* 5. slen(suffix length) 갱신 — 부모 chain까지 전파
*/
static void fib_insert_node(struct trie *t,
struct key_vector *tp,
struct fib_alias *new_fa,
t_key key) {
/* Case 1: 빈 슬롯에 leaf 직접 삽입 */
/* Case 2: 기존 leaf와 접두사가 다름 → new internal node 생성
* 기존 leaf와 new leaf를 자식으로 배치 */
/* Case 3: 기존 leaf에 fib_alias만 추가 (같은 prefix, 다른 TOS/type) */
}
/* inflate/halve — level compression 조정 */
static struct key_vector *inflate(struct trie *t,
struct key_vector *oldtnode) {
/* bits를 1 증가 → 자식 배열을 2배로 확장
* 기존 자식들을 새 위치로 재배치
* 사용률이 50% 이상일 때 호출 */
}
static struct key_vector *halve(struct trie *t,
struct key_vector *oldtnode) {
/* bits를 1 감소 → 자식 배열을 절반으로 축소
* 사용률이 25% 이하일 때 호출 */
}
/* resize — 삽입/삭제 후 최적 bits 수 결정 */
static void resize(struct trie *t, struct key_vector *tn) {
/* 자식 노드 채우기 비율 계산:
* full = (non-null children) / (1 << tn->bits)
* full > 50% → inflate() (bits 증가)
* full < 25% → halve() (bits 감소)
* 25~50% → 현재 유지
*/
}
| 연산 | 시간 복잡도 | 핵심 단계 | RCU 고려 |
|---|---|---|---|
조회 (fib_table_lookup) | O(W) worst, 실제 O(log n) | bit 검사 → 자식 선택 → leaf 검증 → backtrack | RCU read lock만 사용, lock-free |
삽입 (fib_table_insert) | O(W) + resize | 위치 탐색 → leaf/internal 생성 → slen 전파 → resize | rtnl_lock 필요, RCU publish로 reader 비차단 |
삭제 (fib_table_delete) | O(W) + resize | alias 제거 → leaf 비면 leaf 제거 → parent resize | rtnl_lock, RCU grace period 후 메모리 해제 |
flush (fib_table_flush) | O(n) | 모든 leaf 순회하며 조건 매칭 엔트리 제거 | 인터페이스 다운, VRF 삭제 시 사용 |
fib_table_lookup)는 rcu_read_lock()만 잡고 수행되므로 어떤 코어에서든 lock 경합 없이 병렬 조회가 가능합니다. 반면 삽입/삭제는 rtnl_lock(네트워크 큰 잠금)을 잡아야 합니다. 이 비대칭 설계 덕분에 데이터 플레인(조회) 성능은 제어 플레인(수정) 빈도에 영향받지 않습니다.
FIB 조회 상세 경로
패킷이 들어오거나 소켓이 전송을 시작하면, 커널은 아래 함수 체인을 따라 FIB를 조회합니다. 각 단계에서 무슨 일이 일어나는지 정확히 이해하면 라우팅 문제의 원인 지점을 빠르게 좁힐 수 있습니다.
fib_table_lookup()은 패킷이 들어올 때마다 호출되는 핵심 함수입니다. 동작을 3단계로 요약하면:
- 하강(Phase 1): 목적지 IP의 비트를 따라 트리를 최대한 깊이 내려갑니다
- 검증(Phase 2): 도달한 leaf의 접두사가 실제로 목적지와 일치하는지 확인합니다
- 복귀(Phase 3): 일치하지 않으면 부모로 올라가(backtrack) 더 짧은 접두사를 찾습니다
for 루프, check_leaf(), backtrace 레이블을 대응시켜 읽어보세요.
/* net/ipv4/fib_trie.c — fib_table_lookup() 핵심 로직 (간략화) */
int fib_table_lookup(struct fib_table *tb,
const struct flowi4 *flp,
struct fib_result *res,
int fib_flags) {
struct trie *t = (struct trie *)tb->__data;
struct key_vector *n, *pn;
t_key key = ntohl(flp->daddr);
unsigned long index;
int ret;
n = rcu_dereference(t->tnode[0]); /* root */
if (!n) return -EAGAIN;
/* Phase 1: trie 하강 — 최대한 깊이 내려감 */
for (;;) {
index = get_cindex(key, n); /* key의 bit[pos..pos+bits]로 인덱스 계산 */
if (IS_LEAF(n)) break; /* leaf 도달 → 검증 단계로 */
if (n->slen > n->pos) { /* 이 서브트리에 더 긴 접두사 존재 */
pn = n;
n = get_child_rcu(n, index);
if (!n) goto backtrace; /* 빈 슬롯 → backtrack */
continue;
}
break;
}
/* Phase 2: leaf 검증 — prefix 길이와 매칭 확인 */
ret = check_leaf(tb, t, n, key, flp, res, fib_flags);
if (ret == 0) return 0; /* 일치! */
/* Phase 3: backtrack — 부모로 올라가 더 짧은 접두사 탐색 */
backtrace:
while ((pn = node_parent_rcu(n)) != NULL) {
/* slen을 기준으로 현재 서브트리에서 더 찾을 가치가 있는지 판단 */
/* 다음 형제(sibling) 슬롯을 시도하거나 부모로 계속 올라감 */
n = pn;
ret = check_leaf(tb, t, n, key, flp, res, fib_flags);
if (ret == 0) return 0;
}
return -ENETUNREACH; /* 어떤 접두사도 일치하지 않음 */
}
/* check_leaf() 내부: fib_alias 체인에서 TOS/scope/type 매칭 */
static int check_leaf(struct fib_table *tb,
struct trie *t,
struct key_vector *l,
t_key key,
const struct flowi4 *flp,
struct fib_result *res,
int fib_flags) {
struct fib_alias *fa;
/* leaf의 fib_alias 리스트를 순회 */
hlist_for_each_entry_rcu(fa, &l->leaf, fa_list) {
/* 1. prefix 길이 확인: key와 fa의 접두사가 실제로 일치하는가 */
if (fa->fa_slen != KEYLENGTH - l->pos)
continue;
/* 2. TOS 매칭: flowi4의 TOS가 이 alias의 TOS와 호환되는가 */
if (fa->fa_dscp &&
fa->fa_dscp != inet_dsfield_to_dscp(flp->flowi4_tos))
continue;
/* 3. scope 매칭: 요청 scope ≤ route scope */
if (fa->fa_info->fib_scope < flp->flowi4_scope)
continue;
/* 4. 일치! fib_result 채움 */
res->fi = fa->fa_info;
res->type = fa->fa_type;
res->prefixlen = KEYLENGTH - fa->fa_slen;
return 0;
}
return 1; /* 이 leaf에서 매칭 실패 → backtrack 계속 */
}
10.0.0.1을 조회할 때 10.0.0.0/24 leaf에 도달했지만, 실제 alias의 scope가 맞지 않으면 backtrack하여 10.0.0.0/8이나 0.0.0.0/0 default route를 찾아야 합니다.
fib_result에서 dst_entry까지
FIB 조회가 완료되면 fib_result에 일치한 경로 정보가 담깁니다. 하지만 커널은 이 조회 결과만으로는 패킷을 보낼 수 없습니다 — 실제 전송에 사용할 dst_entry(패킷과 함께 이동하는 "경로 카드")를 생성해야 합니다. 이 변환이 FIB(경로 찾기)와 출력 경로(패킷 보내기)를 잇는 다리입니다.
fib_table_lookup()이 성공하면 fib_result에 일치한 경로 정보가 채워집니다. 이후 커널은 이 결과를 바탕으로 실제 패킷 전송에 사용할 dst_entry/rtable을 생성합니다.
/* net/ipv4/route.c — __mkroute_output() 핵심 로직 */
static struct rtable *__mkroute_output(
const struct fib_result *res,
const struct flowi4 *fl4,
int orig_oif, struct net_device *dev_out,
unsigned int flags) {
struct fib_info *fi = res->fi;
struct fib_nh_common *nhc = res->nhc;
struct rtable *rth;
/* nexthop exception cache 확인 (PMTU, redirect) */
struct fib_nh_exception *fnhe =
find_exception(nhc, fl4->daddr);
if (fnhe) {
/* 캐시된 PMTU/redirect 정보가 있으면 재사용 */
rth = rcu_dereference(fnhe->fnhe_rth_output);
if (rth && rt_cache_valid(rth))
return rth;
}
/* 새 rtable 할당 */
rth = rt_dst_alloc(dev_out, flags, res->type,
fi && (fi->fib_flags & RTNH_F_POLICY));
rth->rt_gw_family = AF_INET;
rth->rt_gw4 = nhc->nhc_gw.ipv4; /* 게이트웨이 */
rth->rt_type = res->type;
rth->dst.dev = dev_out;
rth->dst.output = ip_output;
return rth;
}
dst_entry 구조체 상세
dst_entry는 패킷이 들고 다니는 "내비게이션 안내문"입니다. 네비게이션 앱이 "다음 교차로에서 우회전, 500m 직진"이라고 안내하듯, dst_entry는 커널에게 "이 패킷은 eth0으로 내보내고, ip_output()을 호출하고, 게이트웨이는 192.168.1.1"이라고 안내합니다. FIB 조회가 "지도 검색"이라면, dst_entry는 검색 결과로 생성된 "경로 안내 카드"입니다.
/* include/net/dst.h — dst_entry 핵심 필드 */
struct dst_entry {
struct net_device *dev; /* 출력 네트워크 디바이스 */
struct dst_ops *ops; /* 프로토콜별 콜백 함수 테이블 */
unsigned long _metrics; /* dst_metrics 포인터 | DST_METRICS_READ_ONLY */
unsigned long expires; /* 만료 시각 (jiffies) */
void *__pad1;
int (*input)(struct sk_buff *skb); /* 수신 처리 함수 */
int (*output)(struct net *net,
struct sock *sk,
struct sk_buff *skb); /* 송신 처리 함수 */
unsigned short flags; /* DST_HOST, DST_NOXFRM 등 */
short error; /* 오류 코드 (EHOSTUNREACH 등) */
short obsolete; /* DST_OBSOLETE_NONE / DEAD / FORCE_CHK */
unsigned long lastuse; /* 마지막 사용 시각 (jiffies) */
atomic_t __refcnt; /* 참조 카운트 */
int __use; /* 사용 횟수 (통계용) */
struct dst_entry *child; /* IPsec/xfrm 번들 체인 */
struct lwtunnel_state *lwtstate; /* 경량 터널 상태 (MPLS, SRv6 등) */
};
fib_info)은 라우팅 정책의 "원본 데이터"이고, dst_entry는 특정 패킷 흐름에 맞게 인스턴스화된 결과물입니다. 같은 10.0.0.0/24 경로라도 소스 주소나 출력 인터페이스가 다르면 별도의 dst_entry가 생성됩니다. 이 분리 덕분에 FIB 변경(경로 추가/삭제)이 기존에 전송 중인 패킷에 즉시 영향을 주지 않습니다.
dst_ops 콜백과 라우트 타입별 동작
RTN_UNICAST, RTN_LOCAL, RTN_BLACKHOLE 등)이 dst_entry의 input/output 콜백을 결정합니다. 즉, "이 패킷을 다른 호스트로 전달할 것인가, 로컬에서 수신할 것인가, 아니면 조용히 버릴 것인가"를 결정하는 핵심 메커니즘입니다.
dst_ops는 프로토콜 패밀리(IPv4/IPv6)별로 하나씩 존재하는 콜백 테이블입니다. C 언어의 함수 포인터로 구현된 일종의 "가상 함수 테이블(vtable)"로, dst_entry의 범용 인터페이스를 통해 프로토콜별 동작을 다형적으로 호출할 수 있게 합니다. C의 함수 포인터 개념이 익숙하지 않다면, "라우트 타입에 따라 자동으로 적절한 처리 함수가 선택된다"는 핵심만 이해하면 충분합니다.
/* include/net/dst_ops.h — dst_ops 핵심 콜백 */
struct dst_ops {
unsigned short family; /* AF_INET 또는 AF_INET6 */
struct dst_entry * (*check)(struct dst_entry *, __u32 cookie);
unsigned int (*default_advmss)(const struct dst_entry *);
unsigned int (*mtu)(const struct dst_entry *);
u32 * (*cow_metrics)(struct dst_entry *, unsigned long);
void (*destroy)(struct dst_entry *);
void (*negative_advice)(struct dst_entry *);
void (*link_failure)(struct sk_buff *);
void (*update_pmtu)(struct dst_entry *,
struct sock *, struct sk_buff *,
u32 mtu, bool confirm);
void (*redirect)(struct dst_entry *,
struct sock *, struct sk_buff *);
struct neighbour * (*neigh_lookup)(const struct dst_entry *,
struct sk_buff *,
const void *daddr);
};
패킷의 라우트 타입(rt_type)에 따라 dst_entry의 input/output 콜백이 다르게 설정됩니다. 이것이 패킷의 운명을 결정합니다:
ip route add unreachable 10.0.0.0/8을 설정하면 해당 대역의 FIB 조회 결과로 rt_type = RTN_UNREACHABLE인 rtable이 생성됩니다. 이 rtable의 dst.output에는 ip_error()가 설정되며, 패킷 송신 시 ip_error()가 ICMP Destination Unreachable 메시지를 생성하여 송신자에게 반환합니다. RTN_BLACKHOLE은 dst_discard()로 아무 응답 없이 폐기하는 반면, RTN_UNREACHABLE과 RTN_PROHIBIT은 ICMP 에러를 반환한다는 차이가 있습니다.
/* net/ipv4/route.c — rt_dst_alloc()에서 라우트 타입별 콜백 설정 */
struct rtable *rt_dst_alloc(struct net_device *dev,
unsigned int flags, u16 type,
bool nopolicy) {
struct rtable *rt = dst_alloc(&ipv4_dst_ops, dev,
DST_OBSOLETE_FORCE_CHK, flags);
rt->rt_type = type;
if (type == RTN_UNICAST || type == RTN_LOCAL ||
type == RTN_BROADCAST || type == RTN_MULTICAST) {
rt->dst.input = ip_forward; /* 기본, 이후 오버라이드 가능 */
rt->dst.output = ip_output;
} else {
/* RTN_BLACKHOLE, RTN_UNREACHABLE, RTN_PROHIBIT */
rt->dst.input = dst_discard; /* 또는 ip_error */
rt->dst.output = dst_discard; /* 또는 ip_error */
}
return rt;
}
dst_entry 레퍼런스 카운팅과 생명주기
dst_entry의 레퍼런스 카운팅은 도서관 대출증과 같습니다. 책(dst_entry)을 빌리면 대출 카드에 +1을 기록하고, 반납하면 -1합니다. 대출 카드가 0이 되면 책장에서 빼서 폐기합니다. 커널에서는 dst_hold()가 빌리기, dst_release()가 반납, __refcnt == 0일 때 dst_destroy()가 폐기입니다.
/* include/net/dst.h — 핵심 레퍼런스 카운팅 함수 */
static inline void dst_hold(struct dst_entry *dst) {
/* dst가 이미 파괴 예정이 아닌지 확인 */
WARN_ON(atomic_inc_not_zero(&dst->__refcnt) == 0);
}
static inline void dst_release(struct dst_entry *dst) {
if (dst) {
int newrefcnt = atomic_dec_return(&dst->__refcnt);
if (unlikely(newrefcnt < 0))
net_warn_ratelimited("dst_release underflow");
}
}
/* skb에 dst_entry를 연결 (참조를 skb로 이전) */
static inline void skb_dst_set(struct sk_buff *skb,
struct dst_entry *dst) {
skb->_skb_refdst = (unsigned long)dst;
}
/* skb에서 dst_entry 참조 해제 */
static inline void skb_dst_drop(struct sk_buff *skb) {
if (!(skb->_skb_refdst & SKB_DST_NOREF))
dst_release(skb_dst(skb));
skb->_skb_refdst = 0;
}
dst_hold() 후 dst_release()를 빼먹으면 dst_entry가 영원히 해제되지 않아 메모리 누수가 발생합니다. 커널 로그에 "dst_cache_gc_timer: dst cache overflow" 또는 "Neighbour table overflow" 경고가 나타날 수 있습니다. SKB_DST_NOREF 플래그가 설정된 경우는 RCU 읽기 구간에서 참조 카운트 없이 dst_entry를 사용하는 최적화 경로이며, 이 경우 skb_dst_drop()이 dst_release()를 호출하지 않습니다.
dst metrics 시스템 (RTAX_*)
dst_entry의 metrics 배열은 라우팅 경로에 연결된 성능 파라미터입니다. TCP/IP 스택이 MSS, 초기 congestion window, RTT 추정값 등을 이 metrics에서 가져와 전송 성능을 최적화합니다.
| RTAX_* 상수 | 인덱스 | 의미 | 영향 |
|---|---|---|---|
RTAX_MTU | 2 | 경로 MTU | IP 단편화 결정, TCP MSS 계산 |
RTAX_ADVMSS | 8 | Advertised MSS | TCP SYN의 MSS 옵션 값 |
RTAX_RTT | 4 | RTT 추정값 (μs) | TCP RTO 초기값 |
RTAX_RTTVAR | 5 | RTT 분산 | TCP RTO 계산의 분산 항 |
RTAX_CWND | 7 | Congestion window | TCP 혼잡 윈도우 힌트 |
RTAX_INITCWND | 11 | 초기 cwnd | TCP 연결 시작 시 cwnd (기본 10) |
RTAX_INITRWND | 14 | 초기 rwnd | TCP 수신 윈도우 초기값 |
RTAX_HOPLIMIT | 10 | Hop limit | IP TTL 기본값 |
RTAX_FEATURES | 12 | 기능 플래그 | ECN, SACK, TIMESTAMP 등 |
/* include/net/dst.h — metrics 접근 함수 */
static inline u32 dst_metric(const struct dst_entry *dst, int metric) {
/* _metrics 하위 비트가 DST_METRICS_READ_ONLY 플래그 */
return dst_metric_raw(dst, metric);
}
static inline void dst_metric_set(struct dst_entry *dst,
int metric, u32 val) {
u32 *p = dst_metrics_write_ptr(dst);
if (p)
p[metric - 1] = val; /* RTAX_* 인덱스는 1부터 시작 */
}
dst_entry는 처음에 fib_info의 공유 metrics를 가리킵니다(DST_METRICS_READ_ONLY 플래그). PMTU 변경 등으로 metrics를 수정해야 할 때, dst_ops->cow_metrics()가 호출되어 metrics 배열의 개인 복사본을 만듭니다. 이는 같은 FIB 경로를 사용하는 다른 dst_entry에 영향을 주지 않기 위한 것입니다. COW 실패(메모리 부족) 시 metrics 수정이 무시되며, 이는 PMTU가 반영되지 않는 드문 원인이 될 수 있습니다.
IPv6 rt6_info 확장
IPv4가 struct rtable로 dst_entry를 확장하듯, IPv6는 struct rt6_info로 확장합니다. 구조는 유사하지만 IPv6의 주소 체계, 소스 주소 라우팅, FIB6 구조 차이로 인해 필드가 다릅니다.
| 항목 | IPv4 (struct rtable) | IPv6 (struct rt6_info) |
|---|---|---|
| 베이스 구조체 | dst_entry dst | dst_entry dst |
| 게이트웨이 | rt_gw4 (__be32) | rt6i_gateway (struct in6_addr) |
| 목적지 프리픽스 | FIB에서 참조 (fib_result) | rt6i_dst (prefix + plen) |
| 소스 프리픽스 | 없음 | rt6i_src (소스 라우팅용) |
| FIB 원본 참조 | fib_info (간접) | from → fib6_info (직접 RCU 포인터) |
| 입력 장치 | 없음 (dst.dev로 충분) | rt6i_idev (inet6_dev) |
| 타입 플래그 | rt_type (RTN_*) | rt6i_flags (RTF_GATEWAY, RTF_CACHE 등) |
| PMTU 처리 | rt_pmtu 필드 | fib6_metrics → RTAX_MTU |
| 예외 캐시 | fib_nh_exception | rt6_exception |
| nexthop | fib_nh_common | fib6_nh (fib6_info 내장) |
| dst_ops | ipv4_dst_ops | ip6_dst_ops |
/* include/net/ip6_fib.h — rt6_info 핵심 구조 */
struct rt6_info {
struct dst_entry dst; /* 베이스 구조체 */
struct rt6key rt6i_dst; /* 목적지 prefix */
struct rt6key rt6i_src; /* 소스 prefix (소스 라우팅) */
struct in6_addr rt6i_gateway; /* 게이트웨이 IPv6 주소 */
struct inet6_dev *rt6i_idev; /* 입력 장치 정보 */
u32 rt6i_flags; /* RTF_GATEWAY, RTF_CACHE 등 */
/* FIB6 원본 참조 — RCU 보호 */
struct fib6_info __rcu *from; /* FIB6 테이블의 원본 경로 */
u16 rt6i_nfheader_len; /* netfilter 헤더 */
bool should_flush; /* FIB6 변경 시 flush 필요 */
};
fib_nh_exception은 nexthop별로 해시 테이블에 저장되며, IPv6의 rt6_exception도 유사하게 rt6_ex_bucket 해시에 저장됩니다. 주요 차이점은 IPv6에서는 fib6_info가 rt6_info와 분리되어 있어, 예외 항목이 fib6_nh에 직접 연결된다는 점입니다. 두 경우 모두 예외 항목에는 만료 시간이 있어 일정 시간 후 자동으로 제거됩니다.
FIB 이벤트 통지 체계
FIB 변경(경로 추가/삭제/교체)은 단순히 테이블만 바꾸는 것이 아니라, 다양한 구독자에게 통지됩니다. 이 통지 메커니즘은 switchdev HW offload, BPF 프로그램, 라우팅 모니터링에 핵심적입니다.
/* net/core/fib_notifier.c — FIB 통지 등록/해제 */
/* 드라이버가 FIB 이벤트 구독 등록 */
struct notifier_block my_fib_nb = {
.notifier_call = my_fib_event_handler,
};
register_fib_notifier(net, &my_fib_nb, my_fib_dump, NULL);
/* 이벤트 핸들러 예시 (switchdev 드라이버) */
static int my_fib_event_handler(struct notifier_block *nb,
unsigned long event, void *ptr) {
struct fib_notifier_info *info = ptr;
switch (event) {
case FIB_EVENT_ENTRY_ADD:
case FIB_EVENT_ENTRY_REPLACE: {
struct fib_entry_notifier_info *feni =
container_of(info, struct fib_entry_notifier_info, info);
/* feni->dst (접두사), feni->dst_len (길이) */
/* feni->fi (fib_info), feni->tos, feni->type */
/* → HW TCAM에 기록 */
hw_fib_add(feni->dst, feni->dst_len, feni->fi);
break;
}
case FIB_EVENT_ENTRY_DEL:
/* HW TCAM에서 제거 */
break;
}
return NOTIFY_DONE;
}
/* BPF에서 FIB 조회: XDP/TC 프로그램 내부 */
struct bpf_fib_lookup params = {
.family = AF_INET,
.ifindex = ctx->ingress_ifindex,
};
params.ipv4_dst = iph->daddr;
params.ipv4_src = iph->saddr;
int rc = bpf_fib_lookup(ctx, ¶ms, sizeof(params), 0);
if (rc == BPF_FIB_LKUP_RET_SUCCESS) {
/* params.dmac → 목적지 MAC (neighbour 해석 완료) */
/* params.smac → 소스 MAC */
/* params.ifindex → 출력 인터페이스 */
/* → XDP_REDIRECT로 직접 전달 (커널 라우팅 스택 우회) */
}
Nexthop 객체 (커널 5.3+)
앞에서 FIB가 경로를 찾고, dst_entry가 생성되고, 변경이 통지되는 과정까지 살펴봤습니다. 전통적인 FIB에서는 nexthop 정보가 각 route 안에 내장되어 있었는데, 커널 5.3부터 이를 독립 객체로 분리했습니다. 이 설계 변경이 왜 필요했고 어떤 이점이 있는지 봅니다.
전통적인 FIB에서는 각 route가 자체 nexthop 배열(fib_nh[])을 가졌습니다. 커널 5.3부터 도입된 nexthop 객체(struct nexthop)는 route와 nexthop을 완전히 분리하여, 여러 route가 동일한 nexthop 그룹을 참조하고 nexthop 변경 시 모든 참조 route가 즉시 업데이트되도록 합니다.
# Nexthop 객체 관리 (iproute2 5.3+)
# 단일 nexthop 생성
ip nexthop add id 10 via 192.168.1.1 dev eth0
ip nexthop add id 20 via 192.168.2.1 dev eth1
# nexthop 그룹 생성 (ECMP)
ip nexthop add id 100 group 10/20
# 가중치 지정 그룹
ip nexthop add id 200 group 10,3/20,1 # 10번에 75%, 20번에 25%
# resilient nexthop 그룹 (5.12+)
ip nexthop add id 300 group 10/20 type resilient buckets 128 \
idle_timer 120 unbalanced_timer 0
# route에서 nexthop 객체 참조
ip route add 10.0.0.0/24 nhid 100
ip route add 10.1.0.0/16 nhid 100 # 같은 nhid 공유
# nexthop 변경 → 참조하는 모든 route에 즉시 반영
ip nexthop replace id 10 via 192.168.1.2 dev eth0
# nexthop 상태 확인
ip nexthop show
ip nexthop show id 100
ip nexthop bucket show nhid 300 # resilient 버킷 상태
# nexthop 그룹 통계
ip -s nexthop show id 100
FIB 메모리 사용량과 확장성
대규모 라우팅 테이블(BGP full table 등)을 운용할 때 FIB의 메모리 사용량을 이해하는 것이 중요합니다. LC-trie의 메모리 효율은 접두사 분포에 크게 의존합니다.
| 구성 요소 | 구조체 크기 (64비트) | 100K prefix 기준 예상 수 | 총 메모리 |
|---|---|---|---|
key_vector (leaf) | ~48 바이트 | ~100K | ~4.8 MB |
key_vector (internal) | 40 + 8×2^bits 바이트 | ~30K~50K | ~2~5 MB |
fib_alias | ~56 바이트 | ~100K~120K | ~5.6~6.7 MB |
fib_info | ~96 + fib_nh 배열 | 수백 (공유됨) | ~0.1 MB |
fib_nh | ~128 바이트 | 수백~수천 | ~0.1 MB |
| 합계 | ~15~20 MB |
# FIB 메모리 사용량 측정 방법
# 1. fib_triestat — trie 크기 직접 확인
cat /proc/net/fib_triestat
# Basic info: size of leaf: 48, size of tnode: 40
# Main:
# Aver depth: 3.12
# Max depth: 8
# Leaves: 847692 ← 약 85만 prefix
# Prefixes: 952311 ← alias 포함 총 수
# Internal nodes: 213847
# 1: 85241 2: 98212 3: 20104 4: 8290 5: 1500 6: 400 7: 100
# Pointers: 1423890
# Null ptrs: 547201
# Total size: 42 MB ← trie만의 크기
# 2. slab 캐시 확인
slabtop -o | grep -E 'fib|ip_dst|ip_fib'
# fib6_nodes 12800 12800 64 64 1
# ip_fib_alias 98400 98400 56 73 1
# ip_fib_trie 51200 51200 48 85 1
# 3. 라우팅 테이블 엔트리 수
ip route show table all | wc -l
ip -6 route show table all | wc -l
# 4. 메모리 증가 모니터링
watch -n1 'cat /proc/net/fib_triestat | head -20'
- BGP full table (~90만 prefix, 2024년 기준): IPv4 FIB에 약 50~80 MB, IPv6 포함 시 추가 ~100 MB 소요
- 메모리 부족 시:
fib_table_insert()가-ENOMEM을 반환하고 경로 추가 실패. 라우팅 데몬이 세션을 재설정할 수 있음 - trie 깊이(Max depth): 8 이하가 정상. 15 이상이면 비정상적 접두사 분포(예: /32 host route 폭증) 의심
- Null ptrs 비율: internal node의 빈 슬롯 비율이 높으면 level compression이 과도 → 메모리 낭비
- gc_thresh:
net.ipv4.neigh.default.gc_thresh3은 neighbour 테이블 한계. FIB가 크면 이 값도 함께 늘려야 함
IPv4 FIB vs IPv6 FIB 비교
IPv4와 IPv6는 모두 FIB prefix tree 기반 LPM을 수행하지만, 자료구조와 동작 특성이 크게 다릅니다.
| 비교 항목 | IPv4 FIB (LC-trie) | IPv6 FIB (fib6 prefix tree) |
|---|---|---|
| 소스 파일 | net/ipv4/fib_trie.c | net/ipv6/ip6_fib.c |
| 주소 크기 | 32비트 | 128비트 |
| 트리 구조 | LC-trie (path + level compressed) | Binary radix tree (path compressed only) |
| 노드 분기 | 다중비트 (bits 필드, 2~7비트 동시 검사) | 1비트 (좌/우 자식) |
| 리밸런싱 | inflate()/halve() 자동 | 없음 (정적 구조) |
| route 구조체 | fib_alias + fib_info (분리) | fib6_info (통합) |
| route 공유 | fib_info 해시 기반 공유 | 공유 없음 (각 route가 자체 fib6_info) |
| TOS/DSCP 구분 | fib_alias.fa_dscp로 같은 prefix에 다중 route | TOS 구분 없음 |
| source route | 지원 안 함 (RPDB로 대체) | fib6_src 필드로 source-specific route 지원 |
| 경로 만료 | 없음 (명시적 삭제만) | expires 필드 (RA 기반 자동 만료) |
| ECMP | fib_info.fib_nh[] 배열 | fib6_nsiblings 연결 리스트 |
| nexthop 객체 | 지원 (5.3+) | 지원 (5.3+) |
| proc 인터페이스 | /proc/net/fib_trie, fib_triestat | /proc/net/ipv6_route |
| BGP full table 메모리 | ~50~80 MB (90만 prefix) | ~100~150 MB (20만 prefix) |
| 최적화 포인트 | Null ptr 비율, trie depth, slab 사용 | gc_thresh, 만료 경로 정리 빈도 |
- 128비트 주소에서 level compression의 이득이 32비트보다 상대적으로 적음 (접두사 분포가 희소)
- source-specific route(
from필드)를 지원하려면 2차원 조회가 필요하여 LC-trie의 단순 key 매칭에 맞지 않음 - IPv6 라우팅 테이블은 IPv4보다 규모가 작은 경우가 많아(2024년 기준 ~20만 prefix) binary radix tree로도 충분한 성능
- 역사적으로 IPv6 FIB는 FreeBSD에서 가져온 radix tree 구현 기반이며, 안정성이 검증되어 교체 동기가 약함
FIB HW Offload (switchdev)
switchdev 프레임워크를 지원하는 네트워크 ASIC(Mellanox Spectrum, Marvell Prestera 등)은 FIB 변경을 하드웨어 TCAM에 동기화하여 와이어 스피드 라우팅을 구현합니다.
# HW offload 상태 확인
ip route show
# 10.0.0.0/24 via 192.168.1.1 dev swp1 offload ← HW에서 포워딩
# 10.0.1.0/24 via 192.168.2.1 dev swp2 trap ← SW fallback
# 10.0.2.0/24 via 192.168.3.1 dev swp3 ← offload 미지원
# offload/trap 플래그 의미:
# offload — HW TCAM에 성공적으로 기록됨. 패킷이 ASIC에서 직접 포워딩
# trap — HW에 기록됐으나 CPU로 트랩됨 (SW 처리 필요)
# (없음) — HW offload 미시도 또는 미지원 디바이스
# devlink를 통한 ASIC 리소스 사용량 확인
devlink resource show pci/0000:03:00.0
# name: IPv4 FIB size: 40000 occ: 23456 ← TCAM 슬롯 사용률
# name: IPv6 FIB size: 20000 occ: 5678
# HW TCAM 가득 차면:
# - 새 경로 offload 실패 → SW fallback (성능 저하)
# - 커널 로그: "Failed to offload FIB entry"
# - 해결: TCAM 크기 확인, 불필요 prefix 정리, 요약 경로 사용
| offload 시나리오 | 패킷 경로 | 성능 | CPU 부하 |
|---|---|---|---|
| 완전 offload | NIC → ASIC TCAM LPM → 출력 포트 | 와이어 스피드 (100Gbps+) | 거의 0 |
| trap (SW fallback) | NIC → ASIC → CPU → 커널 FIB → ASIC 출력 | 커널 라우팅 성능 | 높음 |
| offload 없음 | NIC → CPU → 커널 FIB → NIC 출력 | 커널 라우팅 성능 | 높음 |
| 부분 offload (ECMP) | ASIC에서 hash → 일부 nexthop만 offload | offloaded path만 와이어 스피드 | fallback path만 CPU |
flowi 구조체 — 조회 입력 키 상세
페이지 앞부분의 개요와 파이프라인에서 flowi를 "라우팅 조회의 검색어"로 소개했습니다. 이제 그 검색어의 각 필드가 무엇이고, 어떤 필드가 실제 조회 결과에 영향을 미치는지 상세히 봅니다. 이 이해가 있어야 Policy Routing 문제를 디버깅할 수 있습니다.
FIB 조회의 출발점은 flowi4/flowi6 구조체입니다. 커널은 패킷의 헤더나 소켓 상태에서 이 구조체를 채워 RPDB와 FIB에 전달합니다. 어떤 필드가 실제 조회에 영향을 미치는지 이해해야 policy routing 문제를 디버깅할 수 있습니다.
| flowi4 필드 | 설정 시점 | RPDB 사용 | FIB LPM 사용 | ECMP 해시 | 운영 확인 |
|---|---|---|---|---|---|
daddr | 패킷 목적지 / 소켓 connect | to selector | LPM 키 (핵심) | 해시 입력 | ip route get <dst> |
saddr | 소켓 bind 또는 커널 선택 | from selector | — | 해시 입력 | ip route get ... from <src> |
flowi4_oif | 소켓 SO_BINDTODEVICE / route oif | oif selector | nexthop dev 필터 | — | ip route get ... oif <dev> |
flowi4_iif | 수신 패킷의 dev->ifindex | iif selector | — | — | ip route get ... iif <dev> |
flowi4_mark | Netfilter MARK, SO_MARK | fwmark selector | — | — | ip route get ... mark <val> |
flowi4_tos | IP 헤더 TOS / 소켓 IP_TOS | tos selector | fib_alias TOS 매칭 | — | ip route get ... tos <val> |
flowi4_scope | 커널 설정 (UNIVERSE/LINK/HOST) | — | scope 필터링 | — | 간접 (route scope와 비교) |
flowi4_proto | L4 프로토콜 번호 | ipproto selector | — | 해시 입력 (policy별) | ip rule ... ipproto tcp |
fl4_sport/dport | L4 포트 / 소켓 | sport/dport selector | — | 해시 입력 (L4 policy) | ip rule ... sport 80 |
flowi4_uid | 소켓 소유자 UID | uidrange selector | — | — | ip rule ... uidrange |
/* flowi4 채우기 — 송신 경로 예시 */
/* tcp_v4_connect() → ip_route_connect() 내부 */
struct flowi4 fl4;
flowi4_init_output(&fl4,
sk->sk_bound_dev_if, /* oif: SO_BINDTODEVICE */
sk->sk_mark, /* mark: SO_MARK 또는 Netfilter */
RT_TOS(inet->tos), /* tos: IP_TOS 소켓 옵션 */
RT_SCOPE_UNIVERSE, /* scope: 보통 UNIVERSE */
sk->sk_protocol, /* proto: IPPROTO_TCP(6) */
0, /* flags */
daddr, /* 목적지 주소 */
saddr, /* 소스 주소 (bind 또는 자동) */
dport, /* 목적지 포트 */
sport); /* 소스 포트 */
fl4.flowi4_uid = sock_net_uid(net, sk); /* UID */
struct rtable *rt = ip_route_output_flow(net, &fl4, sk);
/* 수신 경로: ip_rcv() → ip_rcv_finish() */
/* iph에서 daddr/saddr/tos를 추출하고,
* skb->mark에서 mark를, skb->dev에서 iif를 채움 */
struct flowi4 fl4;
fl4.daddr = iph->daddr;
fl4.saddr = iph->saddr;
fl4.flowi4_tos = RT_TOS(iph->tos);
fl4.flowi4_iif = skb->dev->ifindex;
fl4.flowi4_mark = skb->mark;
fl4.flowi4_scope = RT_SCOPE_UNIVERSE;
/* → fib_lookup(net, &fl4, &res, 0) */
ip route get에 from, iif, mark, tos 옵션을 조합하면 커널이 실제로 채우는 flowi4를 시뮬레이션할 수 있습니다. policy routing 문제의 80%는 "어떤 selector가 예상과 다른 값으로 채워지는가"로 귀결됩니다.
FIB Nexthop Exception Cache 상세
커널 3.6에서 전역 route cache가 제거된 후, nexthop exception이 그 역할의 일부를 대신합니다. ICMP "Fragmentation Needed" (PMTU) 또는 ICMP Redirect를 수신하면, 해당 목적지에 대한 예외를 nexthop별 해시 테이블에 기록합니다. 일반 조회는 매번 LC-trie를 직접 타지만, 예외가 있는 목적지는 캐시된 정보를 우선 사용합니다.
/* net/ipv4/route.c — PMTU exception 생성 */
static void __ip_rt_update_pmtu(struct rtable *rt,
struct flowi4 *fl4,
u32 mtu) {
struct fib_nh_common *nhc;
struct fib_nh_exception *fnhe;
nhc = rt_fib_nh_common(rt);
/* nexthop의 exception 해시 테이블에서 이 목적지 검색 또는 생성 */
fnhe = find_or_create_fnhe(nhc, fl4->daddr);
/* PMTU 값 갱신 */
fnhe->fnhe_pmtu = mtu;
fnhe->fnhe_mtu_locked = false;
fnhe->fnhe_stamp = jiffies;
fnhe->fnhe_expires = jiffies + ip_rt_mtu_expires;
/* ip_rt_mtu_expires: 기본 600초 (10분)
* 만료 후 커널은 원래 MTU로 돌아가 PMTU 재탐지 시도 */
}
/* Redirect exception 생성 */
void ip_rt_redirect(__be32 old_gw, __be32 new_gw, ...) {
/* 검증: new_gw가 on-link인지, 합리적인 redirect인지 확인 */
fnhe = find_or_create_fnhe(nhc, daddr);
fnhe->fnhe_gw = new_gw;
fnhe->fnhe_expires = jiffies + ip_rt_redirect_silence;
}
/* mkroute 시 exception 적용 */
static struct rtable *__mkroute_output(...) {
struct fib_nh_exception *fnhe;
fnhe = find_exception(nhc, fl4->daddr);
if (fnhe) {
/* PMTU가 있으면 dst_entry에 반영 */
if (fnhe->fnhe_pmtu)
dst_metric_set(&rth->dst, RTAX_MTU, fnhe->fnhe_pmtu);
/* redirect가 있으면 게이트웨이 교체 */
if (fnhe->fnhe_gw)
rth->rt_gw4 = fnhe->fnhe_gw;
/* 캐시된 rtable이 있으면 재사용 */
struct rtable *cached = rcu_dereference(fnhe->fnhe_rth_output);
if (cached && rt_cache_valid(cached))
return cached;
}
/* ... 새 rtable 할당 ... */
}
# Exception cache 운영 확인
# 1. PMTU 예외 확인
ip route get 203.0.113.50
# 203.0.113.50 via 192.168.1.1 dev eth0 src 192.168.1.100
# cache expires 542sec mtu 1400
# ^^^^^^^^^^^^^^^^^^^^^^^^ PMTU exception 활성
# 2. Redirect 예외 확인
ip route get 10.0.0.50
# 10.0.0.50 via 192.168.1.2 dev eth0 src 192.168.1.100
# cache redirect
# ← 원래 gw(192.168.1.1)에서 redirect됨
# 3. Exception 강제 삭제 (route flush)
ip route flush cache
# 모든 nexthop exception과 cached rtable 제거
# PMTU/redirect 예외도 함께 사라짐 → 재탐지 필요
# 4. PMTU 관련 sysctl
sysctl net.ipv4.route.mtu_expires=600 # PMTU 예외 만료 시간(초)
sysctl net.ipv4.route.min_pmtu=552 # 최소 PMTU (이하로 설정 안됨)
sysctl net.ipv4.ip_forward_use_pmtu=0 # 포워딩 시 PMTU 적용 여부
# 5. Redirect 관련 sysctl
sysctl net.ipv4.conf.all.accept_redirects=0 # redirect ICMP 수신 여부
sysctl net.ipv4.conf.all.send_redirects=1 # redirect ICMP 발신 여부
# 6. Exception 통계 (perf/bpftrace)
bpftrace -e 'kprobe:find_or_create_fnhe {
@create = count();
}'
- PMTU black hole: ICMP가 방화벽에서 차단되면 PMTU discovery가 실패하여 큰 패킷이 무한히 드롭됩니다.
ip route flush cache후에도 재발하면 MSS clamping(iptables -t mangle -A FORWARD -p tcp --tcp-flags SYN,RST SYN -j TCPMSS --clamp-mss-to-pmtu)을 고려하세요 - Redirect 남용: 공격자가 위조 ICMP Redirect를 보내 트래픽을 탈취할 수 있으므로, 라우터/서버에서는
accept_redirects=0이 일반적입니다 - Exception 폭발: DDoS 환경에서 수백만 개의 고유 목적지가 각각 exception을 생성하면 메모리가 급격히 증가합니다. 커널은
fnhe_genid로 세대 관리하여 오래된 exception을 정리합니다 - IPv6 차이: IPv6는
rt6_exception을 사용하며, ICMPv6 "Packet Too Big"으로 PMTU를 처리합니다. 구조는 유사하지만 별도 코드 경로(rt6_do_update_pmtu())를 탑니다
멀티캐스트 FIB (MFC)
여기서부터는 유니캐스트와 다른 별도의 FIB 변종들을 다룹니다. 유니캐스트 FIB의 핵심을 이해했다면, 이 섹션들은 같은 원리가 다른 도메인에 어떻게 적용되는지 보여주는 확장입니다.
유니캐스트 FIB와 별도로 Linux는 멀티캐스트 포워딩을 위한 MFC(Multicast Forwarding Cache)를 운용합니다. PIM(Protocol Independent Multicast) 데몬이 설치한 MFC 엔트리에 따라 멀티캐스트 패킷이 여러 출력 인터페이스로 복제됩니다.
# 멀티캐스트 라우팅 활성화
sysctl net.ipv4.conf.all.mc_forwarding=1
# 또는 PIM 데몬(pimd, FRR)이 자동 설정
# MFC(Multicast Forwarding Cache) 확인
ip mroute show
# (10.0.0.1, 239.1.1.1) Iif: eth0 Oifs: eth1 eth2 tun0
# (10.0.0.2, 239.2.2.2) Iif: eth0 Oifs: eth1
# /proc 인터페이스 — 상세 통계 포함
cat /proc/net/ip_mr_cache
# Group Origin Iif Pkts Bytes Wrong Oifs
# EF010101 0A000001 0 12345 18518 0 1:1 2:1
# VIF(Virtual Interface) 매핑
cat /proc/net/ip_mr_vif
# Interface Bytes In Pkts In Bytes Out Pkts Out Flags
# eth0 1234567 12345 0 0 0
# eth1 0 0 987654 9876 0
# IGMP 그룹 멤버십
cat /proc/net/igmp
ip maddr show
# 멀티캐스트 통계
cat /proc/net/snmp | grep -i 'Ip.*Mcast'
| 비교 | 유니캐스트 FIB | 멀티캐스트 FIB (MFC) |
|---|---|---|
| 키 | 목적지 주소 (LPM) | (소스, 그룹) 쌍 — (S,G) 또는 (*,G) |
| 조회 방식 | Longest Prefix Match | Exact match (해시 테이블) |
| 출력 | 단일 nexthop (또는 ECMP) | 여러 출력 인터페이스로 복제 |
| 설치 주체 | ip route, BGP/OSPF | PIM 데몬 (pimd, FRR pimd) |
| 소스 검증 | rp_filter (선택) | RPF 검사 (필수) |
| cache miss | FIB에 없으면 조회 실패 | PIM 데몬에 upcall → 동적 생성 |
| 자료구조 | LC-trie / fib6_node | 해시 테이블 (mfc_cache_array) |
| 소스 파일 | net/ipv4/fib_trie.c | net/ipv4/ipmr.c |
MPLS FIB
Linux 커널 4.3+부터 MPLS(Multi-Protocol Label Switching) 포워딩을 지원합니다. MPLS FIB는 IP FIB와 완전히 별도의 테이블로, 라벨 번호를 키로 사용하여 다음 동작(swap, pop, push)을 결정합니다.
/* net/mpls/af_mpls.c — MPLS FIB 구조 */
/* MPLS 라우팅 테이블: 라벨 → 동작(nexthop) 매핑 */
struct mpls_route {
struct net_device *rt_dev; /* 출력 디바이스 */
u8 rt_protocol; /* RTPROT_STATIC 등 */
u8 rt_payload_type; /* MPT_IPV4, MPT_IPV6, MPT_UNSPEC */
u8 rt_max_alen; /* 최대 nexthop 주소 길이 */
u8 rt_nhn; /* nexthop 수 */
u8 rt_nhn_alive; /* 활성 nexthop 수 */
u8 rt_nh_size; /* nexthop 구조체 크기 */
u8 rt_via_offset;
u8 rt_via_alen;
/* nexthop: via 주소, 출력 라벨(들), 동작 */
};
/* platform_label[]: 라벨 번호 → mpls_route 배열
* 라벨은 0~1048575 (20비트), 0~15는 예약
* 배열 인덱스로 직접 접근 → O(1) 조회 */
/* MPLS 포워딩 경로:
* 1. 패킷 수신 → MPLS ethertype (0x8847) 확인
* 2. mpls_forward() 호출
* 3. top label → platform_label[label]로 mpls_route 조회
* 4. 동작 수행: swap(라벨 교체), pop(라벨 제거), push(라벨 추가)
* 5. 출력 디바이스로 전달
*/
# MPLS 활성화
modprobe mpls_router
modprobe mpls_iptunnel
sysctl net.mpls.platform_labels=1048575 # 최대 라벨 수 설정
sysctl net.mpls.conf.eth0.input=1 # 인터페이스별 MPLS 수신 허용
# MPLS 라우팅 테이블 설정
ip -f mpls route add 100 via inet 192.168.1.2 dev eth0
# 라벨 100 수신 → pop 후 192.168.1.2로 IPv4 포워딩
ip -f mpls route add 200 as 300 via inet 10.0.0.2 dev eth1
# 라벨 200 수신 → 라벨 300으로 swap 후 10.0.0.2로 전달
# IP → MPLS encap (ip route에서 MPLS 라벨 push)
ip route add 172.16.0.0/24 encap mpls 100/200 via 192.168.1.2 dev eth0
# 172.16.0.0/24 목적지 → MPLS 라벨 스택 [100, 200] push
# MPLS 라우팅 테이블 조회
ip -f mpls route show
# 100 via inet 192.168.1.2 dev eth0 proto static
# 200 as to 300 via inet 10.0.0.2 dev eth1 proto static
# MPLS 통계
cat /proc/net/mpls_stats
# Interface InPkts InBytes InErrors OutPkts OutBytes
encap mpls로 라벨을 push합니다. MPLS 네트워크 내부에서는 MPLS FIB(platform_label 배열)로 라벨 swap/pop을 수행합니다. 마지막 라벨이 pop되면 내부 IP 패킷이 노출되어 다시 IP FIB로 라우팅됩니다. 이 3단계(push → swap × N → pop)가 MPLS의 기본 동작입니다.
FIB와 네트워크 네임스페이스
Linux의 FIB는 네트워크 네임스페이스별로 완전히 격리됩니다. 각 네임스페이스는 독립된 FIB 테이블 세트, RPDB 규칙, nexthop 객체, neighbour 테이블을 가집니다. 이 격리가 컨테이너 네트워킹과 VRF의 기반입니다.
/* include/net/net_namespace.h — per-netns FIB 구조 */
struct net {
/* ... */
struct netns_ipv4 ipv4;
struct netns_ipv6 ipv6;
/* ... */
};
struct netns_ipv4 {
struct fib_rules_ops *rules_ops; /* per-ns RPDB */
struct hlist_head *fib_table_hash; /* per-ns FIB 테이블 해시 */
struct fib_table *fib_main; /* table 254 바로가기 */
struct fib_table *fib_default; /* table 253 바로가기 */
struct fib_table *fib_local; /* table 255 바로가기 */
unsigned int fib_rules_require_fldissect;
bool fib_has_custom_rules; /* policy routing 활성 여부 */
struct hlist_head fib_info_hash[...]; /* per-ns fib_info 해시 */
/* sysctl: ip_forward, rp_filter, fib_multipath_* 등 per-ns */
};
/* 네임스페이스 격리의 의미:
* - 같은 IP 주소(예: 10.0.0.1)가 서로 다른 네임스페이스에서 충돌 없이 사용 가능
* - 한 네임스페이스의 route 변경이 다른 네임스페이스에 영향 없음
* - 각 네임스페이스의 FIB notifier chain도 독립 (per-ns switchdev)
* - sysctl (ip_forward, rp_filter 등)도 per-ns 독립 설정
*/
# 네임스페이스별 FIB 독립 확인
# 1. 새 네임스페이스 생성
ip netns add test_ns
# 2. 각 네임스페이스의 FIB는 완전히 독립
ip netns exec test_ns ip route show
# (비어 있음 — 새로 만든 네임스페이스)
# 3. veth pair로 네임스페이스 간 연결
ip link add veth-host type veth peer name veth-ns
ip link set veth-ns netns test_ns
ip addr add 192.168.100.1/24 dev veth-host
ip link set veth-host up
ip netns exec test_ns ip addr add 192.168.100.2/24 dev veth-ns
ip netns exec test_ns ip link set veth-ns up
# 4. 네임스페이스 안에서 FIB 확인
ip netns exec test_ns ip route show
# 192.168.100.0/24 dev veth-ns proto kernel scope link src 192.168.100.2
ip netns exec test_ns ip route add default via 192.168.100.1
ip netns exec test_ns ip route show
# default via 192.168.100.1 dev veth-ns
# 192.168.100.0/24 dev veth-ns proto kernel scope link src 192.168.100.2
# 5. 호스트 FIB에는 영향 없음
ip route show | grep 'default'
# default via 10.0.0.1 dev eth0 (호스트 기존 route 그대로)
# 6. 네임스페이스별 fib_triestat
ip netns exec test_ns cat /proc/net/fib_triestat
# Main: Leaves: 3 Prefixes: 4 (호스트와 완전히 독립)
# 7. 컨테이너 실행 시 Docker/K8s가 자동으로 수행하는 과정과 동일
FIB와 동적 라우팅 데몬 연동
BGP, OSPF, IS-IS 등 동적 라우팅 프로토콜은 사용자 공간 데몬(FRR, BIRD, GoBGP 등)이 Netlink를 통해 커널 FIB에 경로를 설치합니다. 데몬의 RIB(Routing Information Base)와 커널의 FIB 사이의 동기화 모델을 이해하는 것이 운영에서 중요합니다.
# FRR(Free Range Routing) 설정 예시
# vtysh에서 BGP 경로 RIB 확인
vtysh -c "show bgp ipv4 unicast"
# 여러 peer에서 학습한 경로와 best path 표시
# * = valid, > = best, i = internal
# *> 10.0.0.0/24 192.168.1.2 0 100 200 i
# * 10.0.0.0/24 192.168.2.2 0 200 200 i (backup)
# Zebra RIB (통합 RIB) — 여기서 커널 FIB로 push 결정
vtysh -c "show ip route"
# B>* 10.0.0.0/24 [20/0] via 192.168.1.2, eth0, weight 1, 00:05:12
# O>* 10.1.0.0/16 [110/20] via 192.168.1.3, eth1, weight 1, 00:10:30
# 커널 FIB에 설치된 경로 확인
ip route show proto bgp
# 10.0.0.0/24 via 192.168.1.2 dev eth0 proto bgp metric 20
ip route show proto ospf
# 10.1.0.0/16 via 192.168.1.3 dev eth1 proto ospf metric 20
# proto 값으로 경로 출처 구분:
# proto kernel — 커널 자동 생성 (인터페이스 주소)
# proto boot — 부팅 시 설정
# proto static — ip route add ... (관리자)
# proto bgp — BGP 데몬 (FRR/BIRD)
# proto ospf — OSPF 데몬
# proto isis — IS-IS 데몬
# proto zebra — FRR Zebra (레거시)
# FRR과 커널 FIB 동기화 문제 디버깅
vtysh -c "show ip route summary"
# Route Source Routes FIB (installed count)
# bgp 85000 84998 ← 2개 FIB 설치 실패
# ospf 150 150
# connected 10 10
# static 5 5
# Netlink 에러 모니터링
ip monitor route 2>&1 | head -20
# 경로 추가/삭제가 실시간으로 표시됨
| 항목 | RIB (데몬) | FIB (커널) |
|---|---|---|
| 저장 위치 | 사용자 공간 프로세스 메모리 | 커널 메모리 (LC-trie) |
| 경로 수 | 모든 학습 경로 (best + backup + withdrawn) | best 경로만 |
| 속성 | AS-path, community, MED, local-pref, ... | gateway, dev, metric, proto만 |
| 조회 방식 | 프로토콜별 best path selection | LPM (Longest Prefix Match) |
| 갱신 주체 | 프로토콜 메시지 (BGP UPDATE, OSPF LSA) | Netlink RTM_NEWROUTE |
| 동기화 | Zebra가 best path → FIB push | 수동적 (데몬이 설치한 대로) |
| graceful restart | RIB에서 stale 경로 관리 | FIB는 삭제 명령이 올 때까지 유지 |
| 메모리 (BGP full) | ~2~4 GB (FRR) | ~50~80 MB (커널) |
- ENOMEM: 커널 메모리 부족 →
fib_table_insert()실패.dmesg에 OOM 관련 메시지 - EEXIST: 동일 경로가 이미 존재 (다른 proto).
ip route replace로 해결 - ENETUNREACH: nexthop이 도달 불가능한 인터페이스. 링크 다운 상태 확인
- proto 충돌: BGP 경로가 static 경로와 충돌 시 커널은 먼저 설치된 경로 유지. FRR의
administrative distance는 데몬 내부 개념으로 커널에는 전달 안 됨 - 진단:
vtysh -c "show ip route summary"에서 Routes ≠ FIB 수치 비교
FIB 진화 역사
Linux FIB는 20년 넘게 꾸준히 발전해왔습니다. 각 커널 버전의 핵심 변화를 이해하면 레거시 시스템과 현대 시스템의 차이를 파악하는 데 도움이 됩니다.
| 커널 버전 | 연도 | 핵심 변화 | 영향 |
|---|---|---|---|
| 2.6.13 | 2005 | LC-trie 도입 (Robert Olsson) | 기존 해시 테이블 대비 메모리 효율 3~10배 향상, LPM 성능 개선 |
| 3.6 | 2012 | 전역 route cache 제거 | DoS 공격 표면 제거, GC 오버헤드 소멸. FIB를 매번 직접 조회 |
| 3.6 | 2012 | nexthop exception cache 도입 | PMTU/redirect만 per-nexthop 해시에 캐싱 — route cache의 최소 대체 |
| 4.3 | 2015 | MPLS FIB 지원 | label-based 포워딩 테이블, IP-MPLS encap/decap 통합 |
| 4.8 | 2016 | FIB notifier 프레임워크 | switchdev 드라이버가 FIB 변경을 구독하여 HW 동기화 가능 |
| 4.10 | 2017 | fib_alias에서 TOS 분리 (DSCP) | 같은 prefix에 DSCP별 다른 경로 지원 개선 |
| 4.14 | 2017 | VRF l3mdev 규칙 자동화 | per-VRF RPDB 규칙을 수동 관리할 필요 없이 l3mdev 규칙 하나로 통합 |
| 5.3 | 2019 | nexthop 객체 도입 (ip nexthop) | route와 nexthop 분리, ECMP 그룹 공유, O(1) nexthop 교체 |
| 5.12 | 2021 | resilient nexthop group | nexthop 추가/제거 시 기존 flow 영향 최소화 (consistent hashing) |
| 5.15 | 2021 | BPF FIB lookup 개선 | bpf_fib_lookup()에 더 많은 정보 반환, XDP 고성능 포워딩 지원 |
| 6.1 | 2022 | nexthop group HW stats | nexthop 그룹별 패킷/바이트 통계를 HW에서 수집 가능 |
| 6.6 | 2023 | FIB trie 최적화 (slen 개선) | 대규모 테이블에서 backtrack 성능 향상, 평균 조회 깊이 감소 |
| 6.8 | 2024 | nexthop 객체 per-bucket counter | resilient group의 각 버킷 트래픽 통계, 부하 분산 모니터링 개선 |
실전 FIB 디버깅 시나리오
FIB 관련 라우팅 문제를 체계적으로 진단하는 워크플로우입니다. 각 시나리오는 실제 운영 환경에서 자주 발생하는 패턴입니다.
####################################################
# 시나리오 1: "패킷이 예상과 다른 인터페이스로 나감"
####################################################
# Step 1: 실제 FIB 조회 결과 확인
ip route get 10.0.5.1
# 10.0.5.1 via 192.168.1.1 dev eth0 src 192.168.1.100
# Step 2: 어떤 RPDB 규칙이 매칭됐는지 확인
ip rule show
# 0: from all lookup local
# 100: from 10.0.0.0/8 lookup 100 ← 이 규칙이 개입?
# 32766: from all lookup main
# Step 3: 해당 테이블의 FIB 내용 확인
ip route show table 100
# 10.0.0.0/8 via 192.168.2.1 dev eth1 ← 여기서 잡힘!
# Step 4: fibmatch로 어떤 FIB 엔트리가 일치했는지 직접 확인
ip route get fibmatch 10.0.5.1
# 10.0.0.0/8 table 100 dev eth1 proto static scope link
# Step 5: 의도대로 수정
ip route add 10.0.5.0/24 via 192.168.1.1 table 100
# 더 구체적인 /24 route가 LPM으로 우선 매칭됨
####################################################
# 시나리오 2: "특정 목적지만 연결 안 됨 (PMTU black hole)"
####################################################
# Step 1: 해당 목적지로 route get 확인
ip route get 203.0.113.50
# 203.0.113.50 via 10.0.0.1 dev eth0 src 10.0.0.100
# cache expires 123sec mtu 576 ← MTU가 비정상적으로 작음!
# Step 2: exception cache 확인
ip route show cache
# 203.0.113.50 via 10.0.0.1 dev eth0
# cache expires 123sec mtu 576 ← PMTU exception 확인
# Step 3: exception 삭제 (주의: 모든 exception 삭제)
ip route flush cache
# Step 4: MSS clamping 설정 (근본 해결)
iptables -t mangle -A FORWARD -p tcp --tcp-flags SYN,RST SYN \
-j TCPMSS --clamp-mss-to-pmtu
####################################################
# 시나리오 3: "ECMP 부하가 불균형"
####################################################
# Step 1: ECMP 경로 확인
ip route show 10.0.0.0/24
# 10.0.0.0/24
# nexthop via 192.168.1.1 dev eth0 weight 1
# nexthop via 192.168.2.1 dev eth1 weight 1
# Step 2: ECMP 해시 정책 확인
sysctl net.ipv4.fib_multipath_hash_policy
# 0 = L3 (src+dst IP만), 1 = L4 (src+dst IP+port), 2 = L3+dev, 3 = custom
# Step 3: L4 해시 활성화 (더 균등한 분배)
sysctl -w net.ipv4.fib_multipath_hash_policy=1
# Step 4: resilient 그룹 사용 (nexthop 변경 시 flow 유지)
ip nexthop add id 10 via 192.168.1.1 dev eth0
ip nexthop add id 20 via 192.168.2.1 dev eth1
ip nexthop add id 100 group 10/20 type resilient buckets 128
ip route replace 10.0.0.0/24 nhid 100
# Step 5: 버킷 분포 확인
ip nexthop bucket show nhid 100
####################################################
# 시나리오 4: "BGP 경로가 커널에 설치 안 됨"
####################################################
# Step 1: 데몬 RIB에서 경로 확인
vtysh -c "show bgp ipv4 unicast 10.99.0.0/24"
# best path 있는지, 왜 best인지 확인
# Step 2: Zebra RIB에서 FIB 설치 상태 확인
vtysh -c "show ip route 10.99.0.0/24"
# 없으면: route-map에서 deny되었을 가능성
# Step 3: 커널 FIB에서 직접 확인
ip route show 10.99.0.0/24
ip route get 10.99.1.1
# Step 4: Netlink 에러 확인 (실시간 모니터링)
ip monitor route &
vtysh -c "clear bgp ipv4 unicast * soft in"
# 경로 재학습 → 설치 시도 → Netlink 에러 메시지 관찰
# Step 5: nexthop이 유효한지 확인
ip route get <nexthop-gateway>
ip neigh show <nexthop-gateway>
# nexthop gateway가 ARP 해결 안 되면 경로 설치 실패 가능
####################################################
# 시나리오 5: "FIB 조회 성능이 갑자기 저하"
####################################################
# Step 1: trie 상태 확인
cat /proc/net/fib_triestat
# Max depth가 15 이상이면 비정상적 prefix 분포
# Null ptrs 비율이 80% 이상이면 메모리 낭비
# Step 2: perf로 FIB 핫스팟 확인
perf top -e cycles -g -- sleep 10
# fib_table_lookup(), check_leaf()의 CPU 점유율 확인
# Step 3: /32 host route 폭증 확인
ip route show table all | grep '/32' | wc -l
# 수만 개의 /32 route → trie 깊이 증가 원인
# Step 4: bpftrace로 조회 지연 측정
bpftrace -e 'kprobe:fib_table_lookup { @start[tid] = nsecs; }
kretprobe:fib_table_lookup /@start[tid]/ {
@latency_ns = hist(nsecs - @start[tid]);
delete(@start[tid]);
}'
FIB 성능 특성과 벤치마크
FIB 조회 성능은 라우터/서버의 패킷 처리 능력에 직접적인 영향을 미칩니다. LC-trie의 이론적 복잡도와 실제 측정값을 이해하면 성능 병목을 예측할 수 있습니다.
| 측정 항목 | 일반 서버 (수십 route) | BGP 라우터 (90만 route) | 비고 |
|---|---|---|---|
| fib_table_lookup() 지연 | ~50~100ns | ~200~500ns | 단일 코어, cache warm 상태 |
| trie 평균 깊이 | 2~3 | 4~6 | fib_triestat의 Aver depth |
| trie 최대 깊이 | 3~5 | 6~10 | Max depth (15+ 이면 비정상) |
| 초당 조회 수 (단일 코어) | ~10~20 Mpps | ~2~5 Mpps | RCU lock-free, cache 영향 큼 |
| 멀티코어 확장성 | 거의 선형 | 거의 선형 | RCU read-side lock 사용, 경합 없음 |
| 경로 삽입 속도 | — | ~50K~100K routes/sec | rtnl_lock 직렬화 병목 |
| FIB 메모리 | ~100 KB | ~50~80 MB | fib_triestat의 Total size |
| L1/L2 cache 영향 | 대부분 cache hit | 빈번한 cache miss | 대규모 trie는 L3까지 사용 |
# FIB 성능 측정 도구
# 1. fib_table_lookup 호출 빈도와 지연
bpftrace -e '
kprobe:fib_table_lookup { @start[tid] = nsecs; }
kretprobe:fib_table_lookup /@start[tid]/ {
@lookup_ns = hist(nsecs - @start[tid]);
delete(@start[tid]);
@total = count();
}
interval:s:5 { print(@lookup_ns); print(@total); clear(@lookup_ns); clear(@total); }
'
# 2. perf로 FIB 관련 함수 프로파일링
perf record -g -a -e cycles -- sleep 10
perf report --symbol-filter=fib
# fib_table_lookup, check_leaf, get_child_rcu 비율 확인
# 3. cache miss 분석 (L1/L2/L3)
perf stat -e cache-misses,cache-references,instructions \
-a -- sleep 10
# cache miss 비율이 높으면 trie가 캐시보다 큰 상황
# 4. XDP/BPF FIB lookup 성능 (커널 스택 우회)
# XDP에서 bpf_fib_lookup()은 커널 라우팅 스택 전체를 우회하므로
# 단일 코어에서 ~20~40 Mpps 달성 가능 (64B 패킷 기준)
# 5. ECMP 해시 분포 확인
# nstat으로 인터페이스별 패킷 수 비교
nstat -z -a | grep -i 'Ip.*Forward'
# 또는 각 인터페이스의 RX/TX 카운터 비교
ip -s link show eth0 | grep -A1 TX
ip -s link show eth1 | grep -A1 TX
- ECMP 해시 정책:
fib_multipath_hash_policy=1(L4)이 L3보다 균등 분배. 더 세밀한 제어는hash_policy=3(custom) +fib_multipath_hash_fields - nexthop 상태 반영:
fib_multipath_use_neigh=1로 dead nexthop 자동 우회 - linkdown 무시:
ignore_routes_with_linkdown=1로 다운된 인터페이스 경로 제외 - 불필요 경로 정리: /32 host route가 과도하면 trie 깊이 증가 → 요약(summarization) 적용
- XDP 가속: 고성능 포워딩이 필요하면
bpf_fib_lookup()+XDP_REDIRECT로 커널 스택 우회 - HW offload: switchdev 지원 NIC에서
RTNH_F_OFFLOAD확인, TCAM 용량 모니터링
- FIB는 커널이 라우팅 테이블을 내부적으로 저장하는 방식입니다.
ip route로 보는 것과 같은 정보를 매우 빠르게 조회할 수 있도록 트리 자료구조로 관리합니다. - LC-trie는 IPv4 FIB가 사용하는 비트 단위 트리입니다. 목적지 IP의 비트를 따라 내려가며 가장 구체적인 접두사(Longest Prefix Match)를 찾습니다. path compression과 level compression으로 대부분 3~5단계만에 조회가 완료됩니다.
- prefix와 route의 분리:
fib_alias(경로 카드)와fib_info(nexthop 정보)를 분리하여, 동일한 nexthop을 쓰는 수만 개의 경로가 하나의fib_info를 공유합니다. - 조회는 lock-free: RCU 덕분에 패킷 포워딩(조회)은 lock 없이 모든 코어에서 동시에 수행됩니다. 경로 추가/삭제만
rtnl_lock을 잡습니다. - Nexthop 객체(5.3+)로 route와 nexthop을 완전히 분리하여, nexthop 변경 시 모든 참조 route가 O(1)로 업데이트됩니다.
- HW offload: switchdev 프레임워크로 FIB 변경을 하드웨어 TCAM에 동기화하면 와이어 스피드 포워딩이 가능합니다.
- 디버깅 핵심:
ip route get [dst] from [src] iif [dev] mark [val]로 커널이 실제로 선택하는 경로를 정확히 확인할 수 있습니다.
라우팅 테이블 관리
앞의 섹션들에서 커널 내부의 FIB 자료구조를 깊이 살펴봤습니다. 이제 사용자가 ip route 명령으로 이 테이블을 실제로 어떻게 조회하고 관리하는지 봅니다.
ip route 명령
# 현재 라우팅 테이블 조회
ip route show # main 테이블
ip route show table local # local 테이블
ip route show table all # 모든 테이블
# 경로 추가
ip route add 10.0.0.0/24 via 192.168.1.1 # 게이트웨이 경유
ip route add 10.0.0.0/24 dev eth0 # 직접 연결
ip route add 10.0.0.0/24 via 192.168.1.1 metric 100 # 메트릭 지정
# scope/type 이해
ip route add 10.0.0.0/24 via 192.168.1.1 scope global # 기본: 전역
ip route add 10.0.0.0/24 dev eth0 scope link # 직접 연결 네트워크
# scope: global > site > link > host > nowhere
# 라우팅 타입
ip route add unreachable 10.0.99.0/24 # ICMP unreachable 반환
ip route add blackhole 10.0.99.0/24 # 조용히 드롭
ip route add prohibit 10.0.99.0/24 # ICMP prohibited 반환
ip route add throw 10.0.99.0/24 # 다음 rule로 넘김
# 특정 소스 주소 지정
ip route add 10.0.0.0/24 via 192.168.1.1 src 192.168.1.100
# 경로 삭제/변경
ip route del 10.0.0.0/24
ip route change 10.0.0.0/24 via 192.168.1.2
라우팅 테이블 종류
| 테이블 ID | 이름 | 용도 | 우선순위 |
|---|---|---|---|
| 255 | local | 로컬 주소, 브로드캐스트 주소 (커널 자동 관리) | rule 0 (최우선) |
| 254 | main | 일반 라우팅 (ip route 기본 대상) | rule 32766 |
| 253 | default | 기본 경로 (거의 사용 안 함) | rule 32767 |
| 1~252 | 사용자 정의 | Policy Routing용 커스텀 테이블 | ip rule로 지정 |
기본 RPDB 규칙
ip-rule(8)가 설명하듯이, 커널은 부팅 직후 세 개의 기본 규칙을 자동으로 설치합니다. 이 기본 규칙을 이해하지 못하면 table local과 main의 우선순위를 자주 오해하게 됩니다.
| priority | selector | action | 실제 의미 |
|---|---|---|---|
| 0 | from all | lookup local | 내 호스트의 주소, broadcast, anycast 성격의 제어 엔트리를 가장 먼저 확인 |
| 32766 | from all | lookup main | 관리자가 일반적으로 추가한 라우트를 조회 |
| 32767 | from all | lookup default | 마지막 후처리용 예약 테이블. 보통 비어 있음 |
route type / scope / proto 의미
| 필드 | 대표 값 | 의미 | 운영 포인트 |
|---|---|---|---|
type | unicast | 정상적인 전달 경로 | 직접 연결과 게이트웨이 경유 모두 포함 |
type | local | 이 호스트 자신에게 배달 | 보통 table local에 자동 생성되며, 직접 지우는 작업은 위험 |
type | throw | 현재 테이블 조회를 "실패한 것처럼" 종료 | RPDB와 함께 쓸 때 다음 규칙으로 넘어가게 만드는 제어 route |
type | unreachable | 드롭 + ICMP unreachable | 로컬 송신자는 EHOSTUNREACH를 받음 |
type | prohibit | 드롭 + administratively prohibited | 로컬 송신자는 EACCES를 받음 |
type | blackhole | 조용히 드롭 | 로컬 송신자는 EINVAL을 받으므로 애플리케이션 관찰 결과가 다를 수 있음 |
scope | global | 원격 네트워크까지 도달 가능 | 게이트웨이 경유 unicast route의 기본 scope |
scope | link | 해당 링크에서 직접 도달 가능 | 직접 연결 경로, on-link next hop 해석의 기준 |
scope | host | 호스트 내부 로컬 주소 | local route와 loopback 의미 해석에 중요 |
proto | kernel | 커널 자동 생성 | 주소 부여, connected route, local route에서 자주 보임 |
proto | static | 관리자 또는 정적 설정이 설치 | 동적 라우팅 데몬이 보통 덮어쓰지 않으므로 운영 의도 전달에 유용 |
proto | boot | 부팅 시 초기 설정 | 라우팅 데몬이 시작되면 정리 대상으로 보는 경우가 많음 |
proto | ra | IPv6 Router Advertisement가 설치 | expires가 붙을 수 있고, RA 갱신/만료를 함께 봐야 함 |
throw는 "드롭 route"가 아니라 "현재 테이블에서 답을 못 찾은 것처럼 만들고 RPDB 다음 규칙으로 진행"시키는 제어 route입니다.
반대로 unreachable, prohibit, blackhole은 lookup 자체를 종료하는 최종 결정입니다.
커널 FIB 조회 API
/* 커널 모듈에서 라우팅 테이블 조회 */
#include <net/ip_fib.h>
struct fib_result res;
struct flowi4 fl4 = {
.daddr = htonl(0x0A000001), /* 10.0.0.1 */
.flowi4_oif = 0,
.flowi4_scope = RT_SCOPE_UNIVERSE,
};
int err = fib_table_lookup(table, &fl4, &res, FIB_LOOKUP_NOREF);
if (!err) {
/* res.fi → fib_info (nexthop 정보) */
/* res.type → RTN_UNICAST, RTN_LOCAL 등 */
/* res.prefixlen → 일치한 접두사 길이 */
}
/* 또는 전체 라우팅 조회 (rules + table + nexthop 해석) */
struct rtable *rt = ip_route_output_flow(net, &fl4, sk);
if (!IS_ERR(rt)) {
/* rt->dst.dev → 출력 디바이스 */
/* rt->rt_gw4 → 게이트웨이 주소 */
ip_rt_put(rt);
}
fib_table_lookup()는 "지정된 테이블 내부 조회"만 수행하므로 RPDB를 건너뜁니다.
규칙, 소스 주소 선택, nexthop 해석까지 포함한 실제 결과가 필요하면 fib_rules_lookup() 또는 ip_route_output_flow() 계열을 봐야 합니다.
Policy Routing
Policy Routing은 목적지 주소뿐 아니라 소스 주소, fwmark, 입력 인터페이스 등 다양한 조건에 따라 다른 라우팅 테이블을 선택합니다.
ip rule 규칙
# 현재 규칙 조회
ip rule show
# 0: from all lookup local
# 32766: from all lookup main
# 32767: from all lookup default
# 규칙 추가: 소스 주소 기반
ip rule add from 10.0.0.0/24 table 100 priority 1000
# fwmark 기반 (netfilter와 연동)
iptables -t mangle -A OUTPUT -p tcp --dport 80 -j MARK --set-mark 1
ip rule add fwmark 1 table 200 priority 2000
# 입력 인터페이스 기반
ip rule add iif eth1 table 300 priority 3000
# 목적지 주소 기반
ip rule add to 203.0.113.0/24 table 400
# uidrange 기반 (특정 사용자의 트래픽)
ip rule add uidrange 1000-1000 table 500
# 복합 조건
ip rule add from 10.0.0.0/24 to 172.16.0.0/12 fwmark 0x10/0xff table 600
# 규칙 삭제
ip rule del priority 1000
커널 내부: fib_rules_ops
/* net/core/fib_rules.c */
/* fib_rules_ops: 프로토콜별 라우팅 규칙 구현 */
struct fib_rules_ops {
int family; /* AF_INET, AF_INET6, AF_DECnet */
int (*action)(struct fib_rule *, struct flowi *, int,
struct fib_lookup_arg *);
int (*match)(struct fib_rule *, struct flowi *, int);
/* ... */
};
/* 라우팅 조회 과정:
* 1. fib_rules_lookup() → 규칙 리스트를 priority 오름차순으로 순회
* 2. 각 규칙에 대해 match() 호출 (from/to/mark/iif 검사)
* 3. 매칭 시 action() 호출 → 해당 테이블에서 fib_table_lookup()
* 4. 결과 없으면 (throw) 다음 규칙으로 계속
*/
자주 쓰는 selector 와 action
| 항목 | 예시 | 의미 | 실무 포인트 |
|---|---|---|---|
from / to | ip rule add from 10.0.0.0/24 table 100 | 소스/목적지 접두사 기준 분기 | 멀티홈, source-based routing, VRF 외부 누수 방지에 자주 사용 |
fwmark | ip rule add fwmark 0x10/0xff table 200 | Netfilter/nftables가 찍은 mark 기준 | VPN split tunnel, transparent proxy, 서비스 체이닝에 적합 |
iif / oif | ip rule add iif eth1 table 300 | 입력/출력 인터페이스 기준 | 로컬 생성 트래픽과 포워딩 트래픽을 분리할 때 유용 |
uidrange | ip rule add uidrange 1000-1999 table 500 | 프로세스 소유자 기준 | 호스트 기반 멀티테넌시, 빌드/배포 경로 분리 |
l3mdev | ip rule show | grep l3mdev | VRF master 디바이스와 연계된 조회 | 현대 VRF 구현의 핵심. per-VRF rule 다발 대신 공통 규칙 하나로 동작 |
goto | ip rule add pref 100 goto 1000 | RPDB를 다른 priority 지점으로 점프 | 규칙 블록을 계층화할 때 유용하지만, 과도하면 운영 가독성이 급격히 나빠짐 |
suppress_prefixlength | ip rule add table main suppress_prefixlength 0 | 특정 길이 이하 접두사 결과를 무시 | 기본 경로 누수를 막고, policy table 우선 적용을 강제할 때 사용 |
Policy Routing 활용 예:
- 멀티홈(dual ISP): 소스 주소에 따라 다른 ISP 게이트웨이 사용
- VPN split tunneling: fwmark로 VPN/직접 경로 분리
- QoS 기반 라우팅: TOS/DSCP 값에 따라 경로 분리
- 컨테이너 네트워킹: veth 입력에 따라 별도 라우팅 테이블
IPv6 라우팅
IPv6 라우팅은 IPv4와 같은 "destination lookup" 원칙을 따르지만, 128비트 주소, RA(Router Advertisement)로 설치되는 경로, source-specific route, RFC 6724 기반 소스 주소 선택 때문에 운영 체감이 더 복잡합니다.
FIB6 구조
/* net/ipv6/ip6_fib.c */
/* IPv6는 fib6_table / fib6_node 기반 prefix tree를 사용 */
/* IPv4 LC-trie 구현을 그대로 재사용하지는 않음 */
struct fib6_info {
struct fib6_table *fib6_table;
struct fib6_info *fib6_nsiblings; /* ECMP siblings */
struct fib6_nh *fib6_nh; /* nexthop */
struct rt6_key fib6_dst; /* 목적지 접두사 */
struct rt6_key fib6_src; /* 소스 접두사 (optional) */
u32 fib6_metric; /* 경로 메트릭 */
u32 fib6_flags; /* RTF_GATEWAY, RTF_REJECT, ... */
unsigned long expires; /* RA 기반 경로 만료 시간 */
};
IPv6 라우팅 특성
# IPv6 라우팅 테이블
ip -6 route show
# ::1 dev lo proto kernel metric 256
# 2001:db8:1::/64 dev eth0 proto kernel metric 256 expires 86400sec
# fe80::/64 dev eth0 proto kernel metric 256
# default via fe80::1 dev eth0 proto ra metric 1024 expires 1800sec
# RA(Router Advertisement)에 의한 자동 경로
# proto ra = 라우터 광고로 설치된 경로 (expires 있음)
# proto kernel = 인터페이스 설정 시 자동 생성
# IPv6 경로 추가
ip -6 route add 2001:db8:2::/48 via 2001:db8:1::1
ip -6 route add 2001:db8:2::/48 dev eth0 # on-link
# link-local 게이트웨이 (일반적 구성)
ip -6 route add default via fe80::1 dev eth0
# IPv6 NDP (Neighbor Discovery) — ARP 대체
ip -6 neigh show
# fe80::1 dev eth0 lladdr 00:11:22:33:44:55 router REACHABLE
# RA 수신 제어
sysctl net.ipv6.conf.eth0.accept_ra=2 # 포워딩 활성 시에도 RA 수신
sysctl net.ipv6.conf.eth0.autoconf=1 # SLAAC 주소 자동 구성
IPv6 vs IPv4 라우팅 차이점:
- NDP vs ARP: IPv6는 ICMPv6 기반 NDP 사용 (더 효율적, 보안 확장 가능)
- RA 기반 자동 구성: IPv6는 라우터가 접두사와 게이트웨이를 광고
- 소스 주소 선택: IPv6 인터페이스에 여러 주소 존재 — 소스 주소 선택 알고리즘(RFC 6724) 중요
- FIB 구조: IPv6는
fib6_node기반 prefix tree, IPv4는 LC-trie (자료구조와 튜닝 포인트가 다름) - 경로 만료: RA 경로는 expires 타이머로 자동 만료/갱신
Multipath / ECMP
앞에서 IPv4와 IPv6의 단일 경로 라우팅을 살펴봤습니다. 하지만 실제 네트워크에서는 동일 목적지로 가는 경로가 여러 개 있을 때 트래픽을 분산하고 싶은 경우가 많습니다. ECMP가 바로 그 방법이며, 앞에서 배운 nexthop 객체의 실전 활용 사례이기도 합니다.
ECMP(Equal-Cost Multi-Path)는 동일 비용의 여러 경로로 트래픽을 분산합니다. Linux는 flow 해시 기반 부하 분산을 사용하여 동일 플로우의 패킷이 같은 경로로 전송되도록 보장합니다.
ECMP 설정
# 레거시 multipath 경로
ip route add 10.0.0.0/24 \
nexthop via 192.168.1.1 dev eth0 weight 1 \
nexthop via 192.168.2.1 dev eth1 weight 1
# nexthop 객체 사용 (5.x+, 권장)
ip nexthop add id 1 via 192.168.1.1 dev eth0
ip nexthop add id 2 via 192.168.2.1 dev eth1
ip nexthop add id 10 group 1/2 # nexthop 그룹
ip route add 10.0.0.0/24 nhid 10
# 가중치 기반 분산 (비균등 분배)
ip nexthop add id 10 group 1,3/2,1 # nh1:weight3, nh2:weight1 → 3:1 분배
# ECMP 해시 알고리즘 선택
sysctl net.ipv4.fib_multipath_hash_policy=0 # L3 only (src/dst IP)
sysctl net.ipv4.fib_multipath_hash_policy=1 # L4 (src/dst IP + port)
sysctl net.ipv4.fib_multipath_hash_policy=2 # L3+inner (터널용)
sysctl net.ipv4.fib_multipath_hash_policy=3 # custom (hash field bitmask 사용)
sysctl net.ipv4.fib_multipath_hash_fields=$((0x0001|0x0002|0x0004|0x0010|0x0020))
sysctl net.ipv4.fib_multipath_use_neigh=1 # neighbour 상태를 고려해 다음 홉 선택
ip-nexthop(8) 기준으로 modern Linux는 route 엔트리가 직접 게이트웨이 배열을 들고 있기보다, nhid로 독립적인 nexthop object 또는 group을 참조할 수 있습니다. 이 구조 덕분에 여러 route가 동일 ECMP 세트를 재사용하고, 라우팅 데몬(FRR/BGP)도 변경 전파를 더 효율적으로 처리할 수 있습니다.
Resilient Hashing
기존 ECMP는 nexthop 변경 시 모든 플로우의 경로가 재분배되어 대규모 환경에서 문제가 됩니다. Resilient hashing은 변경 영향을 최소화합니다:
# resilient nexthop 그룹 (consistent hashing)
ip nexthop add id 10 group 1/2 type resilient buckets 128 idle_timer 120
# buckets: 해시 버킷 수 (많을수록 정밀한 가중치 분배)
# idle_timer: 유휴 버킷 재할당 대기 시간(초)
# nexthop 삭제/추가 시:
# 기존 ECMP: 모든 플로우 재분배 (기존 연결 끊김 가능)
# resilient: 삭제된 nexthop의 버킷만 재할당 (영향 최소화)
# 버킷 상태 확인
ip nexthop bucket show id 10
ECMP 주의사항:
- 비대칭 라우팅: ECMP에서 요청/응답이 다른 경로를 사용할 수 있음.
rp_filter=2(loose) 설정 필요 - conntrack 상호작용: DNAT + ECMP에서 conntrack이 경로를 고정하므로 의도한 분배가 안 될 수 있음
- 해시 편향: 특정 플로우 패턴에서 해시 충돌로 불균등 분배 발생. L4 해시 정책 사용 권장
- nexthop 장애 감지:
fib_multipath_use_neigh=1은 기존 neighbour 상태를 참고하지만, BFD 같은 제어 평면 장애 검출을 완전히 대체하지는 못함
neighbour 해석과 실제 전송
페이지 상단의 파이프라인 다이어그램에서 1~3단계(selector → RPDB → FIB 조회)와 dst_entry 생성까지 다뤘습니다. 이제 4단계 — 결정된 nexthop IP 주소를 실제 MAC 주소로 변환하여 패킷을 물리 회선에 올리는 "Last Mile"을 살펴봅니다.
라우팅 조회(FIB lookup)가 "이 패킷은 192.168.1.1을 거쳐 eth0으로 보내라"라고 결정해도, 실제로 패킷이 wire 위를 타려면 한 단계가 더 남아 있습니다. Ethernet 프레임을 만들려면 게이트웨이(또는 on-link 목적지)의 MAC 주소를 알아야 합니다. 이 "IP → MAC 변환"을 담당하는 것이 neighbour 서브시스템이며, IPv4에서는 ARP, IPv6에서는 NDP(Neighbor Discovery Protocol)를 사용합니다.
neighbour의 역할과 위치
| 역할 | 설명 | 관련 함수/구조체 |
|---|---|---|
| 주소 해석 | IP 주소를 L2 주소(MAC)로 변환. IPv4는 ARP, IPv6는 NDP 사용 | neigh_resolve_output(), arp_solicit(), ndisc_solicit() |
| 캐시 관리 | 해석된 IP→MAC 매핑을 해시 테이블에 캐싱하여 매번 ARP/NDP를 보내지 않아도 됨 | struct neighbour, neigh_lookup() |
| 도달성 추적 | 이웃이 여전히 살아있는지 주기적으로 확인 (NUD 상태 머신) | neigh_timer_handler(), NUD states |
| L2 헤더 생성 | 확인된 MAC으로 Ethernet 프레임 헤더를 만들어 패킷 전송 | neigh_hh_output(), dev_hard_header() |
| 대기 큐 | ARP/NDP 응답을 기다리는 동안 패킷을 임시 보관 | neigh->arp_queue (최대 app_probes개) |
neighbour 핵심 자료구조
neigh_table= ARP 테이블 또는 NDP 테이블 전체 (IPv4용과 IPv6용이 별도)neighbour= 개별 이웃 엔트리 하나 (IP + MAC + 상태)neigh_hash_table= IP 주소로 빠르게 검색하기 위한 해시 테이블neigh_ops= 프로토콜별 동작 함수 (ARP 보내기, NDP 보내기 등)hh_cache= L2 헤더 캐시 (매번 헤더를 새로 만들지 않도록)
/* include/net/neighbour.h — neighbour 핵심 구조체 */
/* neigh_table: 프로토콜별 이웃 테이블 (ARP=arp_tbl, NDP=nd_tbl) */
struct neigh_table {
int family; /* AF_INET 또는 AF_INET6 */
unsigned int entry_size; /* sizeof(struct neighbour) */
int key_len; /* 4(IPv4) 또는 16(IPv6) */
__be16 protocol; /* ETH_P_IP 또는 ETH_P_IPV6 */
struct neigh_hash_table nht; /* 해시 테이블 */
struct neigh_statistics *stats; /* per-CPU 통계 */
struct neigh_parms parms; /* 기본 타이머/임계값 파라미터 */
int gc_thresh1; /* GC 시작 임계값 */
int gc_thresh2; /* GC 적극 수행 임계값 */
int gc_thresh3; /* 최대 엔트리 수 (hard limit) */
struct timer_list gc_timer; /* 주기적 GC 타이머 */
/* 전역: arp_tbl(IPv4), nd_tbl(IPv6) */
};
/* neighbour: 개별 이웃 엔트리 하나 */
struct neighbour {
struct neighbour *next; /* 해시 버킷 체인 */
struct neigh_table *tbl; /* 소속 테이블 */
struct neigh_parms *parms; /* 파라미터 (타이머 등) */
unsigned long used; /* 마지막 사용 시각 */
unsigned long confirmed; /* 마지막 도달성 확인 시각 */
unsigned long updated; /* 마지막 갱신 시각 */
rwlock_t lock;
refcount_t refcnt;
unsigned int arp_queue_len_bytes; /* 대기 큐 바이트 수 */
struct sk_buff_head arp_queue; /* ARP 응답 대기 중인 패킷 큐 */
struct timer_list timer; /* NUD 상태 전이 타이머 */
__u8 nud_state; /* NUD 상태 (REACHABLE, STALE, ...) */
__u8 type; /* 유형 */
__u8 dead; /* 삭제 예정 플래그 */
u8 protocol; /* 학습 프로토콜 (ARP, NDP, ...) */
u8 ha[ALIGN(MAX_ADDR_LEN, sizeof(unsigned long))];
/* hardware address (MAC) */
struct hh_cache hh; /* L2 헤더 캐시 */
int (*output)(struct neighbour *, struct sk_buff *);
/* 출력 함수 포인터 */
const struct neigh_ops *ops; /* 프로토콜별 동작 */
struct net_device *dev; /* 연결된 디바이스 */
u8 primary_key[]; /* IP 주소 (가변 길이 키) */
};
/* hh_cache: L2 헤더 캐시 — 성능 최적화의 핵심 */
struct hh_cache {
unsigned int hh_len; /* 헤더 길이 (Ethernet=14바이트) */
u16 hh_type; /* 프로토콜 타입 (ETH_P_IP 등) */
seqlock_t hh_lock;
unsigned long hh_data[HH_DATA_ALIGN / sizeof(long)];
/* 미리 생성된 L2 헤더 데이터 */
/* REACHABLE 상태에서 이 캐시를 직접 복사하여
* dev_hard_header() 호출을 건너뛸 수 있음 → fast path */
};
neigh_table→ 프로토콜별 전역 테이블. IPv4는arp_tbl, IPv6는nd_tbl이라는 전역 변수neigh_hash_table→ IP 주소를 키로neighbour를 빠르게 찾는 해시 테이블. 동적 리사이즈neighbour→ 개별 이웃 하나. IP 주소, MAC 주소, NUD 상태, 출력 디바이스를 보유hh_cache→ REACHABLE 상태의 neighbour에 붙는 L2 헤더 캐시. 패킷마다 헤더를 새로 만들지 않고 memcpy로 복사arp_queue→ ARP/NDP 응답을 기다리는 동안 패킷을 보관하는 큐 (기본 최대 3개)
NUD 상태 머신 상세
neighbour의 핵심은 NUD(Neighbor Unreachability Detection) 상태 머신입니다. 각 이웃 엔트리는 아래 상태 중 하나에 있으며, 이벤트(ARP 응답, 타이머 만료, 상위 프로토콜 확인 등)에 따라 상태가 전이됩니다.
| 상태 | 의미 | 패킷 처리 | 다음 상태 전이 | 운영 해석 |
|---|---|---|---|---|
NONE | 엔트리 없음 | — | → INCOMPLETE (패킷 전송 시도 시) | 아직 이 IP로 통신한 적 없음 |
INCOMPLETE | ARP/NDP 요청 보냄, 응답 대기 | arp_queue에 대기 (최대 3개) | → REACHABLE (응답 수신) / → FAILED (timeout) | 첫 패킷 지연. route 맞아도 전송 멈춤 |
REACHABLE | 도달성 확인됨 | 즉시 전송 (hh_cache fast path) | → STALE (reachable_time 만료) | 정상. 가장 좋은 상태 |
STALE | 캐시 있으나 확인 안 됨 | 일단 보냄 (기존 MAC 사용) | → DELAY (패킷 전송 시) | 오래 안 쓰인 이웃. 곧 재검증 |
DELAY | 상위 프로토콜 확인 대기 | 보냄 (기존 MAC 사용) | → REACHABLE (TCP ACK 등) / → PROBE | 5초간 TCP ACK 등으로 확인 시도 |
PROBE | 유니캐스트 ARP/NDP 재질의 중 | 보냄 (기존 MAC 사용) | → REACHABLE (응답) / → FAILED | 직접 ARP로 확인 시도 (최대 3회) |
FAILED | 도달 불가 | 드롭 | → INCOMPLETE (새 패킷 시도 시) | 게이트웨이 단절, VLAN/VRF 오류, 방화벽 차단 |
PERMANENT | 관리자 수동 설정 | 즉시 전송 | 만료 없음 | ip neigh add ... nud permanent |
NOARP | ARP 불필요 디바이스 | 즉시 전송 | 만료 없음 | loopback, point-to-point 인터페이스 |
neighbour 조회와 패킷 전송 상세 경로
패킷이 FIB 조회를 마치고 dst_entry를 얻으면, dst->output()을 통해 neighbour 해석 단계로 진입합니다. 이 과정의 함수 호출 체인을 정확히 이해하면 "route는 맞는데 패킷이 안 나가는" 문제를 체계적으로 디버깅할 수 있습니다.
/* net/ipv4/ip_output.c — neighbour 해석 진입점 */
static int ip_finish_output2(struct net *net,
struct sock *sk,
struct sk_buff *skb) {
struct dst_entry *dst = skb_dst(skb);
struct rtable *rt = (struct rtable *)dst;
struct net_device *dev = dst->dev;
struct neighbour *neigh;
u32 nexthop;
/* 1. nexthop IP 결정: 게이트웨이가 있으면 게이트웨이, 없으면 목적지 자체 */
nexthop = rt->rt_gw4 ? rt->rt_gw4 : ip_hdr(skb)->daddr;
/* 2. neighbour 테이블에서 nexthop IP로 검색 */
neigh = __ipv4_neigh_lookup_noref(dev, nexthop);
if (unlikely(!neigh))
neigh = __neigh_create(&arp_tbl, &nexthop, dev, false);
if (!IS_ERR(neigh)) {
/* 3. neighbour를 통해 L2 헤더 만들고 전송 */
return neigh_output(neigh, skb, is_connected);
}
kfree_skb(skb);
return -EINVAL;
}
/* include/net/neighbour.h — neigh_output() 분기 */
static inline int neigh_output(struct neighbour *n,
struct sk_buff *skb,
int skip_cache) {
const struct hh_cache *hh = &n->hh;
/* Fast path: hh_cache가 유효하면 memcpy로 L2 헤더 복사 */
if (!skip_cache && (n->nud_state & NUD_CONNECTED) && hh->hh_len)
return neigh_hh_output(hh, skb);
/* Slow path: ARP 해석이 필요하거나 hh_cache가 없는 경우 */
return n->output(n, skb);
/* → neigh_resolve_output() 또는 neigh_connected_output() */
}
ARP 프로토콜 상세 (IPv4)
/* net/ipv4/arp.c — ARP 요청/응답 처리 핵심 */
/* ARP 요청 전송 (INCOMPLETE 상태에서 호출) */
static void arp_solicit(struct neighbour *neigh,
struct sk_buff *skb) {
__be32 target = *(__be32 *)neigh->primary_key;
struct net_device *dev = neigh->dev;
__be32 saddr;
/* 소스 주소 선택: 출력 디바이스의 주소 중 target과 같은 서브넷 */
saddr = inet_select_addr(dev, target, RT_SCOPE_LINK);
/* ARP Request 패킷 생성 및 전송 */
arp_send(ARPOP_REQUEST, /* 요청 */
ETH_P_ARP, /* ARP 프로토콜 */
target, /* 대상 IP (누구의 MAC을 알고 싶은가) */
dev, /* 출력 디바이스 */
saddr, /* 내 IP */
NULL, /* 대상 MAC (모르니까 NULL → broadcast) */
dev->dev_addr, /* 내 MAC */
NULL); /* 대상 MAC (NULL → ff:ff:ff:ff:ff:ff) */
}
/* ARP 응답 수신 처리 */
static int arp_rcv(struct sk_buff *skb,
struct net_device *dev,
struct packet_type *pt, ...) {
/* → arp_process() 호출 */
}
static int arp_process(struct net *net,
struct sock *sk,
struct sk_buff *skb) {
struct arphdr *arp = arp_hdr(skb);
if (arp->ar_op == htons(ARPOP_REPLY)) {
/* ARP Reply 처리:
* 1. neighbour 테이블에서 sender IP로 검색
* 2. 찾으면 MAC 주소 갱신 + NUD 상태 → REACHABLE
* 3. arp_queue에 대기 중이던 패킷 전송
*/
neigh_update(n, sha, /* sender의 MAC 주소 */
NUD_REACHABLE,
NEIGH_UPDATE_F_OVERRIDE);
}
if (arp->ar_op == htons(ARPOP_REQUEST)) {
/* ARP Request 처리:
* 1. target IP가 내 주소인지 확인
* 2. 맞으면 ARP Reply 전송
* 3. 동시에 sender의 MAC을 테이블에 기록 (학습)
*/
if (addr_type == RTN_LOCAL) {
arp_send_dst(ARPOP_REPLY, ETH_P_ARP,
sip, dev, tip, sha,
dev->dev_addr, sha, reply_dst);
}
}
}
NDP 프로토콜 상세 (IPv6)
IPv6는 ARP 대신 NDP(Neighbor Discovery Protocol)를 사용합니다. NDP는 ICMPv6 위에서 동작하며, ARP보다 더 많은 기능(라우터 발견, 주소 자동 설정, DAD)을 제공합니다.
| 비교 항목 | ARP (IPv4) | NDP (IPv6) |
|---|---|---|
| 프로토콜 | 독립 프로토콜 (EtherType 0x0806) | ICMPv6 위에서 동작 (type 135/136) |
| 주소 해석 요청 | ARP Request (broadcast) | Neighbor Solicitation (solicited-node multicast) |
| 주소 해석 응답 | ARP Reply (unicast) | Neighbor Advertisement (unicast 또는 multicast) |
| 브로드캐스트 범위 | 전체 L2 세그먼트 (모든 장비가 처리) | solicited-node multicast (해당 IP 장비만 처리) |
| 라우터 발견 | 별도 (DHCP 또는 수동) | 내장 (Router Solicitation/Advertisement) |
| 주소 충돌 감지 | Gratuitous ARP (선택) | DAD — Duplicate Address Detection (필수) |
| 보안 | ARP spoofing에 취약 | SEND(Secure NDP) 확장 가능 |
| 커널 소스 | net/ipv4/arp.c | net/ipv6/ndisc.c |
| 커널 테이블 | arp_tbl | nd_tbl |
# IPv6 neighbour 확인
ip -6 neigh show
# fe80::1 dev eth0 lladdr aa:bb:cc:dd:ee:ff REACHABLE
# 2001:db8::1 dev eth0 lladdr 11:22:33:44:55:66 STALE
# NDP 메시지 모니터링
tcpdump -i eth0 -n 'icmp6 and (ip6[40]==135 or ip6[40]==136)'
# 135 = Neighbor Solicitation, 136 = Neighbor Advertisement
# DAD (Duplicate Address Detection) 상태 확인
ip -6 addr show dev eth0
# "tentative" 플래그가 있으면 DAD 진행 중
neighbour GC(Garbage Collection)와 테이블 관리
neighbour 테이블은 무한히 커질 수 없습니다. 커널은 3단계 임계값(gc_thresh1/2/3)으로 테이블 크기를 관리하며, 주기적으로 오래되거나 FAILED 상태의 엔트리를 정리합니다.
# neighbour 테이블 크기 관련 sysctl
# IPv4 ARP 테이블
sysctl net.ipv4.neigh.default.gc_thresh1=128 # 이하: GC 안 함
sysctl net.ipv4.neigh.default.gc_thresh2=512 # 이상: 온건한 GC
sysctl net.ipv4.neigh.default.gc_thresh3=1024 # 이상: 새 엔트리 거부!
sysctl net.ipv4.neigh.default.gc_stale_time=60 # STALE 엔트리 GC 대상 시간(초)
sysctl net.ipv4.neigh.default.gc_interval=30 # GC 주기(초)
# IPv6 NDP 테이블 (동일 구조)
sysctl net.ipv6.neigh.default.gc_thresh1=128
sysctl net.ipv6.neigh.default.gc_thresh2=512
sysctl net.ipv6.neigh.default.gc_thresh3=1024
# NUD 상태 관련 타이머
sysctl net.ipv4.neigh.default.base_reachable_time_ms=30000 # REACHABLE 유지 시간
sysctl net.ipv4.neigh.default.delay_first_probe_time=5 # DELAY 대기 시간
sysctl net.ipv4.neigh.default.retrans_time_ms=1000 # 재전송 간격
sysctl net.ipv4.neigh.default.ucast_solicit=3 # 유니캐스트 재시도 횟수
sysctl net.ipv4.neigh.default.mcast_solicit=3 # 브로드캐스트 재시도 횟수
sysctl net.ipv4.neigh.default.unres_qlen=31 # arp_queue 최대 패킷 수
# 현재 테이블 크기 확인
ip neigh show | wc -l # 전체 엔트리 수
ip neigh show nud reachable | wc -l # REACHABLE 상태만
ip neigh show nud stale | wc -l # STALE 상태만
ip neigh show nud failed | wc -l # FAILED 상태만
# per-인터페이스 설정 (전역과 별도)
sysctl net.ipv4.neigh.eth0.base_reachable_time_ms=15000 # eth0만 15초
- 커널 로그에
"Neighbour table overflow"메시지 출력 - 새로운 이웃 엔트리 생성 실패 → 새 목적지/게이트웨이로의 패킷이 드롭됨
- 기존 REACHABLE 엔트리는 유지되므로 기존 연결은 동작하지만, 새 연결 수립 불가
- Docker/Kubernetes 환경에서 특히 문제: 수천 개의 Pod가 각각 veth 인터페이스를 가지며, 호스트의 neighbour 테이블이 빠르게 포화
- 해결:
gc_thresh1/2/3를 환경에 맞게 상향 조정
Proxy ARP와 ARP 관련 보안
Proxy ARP는 라우터가 다른 서브넷의 호스트를 대신하여 ARP 응답을 보내는 기능입니다. 또한 ARP는 인증이 없으므로 다양한 보안 위협에 노출됩니다.
# Proxy ARP — 라우터가 다른 서브넷을 대신하여 ARP 응답
sysctl net.ipv4.conf.eth0.proxy_arp=1
# 사용 사례: VLAN 간 라우팅 없이 통신, 컨테이너 네트워킹
# Proxy ARP (pvlan) — 같은 서브넷 내 호스트 간 proxy
sysctl net.ipv4.conf.eth0.proxy_arp_pvlan=1
# ARP announce/ignore — 소스 주소 선택 정책
sysctl net.ipv4.conf.all.arp_announce=2 # best local addr only
sysctl net.ipv4.conf.all.arp_ignore=1 # target IP가 이 dev에 있을 때만 응답
# VRRP, keepalived, LVS 환경에서 ARP 충돌 방지에 중요
# Gratuitous ARP — IP 충돌 감지/알림
arping -U -I eth0 192.168.1.100 # Gratuitous ARP 발송
arping -D -I eth0 192.168.1.100 # DAD (Duplicate Address Detection)
# ARP spoofing 방어
# 1. 정적 ARP 엔트리 (소규모)
ip neigh add 192.168.1.1 lladdr aa:bb:cc:dd:ee:ff nud permanent dev eth0
# 2. arpwatch — ARP 변경 모니터링
arpwatch -i eth0
# 3. DAI (Dynamic ARP Inspection) — 스위치 레벨 방어
| ARP 보안 위협 | 원리 | 방어 방법 |
|---|---|---|
| ARP Spoofing | 위조 ARP Reply로 피해자의 neighbour 테이블 오염 | 정적 ARP, DAI, arpwatch, 802.1X |
| ARP Flooding | 대량 ARP Request로 스위치 CAM 테이블 포화 | 스위치 ARP rate-limiting, storm control |
| Gratuitous ARP 남용 | 위조 Gratuitous ARP로 다른 장비의 IP 탈취 | arp_accept=0, DAI, DHCP snooping |
| ARP Cache Poisoning | MITM: 게이트웨이와 피해자 양쪽 ARP 테이블 조작 | 정적 ARP, 암호화(TLS/IPsec), VPN |
neighbour 디버깅 실전 가이드
####################################################
# 시나리오 1: "route는 맞는데 패킷이 안 나감"
####################################################
# Step 1: route 확인 (정상)
ip route get 10.0.0.5
# 10.0.0.5 via 192.168.1.1 dev eth0 src 192.168.1.100
# Step 2: neighbour 확인 → 문제 발견!
ip neigh show 192.168.1.1 dev eth0
# 192.168.1.1 dev eth0 INCOMPLETE ← MAC을 모른다!
# 또는
# 192.168.1.1 dev eth0 FAILED ← ARP 응답 없음
# Step 3: 원인 진단
# a) 게이트웨이가 물리적으로 연결되어 있는가?
ethtool eth0 | grep 'Link detected'
# b) 같은 VLAN에 있는가?
bridge vlan show dev eth0
# c) 방화벽이 ARP를 차단하고 있는가?
ebtables -L | grep arp
# d) 수동으로 ARP 보내보기
arping -I eth0 192.168.1.1
# Step 4: 임시 해결 — 정적 neighbour 설정
ip neigh replace 192.168.1.1 lladdr aa:bb:cc:dd:ee:ff nud permanent dev eth0
####################################################
# 시나리오 2: "간헐적으로 첫 패킷이 느림" (ARP 지연)
####################################################
# 증상: 통신이 잘 되다가 잠시 안 쓰면 첫 패킷 지연
# 원인: REACHABLE → STALE → DELAY → PROBE 전이 중 지연
# 확인: neighbour 상태 모니터링
ip monitor neigh
# 192.168.1.1 dev eth0 lladdr aa:bb:cc:dd:ee:ff REACHABLE
# (30초 후)
# 192.168.1.1 dev eth0 lladdr aa:bb:cc:dd:ee:ff STALE
# 해결: reachable_time 늘리기 (환경에 따라)
sysctl -w net.ipv4.neigh.eth0.base_reachable_time_ms=120000 # 2분
####################################################
# 시나리오 3: "Neighbour table overflow" 커널 메시지
####################################################
# 확인: 현재 엔트리 수
ip neigh show | wc -l
# 1025 ← gc_thresh3(1024) 초과!
# 확인: 어떤 상태가 많은가?
ip neigh show | awk '{print $NF}' | sort | uniq -c | sort -rn
# 800 STALE ← 대부분 STALE (오래된 캐시)
# 150 REACHABLE
# 75 FAILED
# 해결 1: 임계값 상향
sysctl -w net.ipv4.neigh.default.gc_thresh1=4096
sysctl -w net.ipv4.neigh.default.gc_thresh2=8192
sysctl -w net.ipv4.neigh.default.gc_thresh3=16384
# 해결 2: STALE/FAILED 엔트리 정리
ip neigh flush nud failed
ip neigh flush nud stale dev eth0
####################################################
# 시나리오 4: "MAC 주소가 잘못 캐싱됨" (ARP 오염)
####################################################
# 증상: 특정 IP로 ping이 안 가지만 다른 곳에서는 감
# 확인
ip neigh show 192.168.1.1 dev eth0
# 192.168.1.1 dev eth0 lladdr 00:00:00:00:00:00 REACHABLE
# ^^^^^^^^^^^^^^^^^^^^^^^ 잘못된 MAC!
# 해결: 해당 엔트리 삭제 후 재학습
ip neigh del 192.168.1.1 dev eth0
# 다음 패킷 전송 시 새 ARP Request 자동 발생
# 또는: 모든 캐시 플러시
ip neigh flush all
####################################################
# 유용한 모니터링 명령어
####################################################
# 실시간 neighbour 변경 모니터링
ip monitor neigh
# neighbour 통계
ip -s neigh show dev eth0
# 192.168.1.1 dev eth0 lladdr aa:bb:cc:dd:ee:ff ref 2 used 5/3/1 ...
# used/confirmed/updated
# /proc 통계 (per-CPU)
cat /proc/net/stat/arp_cache
# entries allocs destroys hash_grows lookups hits ...
# bpftrace로 ARP 지연 측정
bpftrace -e 'kprobe:arp_solicit { @arp_req = count(); }
kprobe:arp_process { @arp_resp = count(); }
interval:s:5 { print(@arp_req); print(@arp_resp);
clear(@arp_req); clear(@arp_resp); }'
- neighbour는 FIB 조회 다음 단계로, IP→MAC 변환을 담당합니다. 이것이 실패하면 route가 맞아도 패킷이 나가지 않습니다.
- NUD 상태 머신이 이웃의 도달성을 추적합니다: INCOMPLETE → REACHABLE → STALE → DELAY → PROBE → FAILED (또는 REACHABLE로 복귀)
- hh_cache는 REACHABLE 상태에서 L2 헤더를 미리 캐시하여 fast path를 제공합니다. 이것이 패킷당 ~10ns의 성능을 가능하게 합니다.
- gc_thresh3은 hard limit입니다. 이를 초과하면 새 연결이 실패하므로, 대규모 환경에서는 반드시 상향 조정해야 합니다.
- 디버깅 핵심:
ip neigh show로 NUD 상태를 확인하고,ip monitor neigh로 실시간 변화를 추적하세요.
VRF (Virtual Routing and Forwarding)
앞에서 라우팅 테이블이 여러 개 존재할 수 있고, Policy Routing으로 테이블을 선택할 수 있음을 배웠습니다. VRF는 이 개념을 한 단계 더 발전시켜, 인터페이스 그룹 단위로 라우팅 도메인을 완전히 격리합니다. 앞의 FIB 네임스페이스 섹션과 함께 읽으면 격리 수준의 차이를 이해할 수 있습니다.
VRF는 단일 호스트 안에서 여러 개의 독립된 L3 라우팅 도메인을 제공합니다. 핵심은 "VRF 디바이스가 특정 FIB table을 대표하고, 인터페이스를 그 VRF에 enslave하면 그 인터페이스의 local/connected route가 해당 table로 이동한다"는 점입니다. VRF는 netns처럼 전체 네트워크 스택을 복제하지 않으며, 인터페이스 하나는 한 시점에 하나의 VRF에만 속할 수 있습니다.
# VRF 디바이스 생성
ip link add vrf-red type vrf table 100
ip link set vrf-red up
# 인터페이스를 VRF에 할당
ip link set eth1 master vrf-red
ip link set eth2 master vrf-red
# VRF별 라우팅 테이블 (자동으로 table 100 사용)
ip route add 10.0.0.0/24 via 192.168.1.1 vrf vrf-red
ip route show vrf vrf-red
# VRF 컨텍스트에서 명령 실행
ip vrf exec vrf-red ping 10.0.0.1
ip vrf exec vrf-red ss -tlnp
# VRF에 바인딩된 소켓 (SO_BINDTODEVICE)
# 전역 소켓이 VRF 패킷을 받게 하려면 l3mdev sysctl을 명시
sysctl net.ipv4.tcp_l3mdev_accept=1
sysctl net.ipv4.udp_l3mdev_accept=1
# VRF 목록 확인
ip vrf show
# Name Table
# vrf-red 100
# vrf-blue 200
l3mdev 규칙과 라우팅 흐름
커널 문서 Documentation/networking/vrf.rst 기준으로, 현대 VRF 구현의 핵심은 l3mdev 규칙입니다. 첫 번째 VRF를 만들면 IPv4/IPv6용 공통 l3mdev FIB rule이 자동 추가되며, 이 규칙이 "이 패킷이 어느 VRF table을 봐야 하는가"를 결정합니다.
# 첫 VRF 생성 후 보통 보이게 되는 규칙
ip rule show
# ...
# 1000: from all lookup [l3mdev-table]
# 인터페이스를 VRF에 편입하면 local/connected route가 해당 table로 이동
ip link set dev eth1 master vrf-red
ip route show table 100
# VRF에서 수신하는 서버가 아니더라도 전역 listen 소켓으로 받게 할 수 있음
sysctl net.ipv4.tcp_l3mdev_accept=1
sysctl net.ipv4.raw_l3mdev_accept=0
tcp_l3mdev_accept=1을 켜면 VRF-bound listener와 unbound listener 중 어느 소켓이 연결을 받을지 모호해질 수 있으므로, VRF-aware 서버에서는 명시적 SO_BINDTODEVICE가 더 안전합니다.
VRF 활용 시나리오
| 시나리오 | 구성 | 장점 |
|---|---|---|
| 멀티테넌트 라우터 | 테넌트별 VRF + 라우팅 테이블 | 테넌트 간 IP 충돌 허용, 격리 |
| 관리 네트워크 분리 | 관리 인터페이스를 별도 VRF에 | 데이터 플레인과 관리 트래픽 격리 |
| BGP/MPLS VPN PE | VRF + FRR(BGP) | L3VPN PE 라우터 구현 |
| 컨테이너 네트워킹 | Pod별 VRF (netns 대신) | netns보다 가벼운 L3 격리 |
VRF vs Network Namespace: VRF는 FIB와 L3 라우팅 문맥을 분리하지만, netns처럼 neighbour 테이블, conntrack, netfilter 전체를 별도 인스턴스로 복제하지는 않습니다. 완전한 네트워크 격리가 필요하면 netns, 같은 호스트 내부에서 여러 라우팅 도메인만 분리하려면 VRF가 더 적합합니다.
Routing Cache와 최적화
지금까지 FIB 조회, neighbour 해석, 경로 관리를 모두 살펴봤습니다. 여기서 자연스러운 질문이 하나 떠오릅니다: "매 패킷마다 FIB trie를 순회하면 느리지 않을까?" 과거 커널은 이 문제를 전역 캐시로 해결하려 했지만, 오히려 더 큰 문제를 만들었습니다.
Route Cache 제거 역사 (3.6+)
커널 3.6 이전에는 IPv4 route lookup 결과를 공격 표면이 큰 전역 cache에 적극적으로 올렸습니다. 이 구조는 랜덤 목적지 트래픽으로 cache를 오염시키는 DoS에 취약했고, GC 비용도 컸습니다. 현재는 "일반 route 결과를 전역 cache에서 찾는다"기보다, FIB를 직접 조회하고 PMTU/redirect 같은 예외만 별도 자료구조에 유지하는 방향으로 바뀌었습니다.
/* 커널 3.6 이전: route cache (제거됨)
* - 해시 테이블에 (src, dst, tos, iif) → rtable 캐싱
* - 문제: 랜덤 목적지 트래픽으로 캐시 크기 폭발 (DoS)
* - 문제: 캐시 GC(Garbage Collection) 비용이 높음
* - commit 89aef8921b ("ipv4: Remove rt cache")
*/
/* 커널 3.6+: FIB nexthop exception cache
* - PMTU, 리다이렉트 등 예외만 캐싱
* - 일반 조회는 매번 FIB trie를 직접 조회 (충분히 빠름)
*/
struct fib_nh_exception {
struct fib_nh_exception *fnhe_next;
int fnhe_genid;
__be32 fnhe_daddr; /* 목적지 */
u32 fnhe_pmtu; /* Path MTU */
bool fnhe_mtu_locked;
__be32 fnhe_gw; /* redirect 게이트웨이 */
unsigned long fnhe_expires; /* 만료 시간 */
struct rtable *fnhe_rth_input;
struct rtable *fnhe_rth_output;
unsigned long fnhe_stamp;
struct rcu_head rcu;
};
/proc/net/rt_cache 또는 route 관련 통계가 남아 있어도, 그것을 예전 의미의 "전역 destination route cache"로 이해하면 안 됩니다.
현대 커널에서 핵심은 FIB direct lookup, dst_entry 재사용, nexthop exception cache입니다.
FIB 조회 최적화
| 최적화 기법 | 핵심 아이디어 | 효과 |
|---|---|---|
| LC-trie / fib6 prefix tree | 주소 계열별로 최적화된 prefix 자료구조 사용 | 대규모 FIB에서도 예측 가능한 LPM lookup |
| RCU 기반 읽기 경로 | lookup hot path에서 lock 경합 최소화 | 멀티코어 라우터/호스트에서 읽기 확장성 확보 |
dst_entry 재사용 | 완전히 별도 route cache 대신 결과 객체와 output ops를 재사용 | 출력 경로 메모리 재활용과 PMTU/redirect 반영 |
| nexthop object / group | route와 nexthop 세트를 분리해 공유 | ECMP, offload, 라우팅 데몬 연동 단순화 |
| FIB notifier / offload sync | FIB 변경을 switchdev/HW offload에 전달 | 소프트웨어와 하드웨어 forwarding state 일치 유지 |
라우팅과 Netfilter 상호작용
앞에서 배운 flowi의 mark 필드와 Policy Routing의 fwmark 규칙이 바로 여기서 Netfilter와 만납니다. Netfilter의 mark/NAT 처리는 라우팅 결정에 직접적인 영향을 미칩니다. 핵심은 "변경이 route lookup 이전에 일어났는지, 이후에 일어나서 기존 dst를 무효화해야 하는지"를 구분하는 것입니다.
DNAT와 라우팅 재조회
/* ingress IPv4: PREROUTING 뒤에 첫 route lookup이 일어남
*
* NIC → ip_rcv()
* → NF_INET_PRE_ROUTING
* - mangle/raw/nat PREROUTING이 mark / daddr를 바꿀 수 있음
* → ip_rcv_finish()
* → ip_route_input_noref()
* - 변경된 daddr/mark 기준으로 첫 lookup 수행
*/
/* local output: 먼저 route를 잡고, OUTPUT에서 바뀌면 재평가 필요
*
* socket send → ip_route_output_flow()
* → NF_INET_LOCAL_OUT
* - nat OUTPUT DNAT, mark 변경이 dst를 무효화할 수 있음
* - 이 경우 ip_route_me_harder() 계열 재조회가 개입
* → NF_INET_POST_ROUTING → dev_queue_xmit()
*/
/* conntrack과 라우팅의 상호작용
* - NAT은 conntrack state를 기반으로 동작
* - 첫 패킷이 NAT 결정을 만들고, 이후 패킷은 conntrack tuple을 재사용
* - NOTRACK된 패킷은 NAT 대상이 아님
*/
PREROUTING DNAT는 보통 "재조회"보다 "변경된 목적지 기준 최초 조회"에 가깝고, OUTPUT DNAT는 "이미 잡힌 route를 다시 계산해야 하는 경우"에 가깝습니다.
Netfilter 훅과 라우팅 시점
# 패킷 흐름에서 라우팅과 Netfilter의 순서:
#
# 수신 경로:
# NIC → [PREROUTING] → 라우팅 결정 → [INPUT] → 로컬 프로세스
# ↓ (forward)
# [FORWARD] → [POSTROUTING] → NIC
#
# 송신 경로:
# 로컬 프로세스 → 라우팅 결정 → [OUTPUT] → [POSTROUTING] → NIC
#
# PREROUTING mark/DNAT는 첫 ingress lookup에 반영
# OUTPUT mark/DNAT는 기존 dst를 무효화해 재조회가 필요할 수 있음
# SNAT는 일반적으로 POSTROUTING에서 적용 → route 선택 이후
# REDIRECT는 로컬 주소로 destination을 바꾸는 DNAT 계열
# ingress DNAT 예시: 변경된 목적지 기준으로 lookup
iptables -t nat -A PREROUTING -d 1.2.3.4 -p tcp --dport 80 \
-j DNAT --to-destination 192.168.1.100:80
# → 1.2.3.4:80 목적지가 192.168.1.100:80으로 변경
# → 이후 ingress route lookup은 192.168.1.100 기준으로 수행
라우팅과 네임스페이스
바로 앞의 VRF가 "같은 커널 안에서 L3 테이블만 분리"하는 방식이라면, 네트워크 네임스페이스는 라우팅 테이블뿐 아니라 전체 네트워크 스택(인터페이스, ARP, conntrack, iptables)을 완전히 복제하는 더 강력한 격리입니다. Docker 컨테이너의 네트워크가 바로 이 방식으로 동작합니다.
각 네트워크 네임스페이스는 독립된 라우팅 테이블, RPDB 규칙, nexthop, neighbour, conntrack 문맥을 가집니다. VRF가 "같은 netns 안에서 L3 table만 분리"한다면, netns는 아예 별도 네트워크 스택 인스턴스를 만드는 방식입니다.
네트워크 네임스페이스별 라우팅 격리
# 네임스페이스 생성 및 veth 쌍 연결
ip netns add ns1
ip link add veth0 type veth peer name veth1
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 route add default via 10.0.0.1
# 네임스페이스의 라우팅 테이블은 완전히 독립
ip netns exec ns1 ip route show
# 10.0.0.0/24 dev veth1 proto kernel scope link src 10.0.0.2
# default via 10.0.0.1 dev veth1
# 호스트에서 네임스페이스로의 포워딩 (호스트에서 설정)
sysctl net.ipv4.ip_forward=1
iptables -t nat -A POSTROUTING -s 10.0.0.0/24 -o eth0 -j MASQUERADE
Bridge + Routing
# 브리지는 L2, 라우팅은 L3 — 상호작용 주의
# 브리지 인터페이스에 IP 할당 → L3 라우팅 가능
ip link add br0 type bridge
ip link set eth1 master br0
ip link set eth2 master br0
ip addr add 10.0.0.1/24 dev br0
ip link set br0 up
# 브리지 내 패킷의 netfilter 통과 제어
sysctl net.bridge.bridge-nf-call-iptables=1 # 브리지 패킷이 iptables 통과
sysctl net.bridge.bridge-nf-call-iptables=0 # 성능 우선: iptables 바이패스
# 주의: bridge-nf-call-iptables=1이면
# L2 포워딩 패킷도 iptables FORWARD 체인 통과
# → 예상치 못한 드롭 발생 가능
# → Docker/Kubernetes 환경에서 자주 문제가 됨
성능 튜닝과 주의사항
앞의 섹션들에서 라우팅의 내부 구조를 살펴봤습니다. 이 섹션에서는 실무에서 만나는 성능 문제와 그 해결책을 다룹니다. FIB 메모리 사용량이나 neighbour 테이블 크기 같은 앞의 개념들이 실제 운영에서 어떻게 나타나는지 연결됩니다.
Neighbour table overflow 메시지), ECMP 트래픽 불균형, 대규모 FIB(10만+ 경로)에서의 메모리 부족이나 조회 지연, rp_filter에 의한 정상 패킷 드롭.
ARP/Neighbor 테이블 크기
# 대규모 L2 네트워크에서 ARP 테이블 오버플로 방지
sysctl net.ipv4.neigh.default.gc_thresh1=1024 # GC 시작 임계값
sysctl net.ipv4.neigh.default.gc_thresh2=2048 # soft limit (5초 후 GC)
sysctl net.ipv4.neigh.default.gc_thresh3=4096 # hard limit (즉시 GC)
# IPv6 neighbor 테이블
sysctl net.ipv6.neigh.default.gc_thresh3=4096
# 증상: "Neighbour table overflow" 커널 메시지
# → gc_thresh3 증가 필요
# ARP 캐시 타임아웃
sysctl net.ipv4.neigh.default.gc_stale_time=120 # stale 엔트리 GC 주기(초)
sysctl net.ipv4.neigh.default.base_reachable_time_ms=30000 # REACHABLE 유지 시간
rp_filter (Reverse Path Filtering)
# rp_filter: 소스 주소 기반 패킷 검증 (스푸핑 방지)
sysctl net.ipv4.conf.all.rp_filter=1 # strict mode
# → 소스 주소로의 역경로가 수신 인터페이스와 동일해야 통과
# → 비대칭 라우팅 환경에서 정상 패킷 드롭!
sysctl net.ipv4.conf.all.rp_filter=2 # loose mode
# → 소스 주소로의 역경로가 어떤 인터페이스든 존재하면 통과
# → ECMP, VPN, 멀티홈 환경에서 권장
sysctl net.ipv4.conf.all.rp_filter=0 # disabled
# → 소스 주소 검증 없음 (보안 위험)
# 주의: 인터페이스별 설정과 all의 관계
# 실제 적용값 = max(conf.all.rp_filter, conf.IFNAME.rp_filter)
# → all=1이면 인터페이스별로 0으로 해도 strict 적용됨
ip_forward와 관련 설정
# IP 포워딩 활성화
sysctl net.ipv4.ip_forward=1
sysctl net.ipv6.conf.all.forwarding=1
# 주의: IPv6 forwarding=1 시 RA 수신이 비활성화됨
# 해결: accept_ra=2 설정
sysctl net.ipv6.conf.eth0.accept_ra=2
# 포워딩 관련 성능 파라미터
sysctl net.ipv4.ip_forward_use_pmtu=0 # 0: 인터페이스 MTU 사용 (권장)
sysctl net.ipv4.ip_forward_update_priority=1 # TOS → priority 변환
sysctl net.ipv4.fib_multipath_use_neigh=1 # nexthop 상태 기반 분배
링크 다운과 죽은 경로 회피
# 링크가 down 된 인터페이스의 route를 lookup에서 무시
sysctl net.ipv4.conf.all.ignore_routes_with_linkdown=1
sysctl net.ipv6.conf.all.ignore_routes_with_linkdown=1
# VRF/ECMP 환경에서는 link-down 무시 정책과 neighbour 상태를 함께 봐야 함
sysctl net.ipv4.fib_multipath_use_neigh=1
대규모 라우팅 테이블
대규모 라우팅 테이블(BGP full table ~100만 경로) 주의사항:
- 메모리: full BGP table은 ~500MB~1GB 메모리 사용.
fib_info와fib_alias구조체가 대부분 - 수렴 시간: 대량 경로 추가/삭제 시 LC-trie 리밸런싱 비용 증가
- 조회 성능: LC-trie 깊이 증가로 조회 시간 약간 증가 (여전히 O(W=32) 보장)
- gc_thresh: ARP/neigh 테이블도 충분히 크게 설정
- 모니터링:
/proc/net/fib_triestat으로 trie 깊이, 노드 수, 메모리 사용량 확인
디버깅
이 페이지에서 배운 모든 개념 — FIB 조회, Policy Routing, neighbour 해석, Netfilter 상호작용 — 이 실전에서 문제가 될 때 어떻게 원인을 추적하는지 봅니다.
ip route get으로 커널이 어떤 경로를 선택하는지 확인 → ② ip rule show로 어떤 테이블을 보는지 확인 → ③ 해당 테이블에 경로가 있는지 확인. 이 세 단계로 대부분의 라우팅 문제 원인을 좁힐 수 있습니다.
ip route get — 경로 조회 시뮬레이션
# 특정 목적지로의 경로 확인 (실제 커널 FIB 조회 수행)
ip route get 8.8.8.8
# 8.8.8.8 via 192.168.1.1 dev eth0 src 192.168.1.100 uid 0
# cache
# 소스 주소 지정
ip route get 8.8.8.8 from 10.0.0.1
# mark 지정 (policy routing 테스트)
ip route get 8.8.8.8 mark 0x1
# 입력 인터페이스 지정
ip route get 8.8.8.8 iif eth1
# IPv6
ip -6 route get 2001:4860:4860::8888
# fibmatch: FIB 엔트리 직접 조회 (경로 정보 상세)
ip route get fibmatch 10.0.0.1
# 10.0.0.0/24 dev eth0 proto kernel scope link src 10.0.0.100
# route와 rule 변경을 실시간 감시
ip -ts monitor route rule nexthop neigh
FIB 관련 /proc 파일
# FIB trie 구조 (IPv4)
cat /proc/net/fib_trie
# FIB 통계
cat /proc/net/fib_triestat
# Basic info: size of leaf: 56 bytes, size of tnode: 40 bytes.
# Main:
# Aver depth: 2.35
# Max depth: 5
# Leaves: 15
# Prefixes: 18
# Internal nodes: 6
# 1: 3 2: 2 3: 1
# Pointers: 24
# Null ptrs: 12
# Total size: 2 kB
# 기존 형식 라우팅 테이블
cat /proc/net/route
# Iface Destination Gateway Flags RefCnt Use Metric Mask ...
# IPv6 라우팅
cat /proc/net/ipv6_route
FIB 조회 성능 추적
# ftrace로 FIB 조회 함수 추적
echo fib_table_lookup > /sys/kernel/tracing/set_ftrace_filter
echo function > /sys/kernel/tracing/current_tracer
echo 1 > /sys/kernel/tracing/tracing_on
# ... 트래픽 발생 ...
cat /sys/kernel/tracing/trace
# perf로 FIB 조회 비용 측정
perf record -e 'fib:*' -a sleep 10
perf script
# dropwatch로 라우팅 드롭 추적
dropwatch -l kas
# → ip_error, ip_forward 등에서 드롭 위치 확인
# BPF 기반 FIB 조회 추적
bpftrace -e 'kretprobe:fib_table_lookup /retval != 0/ {
@fail[retval] = count();
}'
# skb mark 확인 (policy routing 디버깅)
bpftrace -e 'kprobe:ip_route_input_slow {
printf("mark=%x daddr=%x\n",
((struct sk_buff *)arg0)->mark,
((struct iphdr *)(((struct sk_buff *)arg0)->head +
((struct sk_buff *)arg0)->network_header))->daddr);
}'
Segment Routing (SRv6)
지금까지의 라우팅은 "목적지 주소 → 다음 홉"이라는 hop-by-hop 결정이었습니다. SRv6는 이를 넘어서, 패킷이 통과할 경유지 목록 전체를 출발지에서 미리 지정하는 소스 라우팅 기술입니다. 앞에서 배운 FIB6 위에 구축되며, VRF와 결합하여 End.DT4/DT6 같은 액션으로 테이블 간 연계도 가능합니다.
SRv6(Segment Routing over IPv6)는 RFC 8986이 정의한 IPv6 Segment Routing 동작을 Linux route encap에 매핑한 기술입니다. 일반 라우팅이 "현재 목적지까지의 다음 홉"을 고르는 반면, SRv6는 SRH(Segment Routing Header)에 담긴 SID 목록에 따라 중간 노드에서 수행할 동작까지 경로 자체에 포함시킵니다.
SRv6 개념
- SRv6 기본 개념:
- Segment: 네트워크 노드의 IPv6 주소 (또는 함수)
- Segment List: 패킷이 통과할 노드의 순서 리스트
- SRH (Segment Routing Header): IPv6 확장 헤더에 세그먼트 리스트 포함
- SID (Segment Identifier): 128비트 IPv6 주소 형태
- 형식: [Locator (네트워크 접두사)] + [Function (동작)]
- 패킷 형태:
- [IPv6 Header (DA=현재 세그먼트)] [SRH: 세그먼트 리스트] [원본 패킷]
- 각 노드에서: Segments Left-- → DA를 다음 세그먼트로 변경 → 포워딩
Linux SRv6 설정
# SRv6 캡슐화 (Encapsulation)
ip route add 10.0.0.0/24 encap seg6 mode encap \
segs fc00:1::1,fc00:2::1 dev eth0
# → 10.0.0.0/24 향 패킷을 SRv6로 캡슐화
# SRv6 인라인 모드 (원본이 이미 IPv6인 경우)
ip route add 2001:db8:2::/48 encap seg6 mode inline \
segs fc00:1::1,fc00:2::1 dev eth0
# SRv6 로컬 SID 액션 (수신 측)
ip -6 route add fc00:1::100 encap seg6local action End count dev eth0
# End: 세그먼트 처리 후 다음 세그먼트로 포워딩
sysctl net.vrf.strict_mode=1
ip -6 route add fc00:1::200 encap seg6local action End.DT4 vrftable 100
# End.DT4: SRH 제거 후 IPv4 패킷을 VRF table 100에서 라우팅
ip -6 route add fc00:1::300 encap seg6local action End.DT6 vrftable 200
# End.DT6: SRH 제거 후 IPv6 패킷을 VRF table 200에서 라우팅
ip -6 route add fc00:1::400 encap seg6local action End.DX4 nh4 10.0.0.1 dev eth1
# End.DX4: SRH 제거 후 특정 IPv4 nexthop으로 전달
주요 SRv6 액션
| 액션 | 설명 | 사용 시나리오 |
|---|---|---|
End | 세그먼트 처리, 다음 SID로 포워딩 | 중간 경유 노드 (transit) |
End.X | End + L3 cross-connect (특정 nexthop으로) | 특정 이웃으로 직접 전달 |
End.DT4 | 캡슐화 해제 → IPv4 라우팅 테이블 조회 | VPN PE에서 IPv4 VRF lookup |
End.DT6 | 캡슐화 해제 → IPv6 라우팅 테이블 조회 | VPN PE에서 IPv6 VRF lookup |
End.DX4 | 캡슐화 해제 → 특정 IPv4 nexthop | 1:1 VPN 터널 종단 |
End.DX6 | 캡슐화 해제 → 특정 IPv6 nexthop | 1:1 VPN 터널 종단 |
End.B6.Encaps | SRv6 재캡슐화 (SRH 추가/수정) | 중간 노드에서 경로 변경 |
SRv6 주의사항:
- MTU 오버헤드: SRH 헤더 추가로 패킷 크기 증가. 세그먼트 1개당 16바이트(IPv6 주소). 4-세그먼트 → +64바이트+SRH 고정 8바이트
- CONFIG_IPV6_SEG6: 커널 설정에서 SRv6 지원 활성화 필요
vrftable제약:End.DT4/End.DT6를 VRF table에 연결할 때는net.vrf.strict_mode=1이 필요- Segments Left 의미: 일부 local action은
Segments Left가 0이거나 0이 아니어야만 동작하므로, 캡슐화와 종단 동작을 혼동하면 드롭 원인을 찾기 어려움 - 보안: SRH를 통한 경로 조작 가능. 경계 라우터에서 외부 SRH 패킷 필터링 권장
- 성능: 소프트웨어 SRv6 처리는 CPU 집약적. SmartNIC offload 또는 DPDK/VPP 활용 고려
# SRv6 상태 확인
ip -6 route show table all | grep seg6
ip -s -6 route show table all | grep -A2 seg6local
# HMAC 키와 상태
cat /proc/net/seg6_hmac
# tcpdump로 SRH 확인
tcpdump -vvv -i eth0 ip6 and 'ip6[40] == 43'
# Routing Header Type 4 (SRH)
# Segments Left: 2
# [0] fc00:2::1
# [1] fc00:1::1
가상 네트워크 디바이스
Linux Bridge, Bonding/Team, VLAN, VXLAN, MACVLAN/IPVLAN 등의 가상 네트워크 디바이스는 별도 전문 문서로 분리되었습니다. 각 주제에 대한 상세 내용은 아래 문서를 참고하세요.
- Linux Bridge 심화 — 소프트웨어 L2 스위치, FDB, STP/RSTP, IGMP Snooping, br_netfilter, TC flower 오프로드
- VLAN/VXLAN/Switchdev — VLAN(802.1Q) 태깅, Bridge VLAN Filtering, VXLAN 오버레이, switchdev HW 오프로드
- Bonding/Team/MACVLAN — NIC 이중화(Bonding/Team), MACVLAN/IPVLAN, veth pair, TUN/TAP
관련 문서
라우팅과 관련된 다른 주제를 더 깊이 이해하고 싶다면 다음 문서를 참고하세요.