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 192.168.1.1 -P 32 -t 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 2>/dev/null | wc -l)
OFFLOADED=$(conntrack -L 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:5 {
printf("전체=%d 스킵=%d (bypass율 ~%d%%)\n",
@total, @skipped,
@total > 0 ? @skipped * 100 / @total : 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 0 # 우선순위 0 = filter보다 먼저 실행
devices = { eth0, eth1 } # WAN/LAN 인터페이스 양쪽 모두 등록 필수
}
chain forward {
type filter hook forward priority 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 0; policy accept;
}
}
table ip nat {
chain postrouting {
type nat hook postrouting priority 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/0000:03:00.0 mode switchdev
# 3. HW 오프로드 nftables 설정
# /etc/nftables/flowtable-hw.conf
table inet filter {
flowtable ft {
hook ingress priority 0
devices = { eth0, eth1 }
flags offload # HW 오프로드 활성화
}
chain forward {
type filter hook forward priority 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 0
devices = { eth0, eth1 }
}
chain forward {
type filter hook forward priority 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 0.9.6+) */
/* /etc/nftables.conf */
/*
* table inet filter {
* flowtable ft {
* hook ingress priority 0
* devices = { eth0, eth1 }
* # timeout은 현재 nftables에서 직접 설정 불가
* # 커널 내부: NF_FLOW_TIMEOUT = 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 2>/dev/null || \
grep "flow_offload" /proc/slabinfo 2>/dev/null
# debugfs로 flowtable 상태 확인 (커널 버전 의존)
ls /sys/kernel/debug/netfilter/ 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 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 100 pvid untagged # LAN: VLAN 100 언태그
bridge vlan add dev eth0 vid 200 pvid untagged # DMZ: VLAN 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 0
devices = { eth0, eth1 }
}
chain forward {
type filter hook forward priority 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 -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 203.0.113.1 local 198.51.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: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 == 1) { // NF_DROP (미스 없음)
@slow_path = count();
} else if (retval == 4) { // NF_STOLEN (fast path)
@fast_path = count();
}
}
interval:s: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:5 {
printf("플로우 만료(teardown): %d\n", @teardown);
clear(@teardown);
}
'
perf 기반 성능 분석
# flowtable 관련 함수 CPU 시간 프로파일링
perf record -g -F 999 -a -- sleep 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 10
# 함수별 호출 횟수 (flat profile)
perf top -e cycles --sort symbol | grep -E "flow_offload|nf_flow"
# conntrack 통계 지속 모니터링
watch -n 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/0000:03:00.0/0
# SmartNIC 플로우 테이블 용량 확인 (드라이버 종속)
ethtool --show-features eth0 | grep offload
# nf_flowtable GC 주기 및 통계 (debugfs)
ls /sys/kernel/debug/netfilter/ 2>/dev/null
cat /proc/net/netfilter/nf_flowtable_stats 2>/dev/null || \
echo "커널 버전에 따라 파일명이 다를 수 있음"
SW 오프로드 상세 패킷 경로
SW 오프로드 fast path에서 패킷이 수신되어 전달되기까지의 모든 세부 단계를 분석합니다. 각 단계에서 수행되는 구체적인 연산과 함수 호출을 이해하면 성능 병목 지점을 정확히 파악할 수 있습니다.
NF_STOLEN 반환과 상위 훅 스킵
NF_STOLEN은 Netfilter 프레임워크에서 "이 패킷은 내가 가져갔다"는 의미입니다.
이 반환값을 받은 nf_hook_slow()는 이후 등록된 훅을 모두 건너뛰고,
sk_buff에 대한 관리 책임도 호출자에게 넘기지 않습니다.
Flowtable hook이 NF_STOLEN을 반환하면 PREROUTING → FORWARD → POSTROUTING 훅이 전부 실행되지 않습니다.
/* net/netfilter/core.c — NF_STOLEN 처리 */
int nf_hook_slow(struct sk_buff *skb,
struct nf_hook_state *state,
const struct nf_hook_entries *e,
unsigned int s)
{
unsigned int verdict;
int ret;
for (; s < e->num_hook_entries; s++) {
verdict = nf_hook_entry_hookfn(&e->hooks[s], skb, state);
switch (verdict & NF_VERDICT_MASK) {
case NF_ACCEPT:
break; /* 다음 훅으로 진행 */
case NF_STOLEN:
return NF_STOLEN; /* 즉시 반환, 이후 훅 실행 안 함 */
case NF_DROP:
kfree_skb(skb);
return NF_DROP;
}
}
return 1;
}
dev_queue_xmit() 직접 전달 경로
일반 경로에서는 ip_output() → ip_finish_output() → ip_neigh_output()를 거쳐 전송하지만,
Flowtable fast path에서는 Ethernet 헤더를 직접 구성한 뒤 dev_queue_xmit()를 바로 호출합니다.
이는 neighbour subsystem 조회를 완전히 우회합니다.
| 단계 | 일반 경로 | Flowtable Fast Path | 절약 사이클 |
|---|---|---|---|
| Neighbour 조회 | neigh_output() → ARP 캐시 | 생략 (MAC 캐시) | ~200 cycles |
| IP 출력 처리 | ip_output() → ip_finish_output() | 생략 | ~150 cycles |
| Ethernet 헤더 | eth_header() via dev_hard_header() | ether_addr_copy() 직접 | ~50 cycles |
| Netfilter 출력 훅 | NF_HOOK(POSTROUTING) | 완전 생략 | ~500 cycles |
| GSO/GRO 처리 | validate_xmit_skb() | 동일 (dev_queue_xmit 내부) | 0 |
HW 오프로드 NIC 내부 처리 메커니즘
HW 오프로드가 활성화되면 SmartNIC의 ASIC/FPGA가 패킷을 직접 처리합니다. NIC 내부의 매치-액션(match-action) 파이프라인은 소프트웨어 Netfilter와는 근본적으로 다른 방식으로 동작합니다.
eSwitch 모드와 Flowtable
Mellanox ConnectX NIC에서 HW 오프로드를 사용하려면 eSwitch를 switchdev 모드로 전환해야 합니다.
이 모드에서 NIC의 내장 스위치가 Linux 커널의 bridge/TC 인프라와 통합됩니다.
| eSwitch 모드 | 설정 명령 | Flowtable HW 오프로드 | 용도 |
|---|---|---|---|
| legacy | devlink dev eswitch set pci/0000:03:00.0 mode legacy |
불가 | 일반 NIC 모드, SR-IOV |
| switchdev | devlink dev eswitch set pci/0000:03:00.0 mode switchdev |
가능 | TC flower HW offload, bridge offload |
# eSwitch switchdev 전환 절차 (Mellanox ConnectX-5+)
# 1. VF 생성 (SR-IOV 환경인 경우)
echo 0 > /sys/class/net/eth0/device/sriov_numvfs
echo 4 > /sys/class/net/eth0/device/sriov_numvfs
# 2. eSwitch switchdev 전환
devlink dev eswitch set pci/0000:03:00.0 mode switchdev
# 3. representor 인터페이스 확인
ip link show | grep "representor"
# eth0_0, eth0_1, ... (VF representor 포트)
# 4. HW TC offload 활성화
ethtool -K eth0 hw-tc-offload on
# 5. nftables flowtable에 flags offload 설정
nft add flowtable inet filter ft \
'{ hook ingress priority 0; devices = { eth0, eth1 }; flags offload; }'
# 6. HW 오프로드 확인
tc filter show dev eth0 ingress
# in_hw 또는 hw_count 필드가 있으면 HW에 등록됨
HW 오프로드 제한사항
| 제한 사항 | 이유 | 동작 |
|---|---|---|
| VLAN stacking (QinQ) 3단계 이상 | NIC ASIC이 2단계까지만 지원 | SW 오프로드로 fallback |
| GRE/VXLAN/Geneve 터널 내부 플로우 | inner 헤더 파싱 제한 | 일부 NIC만 지원 (CX-6 Dx+) |
| IPv6 Extension Header 포함 패킷 | 가변 길이 헤더 파싱 불가 | CPU에서 처리 |
| NAT port 범위 변환 | 1:1 매핑만 지원 | 정확 매칭만 오프로드 |
| FDB 규칙 용량 초과 | HW 테이블 가득 참 | SW 오프로드로 자동 fallback |
| conntrack helper 연결 (FTP ALG 등) | 페이로드 검사 필요 | 전체 slow path 처리 |
Flowtable 등록/bypass 판단 흐름
패킷이 Flowtable에 등록되거나 bypass되는 결정은 여러 단계에서 이루어집니다.
아래 플로우차트는 nft_flow_offload_eval()이 호출될 때부터 최종 등록/거부까지의 전체 판단 과정을 보여줍니다.
conntrack -F로 전체 세션을 초기화하거나 개별 세션을 삭제해야 합니다.
커널 버전별 Flowtable 진화
Flowtable은 커널 4.16에서 최초 도입된 이후 매 릴리스마다 기능이 확장되고 있습니다. 각 버전에서 추가된 핵심 기능과 변경 사항을 정리합니다.
| 커널 버전 | 시기 | 주요 변경 | 관련 커밋/패치 |
|---|---|---|---|
| 4.16 | 2018.04 | Flowtable 최초 도입 (SW 오프로드), nftables flow add 표현식 |
Pablo Neira Ayuso 메인라인 |
| 4.18 | 2018.08 | IPv6 fast path 지원 (nf_flow_offload_ipv6_hook) |
IPv6 관련 nf_flow_table_ip.c 확장 |
| 5.0 | 2019.03 | VLAN encap 지원, flow_offload_tuple에 encap 필드 추가 | VLAN 태그 캐싱 |
| 5.2 | 2019.07 | PPPoE encap/decap 지원 | ISP 환경 가속 |
| 5.3 | 2019.09 | bridge family flowtable 지원, table bridge 환경 동작 |
L2 포워딩 가속 |
| 5.7 | 2020.05 | TC flower 기반 HW 오프로드 기초 인프라 | nf_flow_table_offload.c 추가 |
| 5.13 | 2021.06 | HW 오프로드 안정화, flags offload 키워드, GC 통계 폴링 |
Mellanox/Netronome 드라이버 통합 |
| 5.17 | 2022.03 | DSA(Distributed Switch Architecture) flowtable 지원 | 임베디드 스위치 칩 오프로드 |
| 5.19 | 2022.07 | QinQ(이중 VLAN) 지원, NF_FLOW_TABLE_ENCAP_MAX=2 |
통신사 환경 VLAN stacking |
| 6.0 | 2022.10 | flowtable priority 파라미터, 다중 flowtable 우선순위 지정 | 복합 nftables 설정 지원 |
| 6.2 | 2023.02 | GC 워크큐 최적화, per-flowtable GC 독립 실행 | 대규모 세션 환경 성능 개선 |
| 6.4 | 2023.06 | flow_offload_tuple에 xmit_type 필드 추가 (bridge/route/xfrm) | 다양한 전달 모드 통합 |
| 6.6 LTS | 2023.10 | conntrack 이벤트 통합 개선, timeout 동기화 강화 | 장기 지원 안정 버전 |
| 6.8 | 2024.03 | nft_flowtable에 counter 표현식 지원 | 패킷/바이트 카운팅 통합 |
| 6.10+ | 2024.07+ | rhashtable 성능 개선, GC batch 처리, large-scale 최적화 | 100만+ 세션 환경 대응 |
Ubuntu 22.04 LTS: 5.15 (SW 오프로드 완전 지원, HW 일부)
Ubuntu 24.04 LTS: 6.8 (SW/HW 오프로드 완전 지원)
RHEL 9: 5.14 기반 (백포트로 일부 기능 지원)
Debian 12: 6.1 (SW/HW 오프로드 지원)
uname -r로 현재 커널 확인, modinfo nf_flow_table로 모듈 버전 확인
# 현재 시스템의 Flowtable 지원 수준 확인
# 1. 커널 버전
uname -r
# 2. Flowtable 커널 모듈 존재 여부
modprobe -n nf_flow_table && echo "SW offload 지원"
modprobe -n nf_flow_table_inet && echo "inet family 지원"
# 3. HW offload 커널 옵션 확인
grep -i "NF_FLOW_TABLE" /boot/config-$(uname -r)
# CONFIG_NF_FLOW_TABLE=m → SW 오프로드
# CONFIG_NF_FLOW_TABLE_INET=m → inet family
# CONFIG_NFT_FLOW_OFFLOAD=m → nftables flow offload
# 4. VLAN encap 지원 (5.0+)
grep "NF_FLOW_TABLE_ENCAP" /boot/config-$(uname -r) 2>/dev/null
# 5. NIC HW offload 지원
ethtool -k eth0 | grep "hw-tc-offload"
멀티코어 확장성과 RSS/RPS 연동
Flowtable의 rhashtable 룩업은 RCU read-side lock을 사용하므로 멀티코어 환경에서 잠금 경쟁 없이 동시에 실행됩니다. 하지만 최적의 성능을 위해서는 NIC의 RSS(Receive Side Scaling) 또는 커널의 RPS(Receive Packet Steering)와 올바르게 연동해야 합니다.
RSS + Flowtable 최적 구성
# RSS 큐 수 확인 및 설정
ethtool -l eth0
# Pre-set maximums:
# Combined: 32
# Current settings:
# Combined: 16
# CPU 코어 수에 맞게 RSS 큐 설정
NCPU=$(nproc)
ethtool -L eth0 combined $NCPU
ethtool -L eth1 combined $NCPU
# IRQ affinity 최적화 (각 큐를 별도 CPU에 매핑)
# 방법 1: irqbalance 비활성화 + 수동 설정
systemctl stop irqbalance
# 각 eth0-rxN IRQ를 CPU N에 바인딩
for irq in $(grep eth0 /proc/interrupts | awk '{print $1}' | tr -d ':'); do
cpu_mask=$((1 << (irq % NCPU)))
printf "%x" $cpu_mask > /proc/irq/$irq/smp_affinity
done
# 방법 2: RPS (소프트웨어 RSS, HW RSS 불가 시)
# 모든 CPU에 분산
echo "ff" > /sys/class/net/eth0/queues/rx-0/rps_cpus
echo "ff" > /sys/class/net/eth1/queues/rx-0/rps_cpus
# RFS (Receive Flow Steering) — 같은 플로우를 같은 CPU로 유지
echo 32768 > /proc/sys/net/core/rps_sock_flow_entries
for rxq in /sys/class/net/eth0/queues/rx-*; do
echo 2048 > $rxq/rps_flow_cnt
done
# XPS (Transmit Packet Steering) — TX 큐도 CPU에 매핑
for i in $(seq 0 $((NCPU-1))); do
printf "%x" $((1 << i)) > /sys/class/net/eth0/queues/tx-$i/xps_cpus
done
RCU read-side 잠금과 확장성 분석
/* Flowtable 룩업의 RCU 보호 메커니즘 */
/*
* rhashtable_lookup_fast()는 내부적으로 rcu_read_lock()을 사용합니다.
* RCU read-side lock의 특성:
*
* 1. 비용: 거의 0 (preemption disable만)
* 2. 병렬성: 모든 CPU에서 동시에 읽기 가능
* 3. 쓰기 경합: 삽입/삭제는 per-bucket spinlock (세분화된 잠금)
*
* 따라서 flowtable 룩업은 CPU 수에 비례하여 선형 확장합니다.
*/
/* flow_offload_lookup() 내부 동작 */
struct flow_offload_tuple_rhash *
flow_offload_lookup(struct nf_flowtable *flow_table,
struct flow_offload_tuple *tuple)
{
struct flow_offload_tuple_rhash *tuplehash;
/* RCU read-side 진입 (preempt_disable) */
rcu_read_lock();
/* SipHash 계산 → 버킷 탐색 → 5-튜플 비교 */
tuplehash = rhashtable_lookup(&flow_table->rhashtable,
tuple,
nf_flow_offload_rhash_params);
/* RCU read-side 해제 */
rcu_read_unlock();
return tuplehash;
}
/*
* 멀티코어 성능 스케일링 (실측 참고값):
*
* 코어 수 | Flowtable PPS | 확장 비율
* ---------+---------------+---------
* 1 코어 | 7.5 Mpps | 1.0x
* 2 코어 | 14.8 Mpps | 1.97x
* 4 코어 | 29.0 Mpps | 3.87x
* 8 코어 | 55.0 Mpps | 7.33x
* 16 코어 | 105.0 Mpps | 14.0x
* 32 코어 | 190.0 Mpps | 25.3x
*
* (64B 패킷, RSS 최적 설정, 10만 동시 세션)
* 32코어에서 선형성이 떨어지는 이유:
* - rhashtable resize 시 잠금 경쟁
* - LLC 캐시 미스 증가 (세션 수 증가 시)
* - NUMA 간 메모리 접근 레이턴시
*/
| 최적화 항목 | 설정 | 효과 |
|---|---|---|
| NIC IRQ를 같은 NUMA 노드에 바인딩 | echo N > /proc/irq/IRQ/smp_affinity |
DMA 버퍼와 CPU 간 로컬 메모리 접근 |
| RSS 큐를 NUMA 노드 내 CPU에 분산 | ethtool -L + affinity 설정 |
rhashtable 캐시 라인 로컬리티 향상 |
| GC 워크큐를 NIC NUMA 노드에 배치 | 커널 내부 (자동) | GC 플로우 순회 시 원격 메모리 접근 감소 |
| conntrack 해시 테이블 NUMA 로컬 할당 | sysctl net.netfilter.nf_conntrack_max |
slow path 성능 향상 |
컨테이너/네임스페이스 환경에서의 Flowtable
Docker, Kubernetes, LXC 등 컨테이너 환경에서는 네트워크 네임스페이스(netns)가 사용됩니다. Flowtable은 네임스페이스를 인식하므로 각 netns에서 독립적으로 설정할 수 있지만, 호스트와 컨테이너 간 패킷 경로가 복잡해져 설정에 주의가 필요합니다.
Docker 환경 Flowtable 설정
# Docker bridge 환경에서 호스트 Flowtable 설정
# 1. Docker bridge 인터페이스 확인
ip link show type bridge
# docker0: ... state UP
# 2. 호스트 nftables에 Flowtable 설정
cat > /etc/nftables/docker-flowtable.conf <<'EOF'
table inet filter {
flowtable ft {
hook ingress priority 0
devices = { eth0, docker0 }
}
chain forward {
type filter hook forward priority 0; policy accept;
# Docker 컨테이너 ↔ 외부 트래픽 가속
ip protocol { tcp, udp } flow add @ft
ip6 nexthdr { tcp, udp } flow add @ft
ct state established,related accept
}
}
EOF
nft -f /etc/nftables/docker-flowtable.conf
# 3. Docker 기본 iptables 규칙과의 공존
# Docker는 iptables를 사용하므로 nftables와 충돌 가능
# 해결: Docker를 iptables=false로 시작하고 nftables로 전환
# /etc/docker/daemon.json:
# { "iptables": false }
# 4. 확인: 컨테이너에서 외부로의 세션이 OFFLOAD 되는지
docker exec -it test-container ping -c 3 8.8.8.8
conntrack -L | grep OFFLOAD | grep 172.17
Kubernetes 환경에서의 Flowtable
| CNI 플러그인 | Flowtable 호환성 | 권장 여부 | 비고 |
|---|---|---|---|
| Calico (iptables 모드) | 가능 (호스트 nftables) | 조건부 권장 | kube-proxy iptables 규칙과 공존 필요 |
| Calico (eBPF 모드) | 불필요 | eBPF 자체 가속 | Flowtable보다 빠른 eBPF datapath |
| Cilium | 불필요 | eBPF 자체 가속 | kube-proxy 대체, 자체 conntrack |
| Flannel (VXLAN) | 제한적 | 비추천 | VXLAN 터널 내부 플로우 오프로드 불가 |
| Bridge CNI | 가능 | 권장 | 단순 bridge 환경에서 효과적 |
| Host-networking Pod | 가능 | 권장 | 호스트 Flowtable 그대로 적용 |
PPPoE/DSL 환경 Flowtable
ISP 가정용 라우터나 BRAS(Broadband Remote Access Server)에서는 PPPoE 세션을 통해 인터넷에 연결합니다. 커널 5.2부터 Flowtable은 PPPoE encap/decap을 지원하여 PPPoE 환경에서도 fast path 가속이 가능합니다.
/* PPPoE encap 처리: net/netfilter/nf_flow_table_ip.c */
/*
* PPPoE 환경의 Flowtable 패킷 경로:
*
* [수신] eth0 (PPPoE 서버쪽)
* → PPPoE 헤더 strip
* → IP 패킷 추출
* → flowtable 룩업 (5-tuple)
* → 히트: NAT + TTL + MAC 재작성
* → PPPoE 헤더 재삽입 (반대 방향)
* → eth1 (LAN쪽) 전달
*
* flow_offload_tuple에 PPPoE 세션 ID가 캐싱됨:
* encap[0].proto = ETH_P_PPP_SES (0x8864)
* encap[0].id = PPPoE Session ID
*/
/* PPPoE encap 정보 설정 */
static void nf_flow_rule_route_pppoe(struct flow_offload_tuple *tuple,
struct sk_buff *skb)
{
struct pppoe_hdr *ph = pppoe_hdr(skb);
tuple->encap[0].proto = htons(ETH_P_PPP_SES);
tuple->encap[0].id = ntohs(ph->sid);
tuple->in_vlan_ingress = 1;
}
PPPoE + Flowtable nftables 설정
# PPPoE 환경 Flowtable 설정
# 시나리오: ppp0 (WAN, PPPoE), eth1 (LAN)
table inet filter {
flowtable ft {
hook ingress priority 0
# PPPoE: 물리 인터페이스(eth0)를 지정 (ppp0이 아님)
devices = { eth0, eth1 }
}
chain forward {
type filter hook forward priority 0; policy drop;
# LAN → WAN (PPPoE) 트래픽 가속
ip protocol { tcp, udp } flow add @ft
ct state established,related accept
iifname "eth1" oifname "ppp0" ct state new accept
}
}
table ip nat {
chain postrouting {
type nat hook postrouting priority 100;
oifname "ppp0" masquerade
}
}
- Flowtable devices에는
ppp0이 아닌 물리 인터페이스(eth0)를 지정합니다. - PPPoE MTU는 일반적으로 1492 바이트입니다. PMTU Discovery가 올바르게 동작하는지 확인하세요.
- PPPoE 세션 재연결 시 flowtable의 기존 플로우는 자동으로 GC됩니다 (dst_entry obsolete).
- PPPoE + VLAN 조합(ISP 환경)은 커널 5.19+에서 안정적입니다.
DSL/PPPoE MTU와 Flowtable 상호작용
| 항목 | Ethernet | PPPoE | PPPoE + VLAN |
|---|---|---|---|
| L2 MTU | 1500 | 1500 | 1500 |
| PPPoE 헤더 | - | 8 bytes | 8 bytes |
| VLAN 헤더 | - | - | 4 bytes |
| IP MTU | 1500 | 1492 | 1488 |
| Flowtable 동작 | 정상 | 정상 (5.2+) | 정상 (5.19+) |
| MTU 초과 패킷 | slow path → ICMP Frag Needed | slow path → ICMP Frag Needed | slow path → ICMP Frag Needed |
보안 고려사항과 감사(Audit)
Flowtable은 ESTABLISHED 연결의 패킷을 방화벽 규칙 없이 전달하므로, 보안 정책 설계 시 반드시 고려해야 할 사항들이 있습니다. 잘못 구성하면 보안 정책이 우회되는 심각한 문제가 발생할 수 있습니다.
선택적 Flowtable 등록 (보안 정책 적용)
# 보안 강화를 위한 선택적 Flowtable 설정
# 민감 서비스 트래픽은 항상 slow path에서 규칙 평가
table inet filter {
flowtable ft {
hook ingress priority 0
devices = { eth0, eth1 }
}
chain forward {
type filter hook forward priority 0; policy drop;
# 1. IDS/IPS 대상 트래픽: Flowtable 제외 (slow path 강제)
# SSH, HTTPS 관리 포트, 데이터베이스 포트
tcp dport { 22, 443, 3306, 5432 } ct state established accept
# 2. 일반 TCP/UDP만 Flowtable 오프로드
ip protocol { tcp, udp } flow add @ft
# 3. 나머지 ESTABLISHED 허용
ct state established,related accept
# 4. 신규 연결 허용
iifname "eth1" oifname "eth0" ct state new accept
}
}
보안 감사(Audit) 스크립트
#!/bin/bash
# Flowtable 보안 감사 스크립트
echo "=== Flowtable 보안 감사 ==="
# 1. Flowtable 상태 확인
echo ""
echo "[1] Flowtable 설정:"
nft list flowtable inet filter ft 2>/dev/null || echo " flowtable 미설정"
# 2. OFFLOAD 세션 수
echo ""
echo "[2] OFFLOAD 세션 수:"
TOTAL=$(conntrack -L 2>/dev/null | wc -l)
OFFLOADED=$(conntrack -L 2>/dev/null | grep -c OFFLOAD)
echo " 전체: $TOTAL, OFFLOAD: $OFFLOADED ($((OFFLOADED * 100 / (TOTAL + 1)))%)"
# 3. OFFLOAD된 민감 포트 세션 확인
echo ""
echo "[3] 민감 포트 OFFLOAD 세션 (SSH/DB):"
conntrack -L 2>/dev/null | grep OFFLOAD | \
grep -E "dport=(22|3306|5432|1433|6379)" | head -10
# 4. HW 오프로드 상태
echo ""
echo "[4] HW 오프로드 상태:"
for dev in $(ip -o link show up | awk -F': ' '{print $2}'); do
hw=$(ethtool -k $dev 2>/dev/null | grep "hw-tc-offload" | awk '{print $2}')
[ -n "$hw" ] && echo " $dev: hw-tc-offload=$hw"
done
# 5. conntrack 이벤트 로깅 활성화 여부
echo ""
echo "[5] conntrack 이벤트 로깅:"
sysctl -n net.netfilter.nf_conntrack_log_invalid 2>/dev/null
sysctl -n net.netfilter.nf_conntrack_events 2>/dev/null
echo ""
echo "=== 감사 완료 ==="
실전 트러블슈팅 가이드
Flowtable 운영 중 발생할 수 있는 문제와 해결 방법을 정리합니다. 대부분의 문제는 잘못된 설정, 커널 버전 미지원, 또는 bypass 조건 미인지에서 발생합니다.
문제 1: 세션이 OFFLOAD 되지 않음
# 증상: conntrack -L에 [OFFLOAD] 플래그가 안 보임
# 진단 순서:
# 1. nftables 규칙에 flow add @ft가 있는지 확인
nft list chain inet filter forward | grep "flow"
# 없으면: nft 규칙에 "flow add @ft" 추가
# 2. flowtable 정의에 올바른 devices가 있는지
nft list flowtable inet filter ft
# devices에 입력/출력 인터페이스 모두 포함되어야 함
# 3. conntrack helper가 붙어있는지 (ALG 연결)
conntrack -L | grep "helper="
# helper가 있는 세션은 오프로드 불가
# 해결: 불필요한 helper 비활성화
# sysctl -w net.netfilter.nf_conntrack_helper=0
# 4. 커널 모듈 로드 확인
lsmod | grep -E "nf_flow_table|nft_flow"
# 없으면: modprobe nf_flow_table && modprobe nft_flow_offload
# 5. 방화벽 정책 순서 확인
# flow add @ft는 ct state established accept보다 먼저 와야 함
nft list chain inet filter forward
문제 2: Flowtable 활성화 후 오히려 성능 저하
# 증상: Flowtable 설정 후 처리량 감소 또는 불안정
# 원인 1: rhashtable resize 과도
# 다수의 단기(short-lived) 연결이 계속 등록/삭제되면
# rhashtable이 반복적으로 확장/축소하여 lock 경쟁 발생
# 해결: 단기 연결은 flowtable 제외
# nft: tcp dport { 80 } ct state established accept (flow add 전에)
# 원인 2: GC 과부하
# 10만+ 세션에서 2초마다 전체 순회하면 CPU 부하
dmesg | grep "nf_flow"
# 해결: conntrack_max 조정, 불필요한 세션 오프로드 제한
# 원인 3: dst_cache 무효화 빈도
# 라우팅 테이블이 자주 변경되면 플로우가 계속 teardown
ip monitor route # 라우팅 변경 감지
# 해결: 라우팅 안정화, 동적 라우팅 프로토콜 타이머 조정
# 원인 4: RSS 미설정 (단일 CPU에 패킷 집중)
cat /proc/interrupts | grep eth0
# 해결: RSS 큐 설정 (ethtool -L eth0 combined N)
문제 3: HW 오프로드가 동작하지 않음
# 증상: flags offload 설정했으나 tc filter에 hw_count=0
# 1. NIC HW TC offload 지원 확인
ethtool -k eth0 | grep "hw-tc-offload"
# off이면: ethtool -K eth0 hw-tc-offload on
# "Cannot change"이면: NIC/드라이버 미지원
# 2. eSwitch 모드 확인 (Mellanox)
devlink dev eswitch show pci/0000:03:00.0
# legacy이면: switchdev 전환 필요
# 3. 커널 로그에서 오프로드 실패 메시지 확인
dmesg | grep -i "offload\|flow\|tc" | tail -20
# 4. TC flower 규칙 확인
tc filter show dev eth0 ingress
# in_hw_count 또는 in_hw 필드 확인
# 5. NIC 드라이버 버전 확인
ethtool -i eth0 | grep -E "driver|version"
# mlx5_core, firmware 버전 등 확인
# 6. FDB 용량 초과 확인
tc -s filter show dev eth0 ingress | wc -l
# NIC별 최대 규칙 수 초과 시 추가 규칙은 SW fallback
문제 4: NAT 환경에서 패킷 드롭
# 증상: Flowtable + DNAT 환경에서 간헐적 패킷 드롭
# 1. DNAT 후 라우팅이 변경되는 경우
# DNAT 목적지가 로컬 소켓이면 Flowtable 등록 불가
conntrack -L | grep "dnat"
# DNAT 대상이 로컬인지 확인
# 2. masquerade + Flowtable 충돌
# masquerade는 인터페이스 주소가 변경되면 세션 무효화
# (DHCP 갱신 등)
# 해결: masquerade 대신 snat to 고정IP 사용
# 3. NAT 정보 불일치 디버깅
conntrack -L -o extended | grep OFFLOAD
# 출력에서 src/dst/sport/dport 확인
# reply 방향의 주소가 올바른지 비교
# 4. pcap으로 실제 패킷 검증
tcpdump -i eth0 -c 100 -nn "tcp port 80" -w /tmp/capture.pcap
# Wireshark에서 NAT 변환 전후 주소 확인
흔한 실수 목록
| 실수 | 증상 | 해결 |
|---|---|---|
| devices에 한쪽 인터페이스만 등록 | 한 방향만 오프로드 | 양쪽 인터페이스 모두 devices에 추가 |
flow add 위치가 ct state established accept 뒤에 |
flow add에 도달하지 않음 | flow add를 ct state 규칙 앞으로 이동 |
| bridge 환경에서 br0을 devices에 지정 | 오프로드 안 됨 | 물리 포트(eth0, eth1)를 직접 지정 |
| conntrack helper 자동 로드 활성 상태 | FTP 세션 등 오프로드 불가 | sysctl net.netfilter.nf_conntrack_helper=0 |
| 방화벽 규칙 변경 후 conntrack 미플러시 | 기존 세션에 새 규칙 미적용 | conntrack -F 실행 |
| HW offload에서 ppp0 디바이스 직접 지정 | 오프로드 안 됨 | 물리 인터페이스 지정 (eth0) |
| inet 대신 ip 테이블에 flowtable 정의 | IPv6 미가속 | table inet 사용으로 IPv4/IPv6 동시 지원 |
Flowtable 튜닝 Best Practices
대규모 환경(10만+ 동시 세션)에서 Flowtable 성능을 최대화하기 위한 커널 파라미터 튜닝과 운영 가이드입니다.
sysctl 튜닝 파라미터
# /etc/sysctl.d/99-flowtable-tuning.conf
# --- conntrack 설정 ---
# conntrack 최대 세션 수 (Flowtable 세션 수와 연동)
# Flowtable 세션도 conntrack 테이블에 포함
net.netfilter.nf_conntrack_max = 1000000
# conntrack 해시 테이블 크기 (max / 4 권장)
# 부팅 시 설정: /etc/modprobe.d/nf_conntrack.conf
# options nf_conntrack hashsize=250000
# TCP 연결 타임아웃 (Flowtable timeout과 독립)
net.netfilter.nf_conntrack_tcp_timeout_established = 86400
# conntrack helper 자동 로드 비활성화 (ALG 제한)
net.netfilter.nf_conntrack_helper = 0
# --- 네트워크 스택 ---
# 네트워크 백로그 (고 PPS 환경)
net.core.netdev_max_backlog = 250000
# NAPI 가중치 (한 번에 처리하는 패킷 수)
net.core.dev_weight = 64
# softirq 처리 시간 제한 (고 PPS 시 증가)
net.core.netdev_budget = 600
# ARP/NDP 캐시 (Flowtable MAC 캐시와 연동)
net.ipv4.neigh.default.gc_thresh3 = 8192
net.ipv6.neigh.default.gc_thresh3 = 8192
# 라우팅 캐시 (dst_entry 관련)
net.ipv4.route.max_size = 2048000
# IP forwarding (필수)
net.ipv4.ip_forward = 1
net.ipv6.conf.all.forwarding = 1
메모리 사용량 계산 및 최적화
| 동시 세션 수 | flow_offload 메모리 | conntrack 메모리 | rhashtable 버킷 | 합계 추정 |
|---|---|---|---|---|
| 10,000 | ~10 MB | ~5 MB | ~1 MB | ~16 MB |
| 100,000 | ~100 MB | ~50 MB | ~8 MB | ~158 MB |
| 500,000 | ~500 MB | ~250 MB | ~32 MB | ~782 MB |
| 1,000,000 | ~1 GB | ~500 MB | ~64 MB | ~1.5 GB |
| 4,000,000 | ~4 GB | ~2 GB | ~256 MB | ~6.2 GB |
flow_offload 메모리 ≈ 세션 수 × 1KBconntrack 메모리 ≈ 세션 수 × 0.5KBrhashtable 버킷 ≈ 세션 수 / 16 × 64B (포인터 배열)NUMA 환경에서는 노드별 할당으로 약 10-20% 추가 오버헤드가 발생합니다.
운영 환경 체크리스트
#!/bin/bash
# Flowtable 운영 환경 점검 스크립트
echo "=== Flowtable 운영 점검 ==="
# 1. 커널 버전 및 모듈
echo "[커널] $(uname -r)"
echo "[모듈] $(lsmod | grep -c nf_flow) 개 로드"
# 2. conntrack 용량
CT_MAX=$(sysctl -n net.netfilter.nf_conntrack_max)
CT_CUR=$(sysctl -n net.netfilter.nf_conntrack_count)
CT_PCT=$((CT_CUR * 100 / CT_MAX))
echo "[conntrack] $CT_CUR / $CT_MAX ($CT_PCT%)"
[ $CT_PCT -gt 80 ] && echo " ⚠ 경고: conntrack 사용률 80% 초과!"
# 3. OFFLOAD 비율
TOTAL=$(conntrack -L 2>/dev/null | wc -l)
OFFLOADED=$(conntrack -L 2>/dev/null | grep -c OFFLOAD)
if [ $TOTAL -gt 0 ]; then
echo "[OFFLOAD] $OFFLOADED / $TOTAL ($((OFFLOADED * 100 / TOTAL))%)"
fi
# 4. 메모리 사용량 추정
MEM_EST=$((OFFLOADED * 1536 / 1024 / 1024))
echo "[메모리] 추정 사용량: ~${MEM_EST}MB (OFFLOAD 세션 기준)"
# 5. RSS 큐 설정
for dev in $(nft list flowtable inet filter ft 2>/dev/null | grep -oP '[a-z]+[0-9]+'); do
queues=$(ethtool -l $dev 2>/dev/null | grep "Combined" | tail -1 | awk '{print $2}')
echo "[RSS] $dev: $queues 큐"
done
# 6. IP forwarding
FWD=$(sysctl -n net.ipv4.ip_forward)
echo "[IPv4 forwarding] $FWD"
[ "$FWD" != "1" ] && echo " ⚠ 경고: IP forwarding 비활성!"
echo "=== 점검 완료 ==="
Flowtable vs TC/XDP 비교 및 연동
리눅스 커널에는 Flowtable 외에도 TC(Traffic Control)와 XDP(eXpress Data Path) 등 고성능 패킷 처리 메커니즘이 있습니다. 각 기술의 동작 계층, 프로그래밍 모델, 성능 특성을 비교합니다.
Flowtable과 XDP/TC 공존 설정
Flowtable과 XDP/TC는 서로 다른 계층에서 동작하므로 공존이 가능합니다. 패킷 처리 순서는: XDP → TC ingress → Flowtable hook → Netfilter입니다.
# XDP + Flowtable 공존 예시
# XDP: DDoS 필터링 (초기 차단)
# Flowtable: ESTABLISHED 세션 가속
# 1. XDP 프로그램 로드 (DDoS 차단용)
ip link set dev eth0 xdp obj ddos_filter.o sec xdp
# 2. TC ingress에 eBPF 프로그램 (선택적)
tc qdisc add dev eth0 clsact
tc filter add dev eth0 ingress bpf da obj classifier.o sec tc
# 3. nftables Flowtable 설정
nft -f /etc/nftables/flowtable.conf
# 처리 순서:
# 패킷 수신 → XDP (DDoS 차단) → sk_buff 생성
# → TC ingress (분류) → Flowtable hook (세션 가속)
# → [미스] → Netfilter slow path
# 주의: XDP에서 XDP_PASS를 반환한 패킷만 Flowtable에 도달
# XDP에서 XDP_TX/XDP_DROP된 패킷은 Flowtable에 도달하지 않음
기술 선택 가이드
| 사용 사례 | 1순위 권장 | 2순위 대안 | 이유 |
|---|---|---|---|
| Linux 라우터 (NAT + 방화벽) | Flowtable SW | Flowtable HW | nftables 통합, NAT 완전 지원, 설정 간편 |
| 통신사 NGFW (100G+) | Flowtable HW | DPDK | SmartNIC 활용, CPU 절감, 선형 처리 |
| DDoS 방어 (초당 수천만 패킷) | XDP | TC eBPF | 최소 레이턴시, sk_buff 미생성 |
| L4 로드밸런서 | XDP | Flowtable + DNAT | IPVS 대안, Cilium/Katran |
| Kubernetes Pod 간 통신 | Cilium eBPF | Flowtable (bridge CNI) | kube-proxy 대체, 자체 conntrack |
| ISP PPPoE 라우터 | Flowtable SW | - | PPPoE encap 지원, masquerade 통합 |
| VPN 게이트웨이 (WireGuard/IPSec) | 일반 Netfilter | - | 터널 패킷은 Flowtable bypass 대상 |
| 패킷 미러링/캡처 | TC mirror | XDP | Flowtable은 LOG/NFQUEUE 생략 |
내부 동기화 메커니즘 (RCU/Lock)
Flowtable의 높은 병렬 성능은 정교한 동기화 메커니즘에 기반합니다. rhashtable의 RCU read-side 잠금, per-bucket spinlock, GC 워크큐의 동기화를 이해하면 성능 특성과 한계를 정확히 파악할 수 있습니다.
/*
* 동시성 보장:
* - 읽기 (fast path): 무한 병렬 (RCU read-side, 비용 ~5ns)
* - 쓰기 (insert/delete): per-bucket 직렬화 (~50ns)
* - resize: 전체 테이블 잠금 (드물게 발생, ~1ms)
* - GC: 단일 워크큐 (2초 주기)
*
* 핵심 성능 특성:
* - 읽기:쓰기 비율이 1000:1 이상인 환경에 최적
* - ESTABLISHED 패킷 = 읽기, NEW/FIN/RST = 쓰기
* - 따라서 일반 트래픽에서 거의 잠금 없이 동작
*/
/* RCU grace period와 flow_offload 해제 */
static void flow_offload_del(struct nf_flowtable *flow_table,
struct flow_offload *flow)
{
/* rhashtable에서 제거 (per-bucket spinlock) */
rhashtable_remove_fast(&flow_table->rhashtable,
&flow->tuplehash[FLOW_OFFLOAD_DIR_ORIGINAL].node,
nf_flow_offload_rhash_params);
rhashtable_remove_fast(&flow_table->rhashtable,
&flow->tuplehash[FLOW_OFFLOAD_DIR_REPLY].node,
nf_flow_offload_rhash_params);
/* RCU grace period 후 메모리 해제
* → 현재 rcu_read_lock() 중인 CPU가 모두 unlock할 때까지 대기
* → flow 구조체가 안전하게 해제됨 */
call_rcu(&flow->rcu_head, flow_offload_free_rcu);
}
/* flow_offload_free_rcu — RCU 콜백에서 실행 */
static void flow_offload_free_rcu(struct rcu_head *head)
{
struct flow_offload *flow =
container_of(head, struct flow_offload, rcu_head);
/* dst_entry 참조 해제 */
dst_release(flow->tuplehash[FLOW_OFFLOAD_DIR_ORIGINAL].tuple.dst_cache);
dst_release(flow->tuplehash[FLOW_OFFLOAD_DIR_REPLY].tuple.dst_cache);
/* conntrack 참조 해제 */
nf_ct_put(flow_offload_ct(flow));
kfree(flow);
}
| 연산 | 동기화 방식 | 비용 (cycles) | 발생 빈도 |
|---|---|---|---|
| rhashtable 읽기 (fast path) | RCU read-side | ~5 | 매 패킷 |
| flow_offload_add (삽입) | per-bucket spinlock | ~50 | 신규 ESTABLISHED |
| flow_offload_del (삭제) | per-bucket spinlock + RCU callback | ~100 | 세션 종료/만료 |
| rhashtable resize | mutex + RCU grace period | ~1M (전체 재해시) | 로드 팩터 임계 초과 시 |
| timeout 갱신 | 단순 쓰기 (atomic 불필요) | ~1 | 매 패킷 (fast path) |
| GC 순회 | rhashtable_walk (RCU) | ~N (세션 수) | 2초 주기 |
flow->timeout은 u64 필드로 단순 쓰기입니다.
여러 CPU가 동시에 같은 플로우의 timeout을 갱신해도(RSS가 같은 세션을 다른 CPU에 배분하는 경우)
결과는 "어떤 CPU가 갱신한 값이든 최근 시각"이므로 정확성에 문제가 없습니다.
이를 "benign race"라 하며, 의도적으로 잠금을 생략하여 성능을 최적화합니다.
커널 컴파일 옵션 (Kconfig)
커스텀 커널을 빌드하거나 Flowtable 지원 여부를 확인할 때 필요한 Kconfig 옵션을 정리합니다.
# Flowtable 핵심 (필수)
CONFIG_NF_FLOW_TABLE=m # SW 오프로드 코어
CONFIG_NF_FLOW_TABLE_INET=m # inet (IPv4+IPv6) family
CONFIG_NFT_FLOW_OFFLOAD=m # nftables "flow add/offload" 표현식
# conntrack (Flowtable 의존)
CONFIG_NF_CONNTRACK=m # conntrack 코어
CONFIG_NF_NAT=m # NAT 지원 (SNAT/DNAT)
CONFIG_NF_NAT_MASQUERADE=m # masquerade 지원
# nftables (Flowtable 설정 인터페이스)
CONFIG_NF_TABLES=m # nftables 코어
CONFIG_NF_TABLES_INET=y # inet 패밀리
CONFIG_NF_TABLES_BRIDGE=m # bridge 패밀리 (bridge flowtable)
# HW 오프로드 (선택)
CONFIG_NET_CLS_ACT=y # TC actions (HW offload 기반)
CONFIG_NET_CLS_FLOWER=m # TC flower classifier
CONFIG_NET_ACT_CT=m # TC conntrack action
CONFIG_NET_SWITCHDEV=y # switchdev 프레임워크
# VLAN/PPPoE encap 지원
CONFIG_VLAN_8021Q=m # VLAN 지원
CONFIG_PPPOE=m # PPPoE 지원
# 디버깅 (개발/테스트용)
CONFIG_NF_FLOW_TABLE_PROCFS=y # /proc 인터페이스 (일부 커널)
CONFIG_NETFILTER_XT_TARGET_LOG=m # 로깅
# SmartNIC 드라이버 (사용하는 NIC에 따라 선택)
CONFIG_MLX5_CORE=m # Mellanox ConnectX
CONFIG_MLX5_ESWITCH=y # eSwitch (switchdev 모드)
CONFIG_NFP=m # Netronome
CONFIG_ICE=m # Intel E810
# 현재 커널 설정 확인
grep -E "NF_FLOW_TABLE|NFT_FLOW|NET_CLS_FLOWER|NET_SWITCHDEV" \
/boot/config-$(uname -r)
# 모듈 로드 확인
modprobe -n -v nf_flow_table 2>&1
modprobe -n -v nft_flow_offload 2>&1
# 의존성 확인
modinfo nf_flow_table | grep depends
# depends: nf_conntrack, libcrc32c
VPN(WireGuard/IPSec) 환경과 Flowtable
VPN 터널 환경에서 Flowtable의 동작 방식과 제한사항을 이해하는 것이 중요합니다. IPSec과 WireGuard는 서로 다른 방식으로 Flowtable과 상호작용합니다.
| VPN 프로토콜 | Flowtable 오프로드 | 이유 | 대안 |
|---|---|---|---|
| IPSec (ESP) | 불가 (bypass) | skb_sec_path(skb) → 즉시 slow path |
IPSec HW offload (NIC 지원 시) |
| IPSec (AH) | 불가 | IPsec 처리 필요 | - |
| WireGuard | 외부 패킷: 가능, 내부 패킷: 불가 | wg0 인터페이스의 복호화 후 패킷은 별도 경로 | wg0 ↔ LAN 구간은 별도 flowtable |
| OpenVPN (tun/tap) | 불가 (유저스페이스 처리) | tun 디바이스 → 유저스페이스 → 커널 재진입 | WireGuard로 전환 권장 |
| GRE 터널 | 외부 패킷: 가능, 내부: 불가 | inner 헤더 파싱 불가 | NIC GRE offload (일부) |
| VXLAN | 외부 패킷: 가능, 내부: 제한적 | 일부 SmartNIC에서 inner offload 지원 | CX-6 Dx+ inner flowtable |
WireGuard + Flowtable 최적 구성
# WireGuard 환경에서의 Flowtable 설정
# 시나리오:
# eth0 (WAN) ↔ wg0 (VPN 터널) ↔ eth1 (LAN)
#
# Flowtable 적용 가능 구간:
# 1. eth0 ↔ eth1 (WAN ↔ LAN, VPN 미통과 트래픽)
# 2. wg0 ↔ eth1 (VPN 복호화 후 ↔ LAN)
#
# Flowtable 적용 불가:
# eth0 → wg0 (암호화된 UDP 패킷, WireGuard 처리 필요)
table inet filter {
# 구간 1: WAN ↔ LAN (VPN 미통과)
flowtable ft_wan {
hook ingress priority 0
devices = { eth0, eth1 }
}
# 구간 2: VPN ↔ LAN (복호화 후)
flowtable ft_vpn {
hook ingress priority 0
devices = { wg0, eth1 }
}
chain forward {
type filter hook forward priority 0; policy drop;
# WAN ↔ LAN 트래픽 가속
iifname { "eth0", "eth1" } ip protocol { tcp, udp } flow add @ft_wan
# VPN ↔ LAN 트래픽 가속
iifname { "wg0", "eth1" } ip protocol { tcp, udp } flow add @ft_vpn
ct state established,related accept
ct state new accept
}
}
관련 문서
Netfilter Flowtable과 관련된 다른 주제를 더 깊이 이해하고 싶다면 다음 문서를 참고하세요.