Netfilter Flowtable 심화
Netfilter Flowtable의 SW/HW 오프로드 메커니즘, conntrack 대비 성능 비교, nftables flowtable 설정, SmartNIC 연동, 역방향 경로 캐싱, bypass 조건 종합 가이드. 커널 내부 데이터 경로, 핵심 자료구조/API, 운영 환경 튜닝 포인트와 장애 디버깅 절차까지 실무 관점으로 다룹니다.
핵심 요약
- Flowtable이란 — ESTABLISHED 상태의 TCP/UDP 연결을 Netfilter 훅 체인 밖의 별도 단축 경로로 포워딩하는 커널 가속 구조입니다.
- conntrack과의 관계 — Flowtable은 conntrack을 대체하지 않습니다. conntrack이 연결을 확인하면 그 이후 패킷을 빠른 경로로 처리할 뿐입니다.
- SW 오프로드 — 커널 내에서 L3 라우팅·NAT 조회를 캐싱하여 nftables/iptables 규칙 재평가 없이 패킷을 전달합니다.
- HW 오프로드 — SmartNIC의
ndo_flow_offload 콜백을 통해 세션 정보를 NIC 하드웨어 플로우 테이블에 내려보내 CPU를 완전히 우회합니다.
- 성능 차이 — 일반 Netfilter: ~10 Gbps → SW Flowtable: ~40 Gbps → HW Flowtable: 100 Gbps+ (선형 처리).
- bypass 불가 조건 — NAT helper가 붙은 연결, 멀티캐스트, ICMP 오류 메시지, IP 단편화 패킷은 일반 경로로 처리됩니다.
- nftables 설정 —
flowtable 블록으로 장치를 등록하고, chain에서 flow add @ft로 연결을 등록합니다.
- 역방향 경로 — Flowtable은 양방향 튜플(forward/reply)을 각각 캐싱하여 패킷 방향과 무관하게 단축 경로를 적용합니다.
- NAT 연동 — NAT(SNAT/DNAT)가 적용된 연결도 Flowtable에 등록됩니다. 변환 주소·포트가 flow_offload_tuple에 미리 저장되어 fast path에서 직접 헤더를 수정합니다. 단, FTP/SIP 같은 ALG 연결은 항상 slow path를 사용합니다.
- GC 메커니즘 — GC 워커가 2초 주기로 만료 플로우를 정리합니다. TCP RST·FIN을 받거나 30초 동안 패킷이 없으면 플로우가 DYING → DELETED 상태를 거쳐 제거됩니다.
- IPv6 지원 — nf_flow_offload_ipv6_hook()이 IPv6 fast path를 처리합니다. Extension Header(Hop-by-Hop, Routing, Fragment 등)가 있는 패킷은 slow path로 처리됩니다.
ndo_flow_offload 콜백을 통해 세션 정보를 NIC 하드웨어 플로우 테이블에 내려보내 CPU를 완전히 우회합니다.flowtable 블록으로 장치를 등록하고, chain에서 flow add @ft로 연결을 등록합니다.단계별 이해
- conntrack 기초 확인
conntrack -L로 현재 연결 상태를 확인합니다. Flowtable은 ESTABLISHED 상태의 연결만 오프로드합니다. - nftables flowtable 설정
flowtable ft { hook ingress priority 0; devices = { eth0, eth1 }; }를 정의하고 forward 체인에서flow add @ft를 추가합니다. - SW 오프로드 동작 확인
conntrack -L | grep OFFLOAD로 오프로드된 세션을 확인합니다.nft list flowtable로 등록된 장치 목록을 확인합니다. - 성능 측정
iperf3 또는 pktgen으로 Flowtable 전후 처리량을 비교합니다. 일반적으로 4배 이상 향상을 기대할 수 있습니다. - HW 오프로드 활성화
SmartNIC이 지원되면 flowtable에flags offload를 추가하고ethtool -K eth0 hw-tc-offload on으로 TC offload를 활성화합니다. - bypass 조건 점검
FTP(conntrack helper 사용), 멀티캐스트, IP 단편화 트래픽은 오프로드되지 않으므로 일반 경로로 처리됨을 인지하고 방화벽 규칙을 설계합니다. - VLAN/Bridge 환경 구성
bridge 아래 물리 포트를 flowtable devices에 직접 등록합니다. VLAN filtering이 활성화된 bridge에서도 동작하며, VLAN encap 정보가 flow tuple에 캐싱됩니다.ip link add br0 type bridge vlan_filtering 1으로 브리지를 생성한 후 nftables flowtable에 물리 포트(eth0, eth1)를 devices로 지정하세요. - IPv6 Flowtable 활성화
nftables에서ip6 nexthdr { tcp, udp } flow add @ft를 forward chain에 추가합니다. IPv6 Extension Header가 없는 일반 TCP/UDP 세션이 fast path로 처리됩니다.conntrack -L -f ipv6 | grep OFFLOAD로 IPv6 오프로드 세션을 확인할 수 있습니다.
개요: Flowtable과 NGFW
Netfilter Flowtable(이하 Flowtable)은 리눅스 커널 4.16에서 도입된 세션 기반 패킷 가속 메커니즘입니다. 기존 Netfilter 아키텍처는 모든 패킷이 PREROUTING → FORWARD → POSTROUTING 훅 체인을 순서대로 통과해야 했지만, Flowtable은 이미 검사가 완료된 ESTABLISHED 연결의 패킷을 별도의 단축 경로(fast path)로 전달합니다.
차세대 방화벽(NGFW)과 통신사 장비에서는 수십만~수백만 개의 동시 세션을 처리해야 합니다. 전통적인 Netfilter 경로는 각 패킷마다 모든 테이블·체인·규칙을 재평가하므로, 세션이 많아질수록 CPU 부하가 선형 증가합니다. Flowtable은 이 문제를 세션 단위 캐싱으로 해결합니다.
| 방식 | 경로 | 규칙 재평가 | CPU 개입 | 적용 대상 |
|---|---|---|---|---|
| 일반 Netfilter | PREROUTING → FORWARD → POSTROUTING | 매 패킷마다 | 항상 | 모든 패킷 |
| Flowtable SW 오프로드 | Ingress → flowtable lookup → 직접 전달 | 없음 (캐시 히트) | 항상 (커널 내) | ESTABLISHED TCP/UDP |
| Flowtable HW 오프로드 | NIC 내부 플로우 테이블 | 없음 | 없음 (NIC 처리) | ESTABLISHED TCP/UDP (SmartNIC 지원) |
Flowtable 아키텍처
Flowtable의 핵심 자료구조는 net/netfilter/nf_flow_table_core.c에 정의되어 있습니다.
nf_flowtable은 해시 테이블 기반의 플로우 항목 집합이며, 각 세션은 양방향 튜플로 표현됩니다.
/* include/net/netfilter/nf_flow_table.h */
/* 플로우 테이블 전체를 나타내는 구조체 */
struct nf_flowtable {
struct list_head list; /* 전역 flowtable 링크드 리스트 */
struct rhashtable rhashtable; /* 튜플 해시 테이블 */
struct flow_block flow_block; /* TC flow block (HW 오프로드) */
struct delayed_work gc_work; /* GC 워커 (만료 엔트리 정리) */
const struct nf_flowtable_type *type;
u32 flags; /* NF_FLOWTABLE_HW_OFFLOAD 등 */
struct net *net;
};
/* 단일 플로우 항목 (세션 1개 = entry 1개) */
struct flow_offload {
struct flow_offload_tuple_rhash tuplehash[FLOW_OFFLOAD_DIR_MAX];
u32 flags; /* FLOW_OFFLOAD_DYING 등 */
u64 timeout; /* 만료 시각 (jiffies) */
struct rcu_head rcu_head;
};
/* 단방향 튜플 (ORIGINAL 또는 REPLY 방향) */
struct flow_offload_tuple {
union nf_inet_addr src_v4; /* 출발지 주소 */
union nf_inet_addr dst_v4; /* 목적지 주소 */
__be16 src_port; /* 출발지 포트 */
__be16 dst_port; /* 목적지 포트 */
u8 l3proto; /* NFPROTO_IPV4 또는 NFPROTO_IPV6 */
u8 l4proto; /* IPPROTO_TCP 또는 IPPROTO_UDP */
u8 dir; /* FLOW_OFFLOAD_DIR_ORIGINAL/REPLY */
struct net_device *iifidx; /* 입력 인터페이스 */
struct dst_entry *dst_cache; /* 라우팅 캐시 */
u8 dst_mac[ETH_ALEN]; /* 다음 홉 MAC 주소 */
u8 src_mac[ETH_ALEN]; /* 소스 MAC 주소 */
/* NAT이 적용된 경우 변환된 주소/포트도 저장 */
union nf_inet_addr nat_src;
union nf_inet_addr nat_dst;
__be16 nat_sport;
__be16 nat_dport;
};
/* 해시 테이블 항목 래퍼 */
struct flow_offload_tuple_rhash {
struct rhash_head node; /* rhashtable 연결 */
struct flow_offload_tuple tuple;
};
패킷이 ingress hook에 도달하면 nf_flow_offload_inet_hook()이 호출됩니다.
이 함수는 rhashtable에서 5-튜플(src/dst IP, src/dst port, 프로토콜)로 해시 조회를 수행합니다.
히트(hit)하면 NAT 재작성 후 직접 전달하고, 미스(miss)이면 일반 Netfilter slow path로 진행합니다.
rhashtable 기반 해시 테이블 성능 분석
Flowtable의 룩업 성능은 lib/rhashtable.c에 구현된 RCU-safe resizable 해시 테이블에 의존합니다.
이 구조는 잠금 없는 읽기(lock-free read)와 자동 크기 조정(auto-resize)을 지원하여
수십만 개의 동시 세션 처리에 적합합니다.
/* Flowtable 초기화: net/netfilter/nf_flow_table_core.c */
/* rhashtable 파라미터 — 튜플 기반 해시/비교 함수 */
static const struct rhashtable_params nf_flow_offload_rhash_params = {
.head_offset = offsetof(struct flow_offload_tuple_rhash, node),
.hashfn = nf_flow_offload_hash, /* SipHash 기반 */
.obj_hashfn = nf_flow_offload_hash_obj,
.obj_cmpfn = nf_flow_offload_cmp, /* 5-튜플 비교 */
.automatic_shrinking = true, /* 세션 감소 시 자동 축소 */
};
/* Flowtable 초기화 */
int nf_flow_table_init(struct nf_flowtable *flowtable)
{
int err;
/* rhashtable 초기화 (초기 bucket 64개, 필요 시 자동 확장) */
err = rhashtable_init(&flowtable->rhashtable,
&nf_flow_offload_rhash_params);
if (err < 0)
return err;
/* GC 워크 초기화 — 2초 후 첫 실행, 이후 주기적으로 반복 */
INIT_DEFERRABLE_WORK(&flowtable->gc_work, nf_flow_offload_work_gc);
queue_delayed_work(system_power_efficient_wq,
&flowtable->gc_work, HZ * 2);
return 0;
}
/* Flowtable 해제 */
void nf_flow_table_free(struct nf_flowtable *flowtable)
{
/* GC 워크 취소 후 잔여 플로우 강제 정리 */
cancel_delayed_work_sync(&flowtable->gc_work);
/* 남은 플로우 모두 teardown */
nf_flow_table_iterate(flowtable, nf_flow_table_do_cleanup, NULL);
/* rhashtable 해제 */
rhashtable_destroy(&flowtable->rhashtable);
}
/* 5-튜플 해시 함수 (SipHash 기반, 충돌 공격 저항성) */
static u32 nf_flow_offload_hash(const void *data, u32 len, u32 seed)
{
const struct flow_offload_tuple *tuple = data;
return siphash(tuple, offsetof(struct flow_offload_tuple, dir),
&nf_flowtable_siphash_key);
}
| 특성 | rhashtable | 일반 해시 테이블 |
|---|---|---|
| 읽기 잠금 | RCU read-side (잠금 없음) | spinlock 또는 rwlock |
| 크기 조정 | 자동 확장/축소 (트리거: 로드 팩터 > 0.75) | 고정 크기 또는 수동 |
| 해시 충돌 방지 | SipHash (랜덤 시드) | MD5/CRC32 (예측 가능) |
| 캐시 효율 | 버킷당 연결 리스트 (캐시 친화적) | 체인 해싱 (캐시 미스 많음) |
| 1M 세션 룩업 | ~100ns (L2 캐시 히트 시) | ~500ns (캐시 미스 시) |
SW 오프로드 메커니즘
SW 오프로드는 커널 내부에서 동작하므로 특별한 하드웨어가 필요 없습니다. 핵심 아이디어는 conntrack이 연결을 ESTABLISHED로 마킹하는 순간, 해당 연결의 라우팅·NAT 정보를 flowtable에 캐싱하는 것입니다. 이후 같은 5-튜플의 패킷은 Netfilter 훅 체인 전체를 건너뛰고 캐시된 경로로 직접 전달됩니다.
/* net/netfilter/nf_flow_table_core.c */
/* nftables 표현식 nft_flow_offload.c 에서 호출됨 */
int flow_offload_add(struct nf_flowtable *flow_table,
struct flow_offload *flow)
{
int err;
/* 1. ORIGINAL 방향 튜플을 rhashtable에 삽입 */
err = rhashtable_insert_fast(&flow_table->rhashtable,
&flow->tuplehash[FLOW_OFFLOAD_DIR_ORIGINAL].node,
nf_flow_offload_rhash_params);
if (err)
return err;
/* 2. REPLY 방향 튜플을 rhashtable에 삽입 (역방향 경로 캐싱) */
err = rhashtable_insert_fast(&flow_table->rhashtable,
&flow->tuplehash[FLOW_OFFLOAD_DIR_REPLY].node,
nf_flow_offload_rhash_params);
if (err) {
rhashtable_remove_fast(&flow_table->rhashtable,
&flow->tuplehash[FLOW_OFFLOAD_DIR_ORIGINAL].node,
nf_flow_offload_rhash_params);
return err;
}
/* 3. HW 오프로드 플래그 확인 후 NIC에 등록 (비동기 워크큐) */
if (nf_flowtable_hw_offload(flow_table))
nf_flow_offload_work_alloc(flow_table, flow, FLOW_CLS_REPLACE);
flow->timeout = nf_flowtable_time_stamp() + NF_FLOW_TIMEOUT;
return 0;
}
/* 패킷 처리 fast path: net/netfilter/nf_flow_table_ip.c */
static int nf_flow_offload_ip_hook(void *priv,
struct sk_buff *skb,
const struct nf_hook_state *state)
{
struct flow_offload_tuple_rhash *tuplehash;
struct nf_flowtable *flow_table = priv;
struct flow_offload_tuple tuple;
struct flow_offload *flow;
enum flow_offload_tuple_dir dir;
/* bypass: 단편화 패킷, 멀티캐스트 등 */
if (nf_flow_tuple_ip(skb, state->in, &tuple, &dir) < 0)
return NF_ACCEPT; /* 파싱 실패 -> slow path */
/* rhashtable 조회 (RCU read-side lock) */
tuplehash = flow_offload_lookup(flow_table, &tuple);
if (!tuplehash)
return NF_ACCEPT; /* 미스 -> slow path */
flow = container_of(tuplehash, struct flow_offload,
tuplehash[tuplehash->tuple.dir]);
/* TCP 상태 검사 (FIN/RST이면 플로우 만료 처리) */
if (nf_flow_tcp_check(skb, flow))
return NF_ACCEPT; /* slow path에서 연결 종료 처리 */
/* NAT 재작성: 캐시된 변환 주소·포트 직접 적용 */
nf_flow_nat_ip(flow, skb, tuplehash->tuple.dir);
/* TTL 감소 및 IP 체크섬 재계산 */
ip_decrease_ttl(ip_hdr(skb));
/* 캐시된 dst_entry로 직접 전달 */
skb_dst_set_noref(skb, tuplehash->tuple.dst_cache);
/* timeout 갱신 (활성 플로우 유지) */
flow->timeout = nf_flowtable_time_stamp() + NF_FLOW_TIMEOUT;
return NF_STOLEN; /* 커널이 직접 처리 완료, 상위 훅 건너뜀 */
}
SW 오프로드가 일반 Netfilter 경로보다 빠른 핵심 이유:
- 규칙 재평가 없음: iptables/nftables 테이블·체인·규칙 순서를 전혀 탐색하지 않습니다.
- conntrack 조회 최소화: conntrack 상태를 재확인하지 않고 flowtable timeout만 갱신합니다.
- 라우팅 캐시 활용:
dst_cache에 이미 해결된 next-hop MAC 주소와 출력 인터페이스가 저장되어ip_route_output()호출이 불필요합니다. - NAT 인라인 처리: 변환 주소와 포트가 튜플에 미리 저장되어
nf_nat_packet()호출 없이 헤더를 직접 수정합니다. - GC 비동기 처리: 플로우 만료(timeout, TCP FIN/RST)는 지연된 워크큐에서 비동기적으로 처리합니다.
체크섬 업데이트 및 경로 유효성 검사
/* net/netfilter/nf_flow_table_ip.c — 체크섬 재계산 */
/* L4 체크섬 업데이트 (NAT 적용 후 필수) */
static void nf_flow_ip_transport_checksum(struct sk_buff *skb,
const struct flow_offload *flow,
enum flow_offload_tuple_dir dir)
{
struct flow_offload_tuple *tuplehash = &flow->tuplehash[dir].tuple;
/* HW 체크섬 오프로드 지원 시 생략 가능 */
if (skb->ip_summed == CHECKSUM_PARTIAL)
return;
/* TCP/UDP 체크섬 pseudo 헤더 업데이트 */
if (tuplehash->l4proto == IPPROTO_TCP) {
struct tcphdr *tcph = tcp_hdr(skb);
inet_proto_csum_replace4(&tcph->check, skb,
tuplehash->old_daddr,
tuplehash->new_daddr, true);
inet_proto_csum_replace2(&tcph->check, skb,
tuplehash->old_dport,
tuplehash->new_dport, false);
} else if (tuplehash->l4proto == IPPROTO_UDP) {
struct udphdr *udph = udp_hdr(skb);
if (udph->check) {
inet_proto_csum_replace4(&udph->check, skb,
tuplehash->old_daddr,
tuplehash->new_daddr, true);
inet_proto_csum_replace2(&udph->check, skb,
tuplehash->old_dport,
tuplehash->new_dport, false);
}
}
}
/* 경로 유효성 검사 — dst_entry 만료 감지 */
static bool nf_flow_table_check_dst_entry(struct flow_offload_tuple *tuple,
struct sk_buff *skb)
{
struct dst_entry *dst = tuple->dst_cache;
/* dst가 obsolete(라우팅 변경)이면 false 반환 -> slow path fallback */
if (unlikely(dst->obsolete > 0)) {
/* 플로우를 dying으로 표시하여 재등록 유도 */
flow_offload_teardown(container_of(tuple,
struct flow_offload,
tuplehash[tuple->dir].tuple));
return false;
}
/* MTU 검사: 패킷이 dst MTU를 초과하면 단편화 또는 ICMP 생성 필요 */
if (unlikely(skb->len > dst_mtu(dst) &&
!skb_is_gso(skb))) {
return false; /* slow path에서 PMTU 처리 */
}
return true;
}
/* dst_output()으로 직접 패킷 전달 */
static int nf_flow_queue_xmit(struct net *net, struct sk_buff *skb,
const struct flow_offload_tuple *tuple,
unsigned short type)
{
struct net_device *outdev = tuple->dst_cache->dev;
/* 출력 인터페이스 설정 */
skb->dev = outdev;
/* Ethernet 헤더 재작성 (캐시된 src/dst MAC 사용) */
skb_push(skb, sizeof(struct ethhdr));
eth_hdr(skb)->h_proto = htons(type);
ether_addr_copy(eth_hdr(skb)->h_dest, tuple->dst_mac);
ether_addr_copy(eth_hdr(skb)->h_source, tuple->src_mac);
/* 직접 전송 (Netfilter 훅 우회) */
return dev_queue_xmit(skb);
}
NF_FLOW_TIMEOUT은 기본 30초(30 * HZ)입니다.
패킷이 통과할 때마다 flow->timeout이 현재 시각 + 30초로 갱신됩니다.
30초 동안 패킷이 없으면 GC 워커가 플로우를 제거하고, 이후 패킷은 slow path에서 conntrack을 통해
다시 flowtable에 등록될 수 있습니다.
| 처리 단계 | 일반 Netfilter | Flowtable SW 오프로드 |
|---|---|---|
| 패킷 수신 | netif_receive_skb() | netif_receive_skb() |
| 라우팅 결정 | ip_rcv() → ip_route_input() | 생략 (dst_cache 사용) |
| conntrack 조회 | nf_conntrack_in() | 생략 |
| 방화벽 규칙 | nft_do_chain() (전체 규칙 평가) | 생략 |
| NAT 변환 | nf_nat_packet() | 인라인 헤더 수정 |
| 출력 라우팅 | ip_route_output() | 생략 (dst_cache 사용) |
| 패킷 전송 | dev_queue_xmit() | dev_queue_xmit() |
HW 오프로드 (SmartNIC 연동)
HW 오프로드는 flowtable에 등록된 세션 정보를 SmartNIC의 내부 플로우 테이블로 내려보내는 기능입니다. 세션이 NIC에 등록되면 이후 패킷은 CPU를 전혀 거치지 않고 NIC 내부에서 처리되어 TX 포트로 직접 전달됩니다. 이를 통해 이론적으로 NIC 라인 레이트(100 Gbps+)에 근접한 처리량을 달성할 수 있습니다.
/* include/linux/netdevice.h */
struct net_device_ops {
/* SmartNIC 드라이버가 구현하는 HW 오프로드 콜백 */
int (*ndo_flow_offload_check)(struct flow_cls_offload *cls_flow);
int (*ndo_flow_offload)(enum flow_cls_cmd cmd,
struct flow_offload *flow,
struct nf_flowtable *flowtable);
};
/* Mellanox ConnectX 드라이버 예시 */
static int mlx5e_tc_flow_offload(enum flow_cls_cmd cmd,
struct flow_offload *flow,
struct nf_flowtable *flowtable)
{
struct mlx5e_priv *priv = netdev_priv(
flow->tuplehash[FLOW_OFFLOAD_DIR_ORIGINAL].tuple.iifidx);
switch (cmd) {
case FLOW_CLS_REPLACE:
/* NIC 하드웨어 플로우 테이블에 엔트리 추가 */
return mlx5e_tc_add_fdb_flow(priv, flow, flowtable);
case FLOW_CLS_DESTROY:
/* NIC 플로우 테이블에서 엔트리 제거 */
mlx5e_tc_del_fdb_flow(priv, flow);
return 0;
case FLOW_CLS_STATS:
/* NIC에서 패킷/바이트 카운터 읽기 (timeout 갱신용) */
return mlx5e_tc_stats_fdb_flow(priv, flow);
}
return -EOPNOTSUPP;
}
/*
* HW 오프로드 등록 단계 (비동기 워크큐):
*
* 1. nftables forward chain: flow add @ft
* └─ nft_flow_offload_eval()
* └─ flow_offload_alloc() <- conntrack 엔트리에서 flow 생성
* └─ flow_offload_add() <- rhashtable + HW offload 큐잉
* └─ nf_flow_offload_work_alloc()
* └─ queue_work(system_unbound_wq, &offload->work)
*
* 2. 워크큐 실행 (별도 컨텍스트):
* └─ nf_flow_offload_work()
* └─ nf_flow_offload_hw()
* └─ flow_cls_offload 구성 (TC flower 형태)
* └─ tc_setup_cb_call() -> NIC 드라이버
* └─ ndo_flow_offload(FLOW_CLS_REPLACE, ...)
* └─ NIC 내부 플로우 테이블 등록
*
* 3. 이후 패킷:
* NIC RX -> NIC 내부 플로우 테이블 히트
* -> NIC 내부에서 NAT 변환 + MAC 재작성 + 포워딩
* -> NIC TX (CPU 개입 없음)
*/
mlx5 드라이버 HW 오프로드 구현 경로
/* drivers/net/ethernet/mellanox/mlx5/core/en/tc/act/act.c 경로 참고 */
/*
* mlx5 HW offload 등록 흐름:
*
* nf_flow_offload_work() [워크큐 컨텍스트]
* └─ nf_flow_offload_hw_add()
* └─ flow_cls_offload 구성
* ├─ key: src/dst IP, src/dst port, proto
* ├─ action: NAT 변환 주소, 출력 포트, MAC 재작성
* └─ tc_setup_cb_call(block, TC_SETUP_CLSFLOWER, &cls_flower)
* └─ mlx5e_setup_tc_cls_flower()
* └─ mlx5e_configure_flower()
* ├─ mlx5e_tc_add_fdb_flow() -- FDB 규칙 추가
* └─ mlx5_eswitch_add_offloaded_rule() -- HW 규칙
*
* HW 처리 경로 (NIC 내부):
* 패킷 수신 → FDB 룩업 (ASIC) → NAT 변환 (ASIC) → 포트 포워딩
* → 패킷 송신 (CPU 개입 없음)
*/
/* HW 오프로드 실패 시 SW fallback */
static void nf_flow_offload_work(struct work_struct *work)
{
struct flow_offload_work *offload_work =
container_of(work, struct flow_offload_work, work);
struct nf_flowtable *flowtable = offload_work->flowtable;
struct flow_offload *flow = offload_work->flow;
int err;
switch (offload_work->cmd) {
case FLOW_CLS_REPLACE:
err = nf_flow_offload_hw_add(offload_work->net, flow, flowtable);
if (err) {
/* HW 오프로드 실패 → SW 오프로드로 계속 동작 */
/* NF_FLOW_HW 플래그 미설정 → fast path는 SW에서 처리 */
pr_debug("HW offload failed (%d), falling back to SW\n", err);
}
break;
case FLOW_CLS_DESTROY:
nf_flow_offload_hw_del(offload_work->net, flow, flowtable);
break;
case FLOW_CLS_STATS:
/* NIC에서 패킷/바이트 카운터를 읽어 timeout 갱신 */
nf_flow_offload_stats(flowtable, flow);
break;
}
kfree(offload_work);
}
/* HW 통계 폴링 — CPU 개입 없이 처리된 패킷 카운팅 */
static void nf_flow_offload_stats(struct nf_flowtable *flowtable,
struct flow_offload *flow)
{
struct flow_cls_offload cls_flow = {};
unsigned long delta_jiffies;
/* NIC 드라이버로부터 패킷/바이트 카운터 읽기 */
tc_setup_cb_call(&flowtable->flow_block, TC_SETUP_CLSFLOWER,
&cls_flow, false, true);
/* 마지막 폴링 이후 패킷이 있었다면 timeout 갱신 */
if (cls_flow.stats.pkts) {
delta_jiffies = cls_flow.stats.lastused - jiffies;
flow->timeout = nf_flowtable_time_stamp() +
NF_FLOW_TIMEOUT - delta_jiffies;
}
}
| 항목 | 요구사항 | 확인 방법 |
|---|---|---|
| 커널 버전 | 5.13 이상 (stable HW offload) | uname -r |
| NIC 지원 | ndo_flow_offload 구현 (Mellanox CX5+, Netronome NFP 등) | ethtool -k eth0 | grep hw-tc-offload |
| TC offload 활성화 | hw-tc-offload = on | ethtool -K eth0 hw-tc-offload on |
| nftables 플래그 | flowtable 내 flags offload |
nft list flowtable inet filter ft |
| switchdev 모드 (선택) | SR-IOV 환경에서 eswitch switchdev 전환 | devlink dev eswitch show pci/0000:03:00.0 |
성능 비교 (conntrack vs flowtable)
아래 수치는 일반적인 x86 서버(Intel Xeon, 1코어 사용) 기준의 참고값입니다. 실제 성능은 패킷 크기, CPU 클럭, NIC 드라이버, 메모리 대역폭에 따라 달라집니다.
| 처리 방식 | 처리량 (Mpps) | 처리량 (Gbps) | 레이턴시 (us) | CPU 사용률 |
|---|---|---|---|---|
| 일반 Netfilter (iptables) | ~1.5 Mpps | ~8 Gbps | 15–25 us | 100% |
| 일반 Netfilter (nftables) | ~1.8 Mpps | ~10 Gbps | 12–20 us | 100% |
| Flowtable SW 오프로드 | ~7.5 Mpps | ~40 Gbps | 3–8 us | 100% |
| XDP/eBPF 포워딩 | ~20 Mpps | ~100 Gbps | 1–3 us | 100% |
| Flowtable HW 오프로드 (SmartNIC) | ~148 Mpps | 100 Gbps+ | <1 us | ~0% |
# iperf3으로 flowtable 전후 처리량 비교
# 서버측
iperf3 -s
# 클라이언트측 (32개 병렬 스트림, 60초)
iperf3 -c "num">192.168."num">1.1 -P "num">32 -t "num">60
# pktgen으로 소형 패킷 PPS 측정
modprobe pktgen
echo "add_device eth0" > /proc/net/pktgen/kpktgend_0
echo "count ">10000000" > /proc/net/pktgen/eth0
echo "pkt_size ">64" > /proc/net/pktgen/eth0
echo "dst_mac aa:bb:cc:dd:ee:ff" > /proc/net/pktgen/eth0
echo "start" > /proc/net/pktgen/pgctrl
# conntrack 통계로 fast/slow path 비율 확인
conntrack -S
# found=X -> flowtable rhashtable 히트 (fast path)
# searched=X -> conntrack 전체 조회 횟수
# flowtable 오프로드된 세션 확인 ([OFFLOAD] 플래그)
conntrack -L | grep OFFLOAD | wc -l
Flowtable bypass 조건
모든 패킷이 flowtable fast path를 사용할 수 있는 것은 아닙니다. 다음 조건에 해당하는 패킷은 flowtable을 우회하여 일반 Netfilter slow path로 처리됩니다. 이 조건을 이해하지 못하면 방화벽 정책이 의도치 않게 적용되지 않는 보안 문제가 발생할 수 있습니다.
| bypass 조건 | 커널 검사 위치 | 이유 |
|---|---|---|
| NAT helper 활성 연결 (FTP, SIP, H.323 등) | nft_flow_offload_eval() | helper가 페이로드를 검사·수정해야 함 (nfct_help(ct) 확인) |
| IP 단편화 패킷 (IP_MF 또는 frag_off > 0) | nf_flow_tuple_ip() | 단편 재조합 없이 5-튜플 추출 불가 |
| 멀티캐스트/브로드캐스트 패킷 | nf_flow_tuple_ip() | ipv4_is_multicast(daddr) → slow path 강제 |
| ICMP/ICMPv6 오류 메시지 | nf_flow_tuple_ip() | 내포된 원본 패킷 헤더 파싱 필요 |
| TCP FIN/RST 수신 | nf_flow_tcp_check() | 연결 종료 처리 및 플로우 teardown 필요 |
| flowtable timeout 만료 (30s idle) | nf_flow_is_dying() | GC 워커가 플로우 제거 중 (재등록 가능) |
| IP TTL = 1 | nf_flow_offload_ip_hook() | TTL 감소 후 0이 되면 ICMP Time Exceeded 생성 필요 |
| IPSec 경로 패킷 | nft_flow_offload_skip() | skb_sec_path(skb) → IPSec 처리 우선 |
| 연결 상태 비ESTABLISHED | flow_offload_alloc() | SYN, SYN-ACK 등 핸드셰이크 패킷은 등록 불가 |
| DNAT 목적지가 로컬 소켓 | 라우팅 결정 시 | 로컬 소켓 전달(local_in)은 별도 경로 |
/* net/netfilter/nft_flow_offload.c — bypass 조건 검사 */
static bool nft_flow_offload_skip(struct sk_buff *skb, int family)
{
if (skb_sec_path(skb)) /* IPSec 경로 */
return true;
if (nf_is_loopback_packet(skb)) /* 루프백 패킷 */
return true;
switch (family) {
case NFPROTO_IPV4: {
const struct iphdr *iph = ip_hdr(skb);
/* 단편화 패킷 */
if (iph->frag_off & htons(IP_MF | IP_OFFSET))
return true;
/* 멀티캐스트 */
if (ipv4_is_multicast(iph->daddr))
return true;
break;
}
case NFPROTO_IPV6: {
const struct ipv6hdr *ip6h = ipv6_hdr(skb);
if (ipv6_addr_is_multicast(&ip6h->daddr))
return true;
break;
}
}
return false;
}
/* conntrack helper 및 상태 검사 */
static bool nft_flow_offload_allow(const struct nf_conn *ct,
enum ip_conntrack_dir dir)
{
/* NAT helper가 붙어있으면 오프로드 금지 */
if (nfct_help(ct))
return false;
/* ESTABLISHED 상태가 아니면 오프로드 금지 */
if (ct->proto.tcp.state != TCP_CONNTRACK_ESTABLISHED)
return false;
/* conntrack이 dying 상태면 오프로드 금지 */
if (nf_ct_is_dying(ct))
return false;
return true;
}
bypass 빈도 모니터링
# conntrack -S 출력 해석 (bypass 빈도 모니터링)
conntrack -S
# 출력 예시:
# cpu=0 found=15234821 invalid=0 ignore=0 insert=0 insert_failed=0
# drop=0 early_drop=0 error=0 search_restart=4
# 핵심 지표:
# found : flowtable fast path 히트 (높을수록 좋음)
# insert : 신규 conntrack 항목 (slow path 신규 연결)
# search_restart: rhashtable 재조회 (resizing 중 발생)
# OFFLOAD 세션 비율 계산
TOTAL=$(conntrack -L "num">2>/dev/null | wc -l)
OFFLOADED=$(conntrack -L "num">2>/dev/null | grep -c OFFLOAD)
echo "OFFLOAD 비율: $((OFFLOADED * ">100 / TOTAL))% ($OFFLOADED / $TOTAL)"
# bypass 원인 별 카운팅 (bpftrace)
bpftrace -e '
kprobe:nft_flow_offload_eval {
@total = count();
}
kprobe:nft_flow_offload_skip {
@skipped = count();
}
interval:s:"num">5 {
printf("전체=%d 스킵=%d (bypass율 ~%d%%)\n",
@total, @skipped,
@total > "num">0 ? @skipped * "num">100 / @total : "num">0);
clear(@total); clear(@skipped);
}
'
nftables flowtable 설정 실전
nftables에서 flowtable을 사용하는 방법을 단계별로 설명합니다.
flowtable은 table 내에서 flowtable 블록으로 정의하고,
forward chain에서 flow add @이름 표현식으로 연결을 등록합니다.
기본 SW 오프로드 설정
# /etc/nftables/flowtable.conf
table inet filter {
# flowtable 정의: ingress hook, 처리할 장치 목록
flowtable ft {
hook ingress priority "num">0 # 우선순위 "num">0 = filter보다 먼저 실행
devices = { eth0, eth1 } # WAN/LAN 인터페이스 양쪽 모두 등록 필수
}
chain forward {
type filter hook forward priority "num">0; policy drop;
# ESTABLISHED/RELATED 연결을 flowtable로 오프로드
# 첫 패킷은 conntrack을 통해 일반 경로로 처리됨
ip protocol { tcp, udp } flow add @ft
# ESTABLISHED 연결 허용 (flowtable 미등록 패킷 대비)
ct state established,related accept
# LAN -> WAN 신규 연결 허용
iifname "eth1" oifname "eth0" ct state new accept
}
chain input {
type filter hook input priority "num">0; policy accept;
}
}
table ip nat {
chain postrouting {
type nat hook postrouting priority "num">100;
oifname "eth0" masquerade
}
}
HW 오프로드 활성화 설정
# 1. NIC TC offload 활성화
ethtool -K eth0 hw-tc-offload on
ethtool -K eth1 hw-tc-offload on
# 2. Mellanox eSwitch switchdev 모드 전환 (SR-IOV 환경, 선택 사항)
devlink dev eswitch set pci/"num">0000:"num">03:"num">00.0 mode switchdev
# 3. HW 오프로드 nftables 설정
# /etc/nftables/flowtable-hw.conf
table inet filter {
flowtable ft {
hook ingress priority "num">0
devices = { eth0, eth1 }
flags offload # HW 오프로드 활성화
}
chain forward {
type filter hook forward priority "num">0; policy drop;
# 'flow offload' 키워드 사용 (HW 오프로드 명시)
ip protocol { tcp, udp } flow offload @ft
ct state established,related accept
iifname "eth1" oifname "eth0" ct state new accept
}
}
# 4. 설정 검증 및 적용
nft -c -f /etc/nftables/flowtable-hw.conf # 문법 검증
nft -f /etc/nftables/flowtable-hw.conf # 적용
# 5. flowtable 상태 확인
nft list flowtable inet filter ft
IPv4 + IPv6 동시 지원
table inet filter {
flowtable ft {
hook ingress priority "num">0
devices = { eth0, eth1 }
}
chain forward {
type filter hook forward priority "num">0; policy drop;
# IPv4 TCP/UDP 오프로드
ip protocol { tcp, udp } flow add @ft
# IPv6 TCP/UDP 오프로드
ip6 nexthdr { tcp, udp } flow add @ft
ct state established,related accept
# ICMPv6는 flowtable을 우회하므로 명시적으로 허용
ip6 nexthdr icmpv6 accept
# 신규 연결 허용
iifname "eth1" oifname "eth0" ct state new accept
}
}
Flowtable + NAT 연동 심화
Flowtable과 NAT(Network Address Translation)가 함께 사용될 때의 패킷 처리 경로를 이해하는 것은 실제 라우터·방화벽 환경에서 매우 중요합니다. NAT가 적용된 연결도 Flowtable에 등록될 수 있으며, 이 경우 변환된 주소 정보가 플로우 튜플에 미리 저장되어 fast path에서 재사용됩니다.
첫 번째 패킷: NAT 정보 수집 및 등록
/* 첫 번째 패킷 처리 흐름 (slow path):
*
* NIC RX
* └─ ip_rcv()
* └─ NF_HOOK(PREROUTING)
* └─ nf_conntrack_in() ← conntrack NEW 상태 생성
* └─ DNAT 처리 (있는 경우): nf_nat_packet()
* └─ ip_forward()
* └─ NF_HOOK(FORWARD)
* └─ nft_do_chain() ← "flow add @ft" 표현식 평가
* └─ nft_flow_offload_eval()
* ├─ nft_flow_offload_allow() — helper/상태 검사
* ├─ flow_offload_alloc(ct) — ct에서 flow 생성
* │ ├─ NAT 정보 복사: ct->tuplehash[REPLY]
* │ │ → flow->tuplehash[ORIGINAL].tuple.nat_*
* │ └─ NF_FLOW_SNAT / NF_FLOW_DNAT 플래그 설정
* └─ flow_offload_add(flowtable, flow)
* └─ NF_HOOK(POSTROUTING)
* └─ SNAT 처리 (masquerade 등): nf_nat_packet()
* └─ NIC TX
*/
/* flow_offload_alloc() 내부: NAT 정보를 flow_offload에 복사 */
struct flow_offload *flow_offload_alloc(struct nf_conn *ct)
{
struct flow_offload *flow;
struct nf_conntrack_tuple *tuple_orig, *tuple_reply;
flow = kzalloc(sizeof(*flow), GFP_ATOMIC);
if (!flow)
return NULL;
tuple_orig = &ct->tuplehash[IP_CT_DIR_ORIGINAL].tuple;
tuple_reply = &ct->tuplehash[IP_CT_DIR_REPLY].tuple;
/* ORIGINAL 방향 튜플 설정 */
flow->tuplehash[FLOW_OFFLOAD_DIR_ORIGINAL].tuple.src_v4 =
tuple_orig->src.u3.in;
flow->tuplehash[FLOW_OFFLOAD_DIR_ORIGINAL].tuple.dst_v4 =
tuple_orig->dst.u3.in;
/* SNAT 감지: reply의 dst ≠ original의 src */
if (!nf_inet_addr_cmp(&tuple_reply->dst.u3,
&tuple_orig->src.u3)) {
flow->flags |= NF_FLOW_SNAT;
/* ORIGINAL 방향: SNAT 변환 후 주소 저장 */
flow->tuplehash[FLOW_OFFLOAD_DIR_ORIGINAL].tuple.nat_src =
tuple_reply->dst.u3.in;
}
/* DNAT 감지: reply의 src ≠ original의 dst */
if (!nf_inet_addr_cmp(&tuple_reply->src.u3,
&tuple_orig->dst.u3)) {
flow->flags |= NF_FLOW_DNAT;
/* ORIGINAL 방향: DNAT 변환 후 주소 저장 */
flow->tuplehash[FLOW_OFFLOAD_DIR_ORIGINAL].tuple.nat_dst =
tuple_reply->src.u3.in;
}
return flow;
}
이후 패킷: Fast Path에서 NAT 적용
/* net/netfilter/nf_flow_table_ip.c — fast path NAT 적용 */
static void nf_flow_nat_ip(const struct flow_offload *flow,
struct sk_buff *skb,
enum flow_offload_tuple_dir dir)
{
struct iphdr *iph = ip_hdr(skb);
/* ORIGINAL 방향 패킷: SNAT 변환 (출발지 주소 변경) */
if (dir == FLOW_OFFLOAD_DIR_ORIGINAL &&
flow->flags & NF_FLOW_SNAT) {
csum_replace4(&iph->check, iph->saddr,
flow->tuplehash[dir].tuple.nat_src.ip);
iph->saddr = flow->tuplehash[dir].tuple.nat_src.ip;
/* L4 체크섬도 갱신 */
nf_flow_snat_port(flow, skb, iph->protocol, dir);
}
/* ORIGINAL 방향 패킷: DNAT 변환 (목적지 주소 변경) */
if (dir == FLOW_OFFLOAD_DIR_ORIGINAL &&
flow->flags & NF_FLOW_DNAT) {
csum_replace4(&iph->check, iph->daddr,
flow->tuplehash[dir].tuple.nat_dst.ip);
iph->daddr = flow->tuplehash[dir].tuple.nat_dst.ip;
nf_flow_dnat_port(flow, skb, iph->protocol, dir);
}
/* REPLY 방향 패킷: 역방향 NAT (응답 패킷에도 동일 변환) */
if (dir == FLOW_OFFLOAD_DIR_REPLY) {
if (flow->flags & NF_FLOW_SNAT) {
/* SNAT의 역방향: 목적지가 원래 출발지로 */
csum_replace4(&iph->check, iph->daddr,
flow->tuplehash[FLOW_OFFLOAD_DIR_ORIGINAL]
.tuple.src_v4.ip);
iph->daddr = flow->tuplehash[FLOW_OFFLOAD_DIR_ORIGINAL]
.tuple.src_v4.ip;
}
if (flow->flags & NF_FLOW_DNAT) {
/* DNAT의 역방향: 출발지가 원래 목적지로 */
csum_replace4(&iph->check, iph->saddr,
flow->tuplehash[FLOW_OFFLOAD_DIR_ORIGINAL]
.tuple.dst_v4.ip);
iph->saddr = flow->tuplehash[FLOW_OFFLOAD_DIR_ORIGINAL]
.tuple.dst_v4.ip;
}
}
}
NF_FLOW_SNAT / NF_FLOW_DNAT 플래그
| 플래그 | 값 | 의미 | 적용 조건 |
|---|---|---|---|
NF_FLOW_SNAT |
BIT(0) | 출발지 NAT(SNAT/masquerade) 적용 | conntrack reply tuple의 dst ≠ original src |
NF_FLOW_DNAT |
BIT(1) | 목적지 NAT(DNAT/port-forward) 적용 | conntrack reply tuple의 src ≠ original dst |
NF_FLOW_DYING |
BIT(2) | 플로우 만료 진행 중 | GC teardown 호출 시 |
NF_FLOW_HW |
BIT(3) | HW 오프로드 등록 완료 | ndo_flow_offload(REPLACE) 성공 시 |
NF_FLOW_HW_DYING |
BIT(4) | HW 오프로드 해제 진행 중 | flow_offload_teardown() + HW 등록된 경우 |
CGNAT 환경과 NAT helper 우회 이유
CGNAT(Carrier-Grade NAT) 환경에서는 수십만~수백만 개의 세션에 SNAT가 적용됩니다. 각 세션의 변환 주소·포트가 flow_offload_tuple에 개별 저장되므로 CGNAT 환경에서도 Flowtable이 정상 동작합니다. 단, 아래 조건에서 NAT helper가 있는 연결은 반드시 slow path를 사용합니다.
- FTP ALG: PORT 명령의 페이로드에 있는 IP:PORT를 동적으로 수정해야 함
- SIP ALG: SDP 본문의 Contact/Via 헤더에 IP 주소가 포함됨
- H.323 ALG: 제어 채널에서 미디어 채널 주소를 협상함
- PPTP ALG: GRE 터널 협상에 별도 Call ID 추적 필요
이러한 ALG(Application Layer Gateway) 연결은 nfct_help(ct)가 NULL이 아니므로
nft_flow_offload_allow()에서 즉시 거부됩니다. 해당 세션은 항상 slow path에서 처리됩니다.
역방향 경로 캐싱 메커니즘
Flowtable의 중요한 특징 중 하나는 양방향 튜플을 동시에 등록한다는 점입니다. ORIGINAL 방향(클라이언트→서버)과 REPLY 방향(서버→클라이언트) 모두 rhashtable에 등록되어 어느 방향의 패킷이 와도 fast path로 처리됩니다.
역방향 튜플 동시 등록
/* net/netfilter/nf_flow_table_core.c */
/* flow_offload_add()에서 양방향 튜플 모두 등록 */
int flow_offload_add(struct nf_flowtable *flow_table,
struct flow_offload *flow)
{
int err;
flow->timeout = nf_flowtable_time_stamp() + NF_FLOW_TIMEOUT;
/* ORIGINAL 방향: 클라이언트 → 서버 */
err = rhashtable_insert_fast(
&flow_table->rhashtable,
&flow->tuplehash[FLOW_OFFLOAD_DIR_ORIGINAL].node,
nf_flow_offload_rhash_params);
if (err < 0)
return err;
/* REPLY 방향: 서버 → 클라이언트 (역방향 경로 캐싱) */
err = rhashtable_insert_fast(
&flow_table->rhashtable,
&flow->tuplehash[FLOW_OFFLOAD_DIR_REPLY].node,
nf_flow_offload_rhash_params);
if (err < 0) {
/* ORIGINAL 등록 롤백 */
rhashtable_remove_fast(
&flow_table->rhashtable,
&flow->tuplehash[FLOW_OFFLOAD_DIR_ORIGINAL].node,
nf_flow_offload_rhash_params);
return err;
}
/* HW 오프로드 요청 (비동기) */
if (nf_flowtable_hw_offload(flow_table))
nf_flow_offload_work_alloc(flow_table, flow, FLOW_CLS_REPLACE);
return 0;
}
nf_flow_route_nexthop() — 라우팅 정보 캐싱
/* net/netfilter/nft_flow_offload.c */
/* conntrack 경로에서 next-hop 정보를 flow_offload_tuple에 캐싱 */
static int nf_flow_route_nexthop(struct flow_offload_tuple *tuple,
const struct dst_entry *dst,
int family)
{
/* dst_entry 참조 카운트 증가 (플로우 유효 기간 동안 유지) */
dst_hold((struct dst_entry *)dst);
tuple->dst_cache = (struct dst_entry *)dst;
/* 출력 인터페이스 인덱스 캐싱 */
tuple->oifidx = dst->dev->ifindex;
/* IPv4/IPv6에 따른 next-hop 주소 캐싱 */
if (family == NFPROTO_IPV4) {
const struct rtable *rt = (const struct rtable *)dst;
struct neighbour *n;
/* ARP로 해결된 next-hop MAC 주소 캐싱 */
n = dst_neigh_lookup(dst,
&rt->rt_gateway.s_addr ?
&rt->rt_gateway : &ip_hdr(NULL)->daddr);
if (n) {
ether_addr_copy(tuple->dst_mac, n->ha);
ether_addr_copy(tuple->src_mac, dst->dev->dev_addr);
neigh_release(n);
}
}
/* MTU 캐싱 — PMTU Discovery와 연동 */
tuple->mtu = dst_mtu(dst);
return 0;
}
/* 라우팅 테이블 변경 시 Flowtable 무효화 */
/* net/netfilter/nf_flow_table_core.c */
void nf_flow_table_gc_run(struct nf_flowtable *flow_table)
{
/* dst_entry obsolete 검사 — route change notifier에 의해 트리거 */
nf_flow_table_iterate(flow_table, nf_flow_table_do_gc, NULL);
}
/* dst_entry가 obsolete이면 플로우 teardown */
static void nf_flow_table_do_gc(struct nf_flowtable *flow_table,
struct flow_offload *flow,
void *data)
{
struct flow_offload_tuple *orig_tuple, *reply_tuple;
orig_tuple = &flow->tuplehash[FLOW_OFFLOAD_DIR_ORIGINAL].tuple;
reply_tuple = &flow->tuplehash[FLOW_OFFLOAD_DIR_REPLY].tuple;
/* 어느 방향이든 dst가 만료되면 플로우 무효화 */
if (dst_is_expired(orig_tuple->dst_cache) ||
dst_is_expired(reply_tuple->dst_cache)) {
flow_offload_teardown(flow);
}
}
ECMP 멀티패스 라우팅에서의 역방향 경로
ECMP(Equal-Cost Multi-Path) 환경에서 Flowtable은 첫 번째 패킷이 선택한 경로를 캐싱합니다. 이후 패킷은 항상 동일한 경로로 전달되므로 ECMP의 부하 분산 효과가 사라지지만 세션 일관성(session persistence)이 보장됩니다.
| 환경 | tuple 필드 | 특수 처리 |
|---|---|---|
| VLAN 태그 있음 | encap[0].id — VLAN ID |
ingress에서 VLAN 헤더 strip 후 룩업, egress에서 재삽입 |
| QinQ (이중 VLAN) | encap[0..1].id |
최대 2단계 VLAN 캐싱 지원 |
| Bridge 포트 | iifidx — bridge 물리 포트 |
bridge forward DB 우회, 직접 포트로 전달 |
| PPPoE | encap[0].proto = ETH_P_PPP_SES |
PPPoE 헤더 encap/decap 처리 |
| ECMP | dst_cache — 첫 선택 경로 고정 |
세션 일관성 보장, ECMP 효과 없음 |
GC(Garbage Collection) 메커니즘
Flowtable GC는 만료된 플로우를 주기적으로 정리하는 메커니즘입니다. GC가 없으면 TCP 종료 이후에도 플로우가 rhashtable에 남아 메모리를 낭비하고, 새로운 연결이 같은 5-튜플을 재사용할 때 충돌이 발생합니다.
TCP 상태 기반 GC 트리거
/* net/netfilter/nf_flow_table_core.c */
/* GC 워크 함수 — system_power_efficient_wq에서 주기적으로 실행 */
static void nf_flow_offload_gc_step(struct nf_flowtable *flow_table,
struct flow_offload *flow,
void *data)
{
/* 1. timeout 만료 검사 */
if (nf_flow_has_expired(flow)) {
/* 30초 idle → teardown */
flow_offload_teardown(flow);
goto check_dying;
}
/* 2. 연관 conntrack이 삭제된 경우 */
if (nf_ct_is_dying(flow_offload_ct(flow))) {
flow_offload_teardown(flow);
goto check_dying;
}
return;
check_dying:
/* dying 상태이면 rhashtable에서 제거 */
if (nf_flow_is_dying(flow)) {
/* HW 오프로드 해제 (있는 경우) */
if (flow->flags & NF_FLOW_HW)
nf_flow_offload_work_alloc(flow_table, flow,
FLOW_CLS_DESTROY);
/* rhashtable에서 양방향 튜플 모두 제거 */
flow_offload_del(flow_table, flow);
}
}
/* TCP FIN/RST 감지 — fast path에서 직접 호출 */
static bool nf_flow_tcp_state_check(struct sk_buff *skb,
struct flow_offload *flow,
enum flow_offload_tuple_dir dir)
{
const struct tcphdr *th;
u8 flags;
/* TCP 헤더 접근 (skb linear 영역 검사) */
if (!pskb_may_pull(skb, skb_transport_offset(skb) + sizeof(*th)))
return false;
th = tcp_hdr(skb);
flags = tcp_flag_byte(th);
/* RST: 즉시 teardown */
if (flags & TCPHDR_RST) {
flow_offload_teardown(flow);
return true; /* slow path에서 RST 처리 */
}
/* FIN: TIME_WAIT 진입 준비 — timeout을 짧게 설정 */
if (flags & TCPHDR_FIN) {
/* FIN 이후 짧은 타임아웃 (5초) 적용 */
flow->timeout = nf_flowtable_time_stamp() + HZ * 5;
flow_offload_teardown(flow);
return true; /* slow path에서 FIN 처리 */
}
return false;
}
/* GC 워크 스케줄링 — 2초 주기 */
static void nf_flow_offload_work_gc(struct work_struct *work)
{
struct nf_flowtable *flow_table;
flow_table = container_of(work, struct nf_flowtable,
gc_work.work);
/* 모든 플로우를 순회하며 GC 검사 */
nf_flow_table_iterate(flow_table, nf_flow_offload_gc_step, NULL);
/* 2초 후 다시 실행 */
queue_delayed_work(system_power_efficient_wq,
&flow_table->gc_work, HZ * 2);
}
HW 오프로드 플로우의 GC: 통계 폴링
HW 오프로드된 플로우는 CPU를 통하지 않으므로 패킷이 흘러도 SW 쪽의 flow->timeout이 갱신되지 않습니다.
이를 해결하기 위해 GC 워크는 주기적으로 NIC 드라이버에서 패킷 카운터를 읽어 timeout을 갱신합니다.
/* HW 오프로드 통계 폴링 흐름:
*
* nf_flow_offload_gc_step()
* └─ flow->flags & NF_FLOW_HW 확인
* └─ nf_flow_offload_work_alloc(FLOW_CLS_STATS)
* └─ 워크큐: nf_flow_offload_work()
* └─ nf_flow_offload_stats()
* └─ tc_setup_cb_call(TC_SETUP_CLSFLOWER)
* └─ NIC 드라이버: ndo_flow_offload(FLOW_CLS_STATS)
* └─ cls_flow.stats.pkts / bytes / lastused
* └─ 패킷 있으면: flow->timeout 갱신
*/
/* nft flowtable timeout 설정 (nftables "num">0.9."num">6+) */
/* /etc/nftables.conf */
/*
* table inet filter {
* flowtable ft {
* hook ingress priority "num">0
* devices = { eth0, eth1 }
* # timeout은 현재 nftables에서 직접 설정 불가
* # 커널 내부: NF_FLOW_TIMEOUT = "num">30 * HZ
* }
* }
*/
플로우 테이블 메모리 사용량 모니터링
# 현재 flowtable 세션 수 확인
conntrack -L | grep -c OFFLOAD
# 전체 conntrack 테이블 사용량
conntrack -L | wc -l
sysctl net.netfilter.nf_conntrack_count
sysctl net.netfilter.nf_conntrack_max
# flow_offload 구조체 메모리 계산:
# 세션 1개 = struct flow_offload (~512 bytes)
# + 2x struct flow_offload_tuple_rhash (~256 bytes each)
# = 약 1KB/세션
# 10만 세션 ≈ 100MB
# /proc/slabinfo에서 nf_flow_table 슬랩 확인
grep "nf_flow" /proc/slabinfo "num">2>/dev/null || \
grep "flow_offload" /proc/slabinfo "num">2>/dev/null
# debugfs로 flowtable 상태 확인 (커널 버전 의존)
ls /sys/kernel/debug/netfilter/ "num">2>/dev/null
VLAN/Bridge 환경 심화
Flowtable은 단순한 L3 라우팅 환경뿐 아니라 VLAN 태깅, Linux bridge, macvlan 환경에서도 동작할 수 있습니다. 단, 각 환경에서 flow_offload_tuple에 추가 정보가 필요하며 설정 방법이 달라집니다.
VLAN 태그와 flow tuple encap 필드
/* include/net/netfilter/nf_flow_table.h */
/* VLAN encap 정보 — 최대 2단계 (QinQ) 지원 */
struct flow_offload_tuple {
/* ... 기본 L3/L4 필드 ... */
/* VLAN encap: 최대 2개 (외부/내부 VLAN) */
struct {
u16 id; /* VLAN ID (0이면 미사용) */
__be16 proto; /* ETH_P_8021Q 또는 ETH_P_8021AD */
} encap[NF_FLOW_TABLE_ENCAP_MAX]; /* NF_FLOW_TABLE_ENCAP_MAX = 2 */
/* in_vlan_ingress: VLAN 태그 처리 방향 */
u8 in_vlan_ingress;
};
Bridge + Flowtable 설정
# Linux bridge + VLAN filtering + Flowtable 조합
# 시나리오: br0 브리지 아래 eth0(WAN), eth1(LAN)
# VLAN 100 = LAN 서브넷, VLAN 200 = DMZ
# 1. Bridge 생성 및 VLAN filtering 활성화
ip link add name br0 type bridge vlan_filtering "num">1
ip link set eth0 master br0
ip link set eth1 master br0
ip link set br0 up
ip link set eth0 up
ip link set eth1 up
# 2. VLAN 할당
bridge vlan add dev eth1 vid "num">100 pvid untagged # LAN: VLAN "num">100 언태그
bridge vlan add dev eth0 vid "num">200 pvid untagged # DMZ: VLAN "num">200 언태그
# 3. Bridge flowtable nftables 설정
# /etc/nftables/bridge-flowtable.conf
#
# table bridge filter {
# flowtable ft {
# hook ingress priority 0
# devices = { eth0, eth1 } # bridge 물리 포트 직접 지정
# }
#
# chain forward {
# type filter hook forward priority 0; policy drop;
# meta l4proto { tcp, udp } flow add @ft
# ct state established,related accept
# ct state new accept
# }
# }
#
# 주의: bridge flowtable은 L2 포워딩을 우회하므로
# iptables -t broute를 사용하는 환경에서는 주의가 필요합니다.
# 4. 설정 적용
nft -f /etc/nftables/bridge-flowtable.conf
macvlan/ipvlan 환경
# macvlan 환경에서의 Flowtable
# macvlan 인터페이스도 devices 목록에 추가 가능
# macvlan 인터페이스 생성
ip link add link eth0 name macvlan0 type macvlan mode bridge
ip link set macvlan0 up
# nftables flowtable에 macvlan 포함
# flowtable ft {
# hook ingress priority 0
# devices = { eth0, macvlan0 } # 부모+macvlan 모두 등록
# }
# ipvlan은 현재 Flowtable과 완전 호환되지 않을 수 있음
# (ingress hook 처리 방식 차이로 인해 테스트 필요)
OVS(Open vSwitch)와 Flowtable 비교
| 특성 | OVS Megaflow | Linux Flowtable |
|---|---|---|
| 동작 계층 | L2~L4 (OpenFlow 기반) | L3~L4 (Netfilter 기반) |
| 룩업 방식 | 분류 트리 (tuple space search) | rhashtable 5-튜플 정확 매칭 |
| HW 오프로드 | TC flower (OVS-TC) | ndo_flow_offload (nf_flow_table) |
| NAT 지원 | ct NAT action (제한적) | nf_nat 완전 통합 |
| 설정 인터페이스 | ovs-vsctl / OpenFlow | nftables / iptables |
| 컨테이너 환경 | Kubernetes CNI (Calico OVS 등) | 일반 Linux 네트워크 네임스페이스 |
| conntrack 통합 | OVS conntrack action | nf_conntrack 완전 통합 |
IPv6 Flowtable 심화
Flowtable은 IPv4뿐 아니라 IPv6도 지원합니다. AF_INET6 패밀리를 위한 별도 hook 함수가
구현되어 있으며, IPv6 특유의 Extension Header 처리, PMTU Discovery, NAT64 환경에서의
동작을 이해하는 것이 중요합니다.
nf_flow_offload_ipv6_hook() 분석
/* net/netfilter/nf_flow_table_ip.c */
/* IPv6 fast path hook */
static unsigned int nf_flow_offload_ipv6_hook(void *priv,
struct sk_buff *skb,
const struct nf_hook_state *state)
{
struct flow_offload_tuple_rhash *tuplehash;
struct nf_flowtable *flow_table = priv;
struct flow_offload_tuple tuple = {};
const struct ipv6hdr *ip6h;
struct flow_offload *flow;
enum flow_offload_tuple_dir dir;
/* IPv6 헤더 검증 */
if (skb->protocol != htons(ETH_P_IPV6))
return NF_ACCEPT;
ip6h = ipv6_hdr(skb);
/* Extension Header 검사 — 복잡한 경우 slow path */
switch (ip6h->nexthdr) {
case IPPROTO_TCP:
case IPPROTO_UDP:
break; /* 지원하는 프로토콜 */
default:
/* Hop-by-Hop, Routing, Destination Options 등 → slow path */
return NF_ACCEPT;
}
/* 멀티캐스트/링크로컬 주소 필터링 */
if (ipv6_addr_is_multicast(&ip6h->daddr) ||
ipv6_addr_type(&ip6h->saddr) & IPV6_ADDR_LINKLOCAL)
return NF_ACCEPT; /* slow path */
/* 5-튜플 추출 */
nf_flow_tuple_ipv6(skb, state->in, &tuple, &dir);
/* rhashtable 조회 */
tuplehash = flow_offload_lookup(flow_table, &tuple);
if (!tuplehash)
return NF_ACCEPT; /* 미스 → slow path */
flow = container_of(tuplehash, struct flow_offload,
tuplehash[tuplehash->tuple.dir]);
/* TCP 상태 검사 */
if (nf_flow_tcp_state_check(skb, flow, tuplehash->tuple.dir))
return NF_ACCEPT;
/* IPv6 NAT 재작성 (있는 경우) */
nf_flow_nat_ipv6(flow, skb, tuplehash->tuple.dir);
/* Hop Limit 감소 (IPv4의 TTL에 해당) */
ip6h = ipv6_hdr(skb);
if (ip6h->hop_limit <= 1) {
/* ICMPv6 Time Exceeded 생성 필요 → slow path */
return NF_ACCEPT;
}
ip6h->hop_limit--;
/* 직접 전달 */
skb_dst_set_noref(skb, tuplehash->tuple.dst_cache);
return NF_STOLEN;
}
IPv6 Extension Header 처리 한계
| Extension Header | Next Header 값 | Flowtable 처리 | 이유 |
|---|---|---|---|
| Hop-by-Hop Options | 0 | slow path 강제 | 모든 라우터가 처리해야 함 |
| Routing Header (type 0) | 43 | slow path 강제 | 경로 변경 가능 (보안 위험) |
| Fragment Header | 44 | slow path 강제 | 5-튜플 추출 불가 (단편화) |
| AH (Authentication Header) | 51 | slow path 강제 | IPsec 처리 필요 |
| ESP (Encapsulating Security Payload) | 50 | slow path 강제 | IPsec 처리 필요 |
| Destination Options | 60 | slow path 강제 | 목적지 노드 처리 필요 |
| 없음 (TCP/UDP 직접) | 6/17 | fast path 가능 | 표준 5-튜플 추출 가능 |
IPv6 PMTU Discovery와 Flowtable
IPv6에서는 라우터가 패킷 단편화를 수행하지 않습니다. 대신 발신측이 PMTU Discovery를 통해 경로 MTU를 파악해야 합니다. Flowtable fast path에서 MTU 초과 패킷을 받으면 slow path로 보내 ICMPv6 "Packet Too Big" 메시지를 생성합니다.
# IPv6 Flowtable nftables 설정 예제
table inet filter {
flowtable ft {
hook ingress priority "num">0
devices = { eth0, eth1 }
}
chain forward {
type filter hook forward priority "num">0; policy drop;
# IPv4 TCP/UDP 오프로드
ip protocol { tcp, udp } flow add @ft
# IPv6 TCP/UDP 오프로드 (extension header 없는 경우만 실제 가속)
ip6 nexthdr { tcp, udp } flow add @ft
# ICMPv6 neighbor discovery 등 허용 (flowtable 우회)
ip6 nexthdr icmpv6 accept
ct state established,related accept
iifname "eth1" oifname "eth0" ct state new accept
}
}
# IPv6 flowtable 세션 확인
conntrack -L -f ipv6 | grep OFFLOAD
# IPv6 MTU 확인 (PMTU Discovery 결과)
ip -"num">6 route show cache
NAT64/NAT46 환경에서의 Flowtable
NAT64는 IPv6 전용 클라이언트가 IPv4 서버에 접근할 수 있게 하는 기술입니다.
Linux에서는 jool 또는 nf_nat_ipv6을 사용합니다.
NAT64 환경에서 Flowtable은 다음 제약이 있습니다.
- AF 변환(IPv6→IPv4)이 필요한 연결: L3 프로토콜이 변환되므로
flow_offload_tuple의l3proto필드가 일관되게 유지되지 않아 현재 Flowtable이 직접 지원하지 않습니다. - Pure IPv6 또는 Pure IPv4 구간: NAT64 게이트웨이의 내부 인터페이스(IPv6 only)와 외부 인터페이스(IPv4 only)를 별도 flowtable로 각각 가속할 수 있습니다.
- 6to4/6in4 터널: 터널 encap/decap이 추가되므로 inner 헤더 파싱이 불가능하여 slow path로 처리됩니다.
# 6in4 터널 환경에서의 Flowtable 동작 확인
# (터널 패킷은 OFFLOAD 되지 않으므로 slow path 예상)
ip tunnel add tun0 mode sit remote "num">203.0."num">113.1 local "num">198.51."num">100.1
ip link set tun0 up
# tun0를 flowtable devices에 추가해도 inner 패킷은 오프로드 안 됨
# → tunnel encap 헤더 파싱 미지원으로 slow path 처리
# 확인: OFFLOAD 세션이 tun0 관련 세션 없음
conntrack -L | grep OFFLOAD
커널 소스 구조
Flowtable 구현은 net/netfilter/ 디렉터리에 집중되어 있습니다.
주요 파일과 역할을 정리합니다.
| 파일 경로 | 역할 | 주요 함수/심볼 |
|---|---|---|
net/netfilter/nf_flow_table_core.c |
flowtable 핵심 (rhashtable, GC, 플로우 생명주기) | flow_offload_alloc, flow_offload_add, flow_offload_del |
net/netfilter/nf_flow_table_ip.c |
IPv4/IPv6 fast path hook 구현 | nf_flow_offload_ip_hook, nf_flow_offload_ipv6_hook |
net/netfilter/nf_flow_table_offload.c |
HW 오프로드 TC flower 연동 | nf_flow_offload_work, nf_flow_offload_hw |
net/netfilter/nft_flow_offload.c |
nftables flow add 표현식 구현 |
nft_flow_offload_eval, nft_flow_offload_init |
include/net/netfilter/nf_flow_table.h |
핵심 자료구조 정의 | nf_flowtable, flow_offload, flow_offload_tuple |
net/netfilter/nf_flow_table_inet.c |
inet 패밀리 flowtable 타입 등록 | nf_flow_inet_module_init |
/* GC 메커니즘: net/netfilter/nf_flow_table_core.c */
/* GC 워커: 30초 주기로 만료 항목 정리 */
static void nf_flow_offload_gc_step(struct nf_flowtable *flow_table,
struct flow_offload *flow,
void *data)
{
/* timeout 만료 또는 conntrack 삭제 시 teardown */
if (nf_flow_has_expired(flow) ||
nf_ct_is_dying(flow_offload_ct(flow))) {
flow_offload_teardown(flow);
}
/* dying 상태이면 rhashtable에서 제거 후 RCU free */
if (nf_flow_is_dying(flow))
flow_offload_del(flow_table, flow);
}
/* TCP 종료 감지: FIN/RST 수신 시 플로우 만료 */
static bool nf_flow_tcp_check(struct sk_buff *skb,
struct flow_offload *flow)
{
const struct tcphdr *th = tcp_hdr(skb);
if (th->fin || th->rst) {
/* FIN/RST 수신 시 플로우를 dying 상태로 표시 */
flow_offload_teardown(flow);
return true; /* slow path에서 연결 종료 처리 */
}
return false;
}
/* 플로우 시간 상수 */
#define NF_FLOW_TIMEOUT (30 * HZ) /* 30초 idle timeout */
진단 및 모니터링
Flowtable 동작 상태를 진단하는 방법을 정리합니다. fast path 적용 비율, HW 오프로드 등록 여부, 플로우 만료 패턴을 모니터링하면 성능 병목과 bypass 조건 위반을 빠르게 탐지할 수 있습니다.
기본 진단 명령
# 1. conntrack 통계 (fast/slow path 비율 확인)
conntrack -S
# found=X -> flowtable rhashtable 히트 횟수 (fast path)
# searched=X -> conntrack 전체 조회 횟수
# invalid=X -> 잘못된 패킷 (단편화, ICMP 오류 등)
# 2. flowtable 오프로드된 세션 목록 ([OFFLOAD] 플래그 확인)
conntrack -L | grep OFFLOAD
# 3. 오프로드 비율 계산
# OFFLOAD 항목수 / 전체 항목수 * 100 = fast path 비율
conntrack -L | grep OFFLOAD | wc -l
conntrack -L | wc -l
# 4. nftables flowtable 현재 상태 확인
nft list flowtable inet filter ft
# 5. flowtable 관련 커널 모듈 확인
lsmod | grep -E "nf_flow|nft_flow"
# nft_flow_offload -- nftables flow add 표현식
# nf_flow_table -- flowtable 코어
# nf_flow_table_inet -- inet 패밀리 지원
# 6. HW TC offload 지원 여부 확인
ethtool -k eth0 | grep hw-tc-offload
bpftrace를 이용한 심층 진단
# flow_offload_add() 호출 추적 (신규 플로우 등록 이벤트)
bpftrace -e '
kprobe:flow_offload_add {
printf("flow_offload_add: cpu=%d\n", cpu);
@add_count = count();
}
interval:s:"num">5 {
printf("5초간 플로우 등록 수: %d\n", @add_count);
clear(@add_count);
}
'
# nf_flow_offload_ip_hook 히트/미스 분석
bpftrace -e '
kretprobe:nf_flow_offload_ip_hook {
if (retval == "num">1) { // NF_DROP (미스 없음)
@slow_path = count();
} else if (retval == "num">4) { // NF_STOLEN (fast path)
@fast_path = count();
}
}
interval:s:"num">10 {
printf("fast_path=%d slow_path=%d\n", @fast_path, @slow_path);
clear(@fast_path); clear(@slow_path);
}
'
# TCP FIN/RST로 인한 플로우 만료 추적
bpftrace -e '
kprobe:flow_offload_teardown {
@teardown = count();
}
interval:s:"num">5 {
printf("플로우 만료(teardown): %d\n", @teardown);
clear(@teardown);
}
'
perf 기반 성능 분석
# flowtable 관련 함수 CPU 시간 프로파일링
perf record -g -F "num">999 -a -- sleep "num">30
perf report --sort comm,dso,symbol | grep -A5 "nf_flow"
# 캐시 미스 분석 (rhashtable 조회 효율)
perf stat -e LLC-load-misses,LLC-store-misses,cache-misses \
-p $(pgrep ksoftirqd) -- sleep "num">10
# 함수별 호출 횟수 (flat profile)
perf top -e cycles --sort symbol | grep -E "flow_offload|nf_flow"
# conntrack 통계 지속 모니터링
watch -n "num">2 'conntrack -S && echo "---" && conntrack -L | grep OFFLOAD | wc -l'
HW 오프로드 진단
# Mellanox ConnectX 하드웨어 플로우 테이블 통계
# TC flower 규칙 목록 (HW 오프로드된 플로우 확인)
tc filter show dev eth0 ingress
# mlx5 드라이버 통계
ethtool -S eth0 | grep -i "flow\|offload"
# devlink 포트 통계
devlink port show pci/"num">0000:"num">03:"num">00.0/"num">0
# SmartNIC 플로우 테이블 용량 확인 (드라이버 종속)
ethtool --show-features eth0 | grep offload
# nf_flowtable GC 주기 및 통계 (debugfs)
ls /sys/kernel/debug/netfilter/ "num">2>/dev/null
cat /proc/net/netfilter/nf_flowtable_stats "num">2>/dev/null || \
echo "커널 버전에 따라 파일명이 다를 수 있음"
관련 문서
Netfilter Flowtable과 관련된 다른 주제를 더 깊이 이해하고 싶다면 다음 문서를 참고하세요.