Netfilter Flowtable 심화

Netfilter Flowtable의 SW/HW 오프로드 메커니즘, conntrack 대비 성능 비교, nftables flowtable 설정, SmartNIC 연동, 역방향 경로 캐싱, bypass 조건 종합 가이드. 커널 내부 데이터 경로, 핵심 자료구조/API, 운영 환경 튜닝 포인트와 장애 디버깅 절차까지 실무 관점으로 다룹니다.

전제 조건: Netfilter 프레임워크 심화네트워크 스택 문서를 먼저 읽으세요. Flowtable은 conntrack 위에서 동작하므로 연결 추적(connection tracking) 기초가 반드시 필요합니다. SmartNIC HW 오프로드를 이해하려면 SmartNIC/DPU 문서도 함께 참고하세요.
일상 비유: Flowtable은 고속도로 하이패스와 같습니다. 처음 진입하는 차량(신규 연결)은 요금소 직원(conntrack + Netfilter 훅)의 검사를 받지만, 이미 등록된 차량(ESTABLISHED 연결)은 하이패스 단말기로 자동 통과합니다. 하이패스 레인(Flowtable)에서는 요금소 처리 없이 전용 경로로 직접 빠져나가기 때문에 처리량이 획기적으로 올라가고, SmartNIC이 장착된 경우에는 NIC이 직접 차량을 안내해 CPU가 전혀 개입하지 않아도 됩니다.

핵심 요약

  • 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로 처리됩니다.

단계별 이해

  1. conntrack 기초 확인
    conntrack -L로 현재 연결 상태를 확인합니다. Flowtable은 ESTABLISHED 상태의 연결만 오프로드합니다.
  2. nftables flowtable 설정
    flowtable ft { hook ingress priority 0; devices = { eth0, eth1 }; }를 정의하고 forward 체인에서 flow add @ft를 추가합니다.
  3. SW 오프로드 동작 확인
    conntrack -L | grep OFFLOAD로 오프로드된 세션을 확인합니다. nft list flowtable로 등록된 장치 목록을 확인합니다.
  4. 성능 측정
    iperf3 또는 pktgen으로 Flowtable 전후 처리량을 비교합니다. 일반적으로 4배 이상 향상을 기대할 수 있습니다.
  5. HW 오프로드 활성화
    SmartNIC이 지원되면 flowtable에 flags offload를 추가하고 ethtool -K eth0 hw-tc-offload on으로 TC offload를 활성화합니다.
  6. bypass 조건 점검
    FTP(conntrack helper 사용), 멀티캐스트, IP 단편화 트래픽은 오프로드되지 않으므로 일반 경로로 처리됨을 인지하고 방화벽 규칙을 설계합니다.
  7. 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로 지정하세요.
  8. 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은 이 문제를 세션 단위 캐싱으로 해결합니다.

Flowtable 지원 커널 버전: SW 오프로드는 커널 4.16+, HW 오프로드(SmartNIC)는 커널 5.13+에서 완전 지원됩니다. nftables flowtable 설정은 nftables 0.9.1+ (libnftnl 1.1.5+)이 필요합니다.
Netfilter 처리 방식 비교
방식 경로 규칙 재평가 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;
};
Netfilter Flowtable 패킷 처리 경로 NIC RX eth0/eth1 TC Ingress Hook flowtable lookup 캐시 히트 nf_flowtable rhashtable lookup L3/L4 tuple match NAT 재작성 src/dst 주소·포트 변환 직접 전달 NIC TX eth1/eth0 캐시 미스 일반 Netfilter 경로 (slow path) PREROUTING → conntrack → FORWARD → 규칙 재평가 → POSTROUTING flow_offload_add() (ESTABLISHED 후 등록) HW 오프로드 (SmartNIC) ndo_flow_offload() 콜백 NIC 내부 플로우 테이블 등록 CPU 없이 NIC에서 직접 처리 HW offload 등록 GC Worker 만료 항목 제거 (30s idle) 범례: SW fast path HW 오프로드 slow path

패킷이 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 룩업 성능 특성
특성 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 경로보다 빠른 핵심 이유:

체크섬 업데이트 및 경로 유효성 검사

/* 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);
}
SW 오프로드 타임아웃: NF_FLOW_TIMEOUT은 기본 30초(30 * HZ)입니다. 패킷이 통과할 때마다 flow->timeout이 현재 시각 + 30초로 갱신됩니다. 30초 동안 패킷이 없으면 GC 워커가 플로우를 제거하고, 이후 패킷은 slow path에서 conntrack을 통해 다시 flowtable에 등록될 수 있습니다.
SW 오프로드 vs 일반 Netfilter 함수 호출 비교
처리 단계 일반 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;
    }
}
HW 오프로드 활성화 요구사항
항목 요구사항 확인 방법
커널 버전 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 드라이버, 메모리 대역폭에 따라 달라집니다.

패킷 포워딩 성능 비교 (64 byte 소형 패킷, 단일 CPU 코어)
처리 방식 처리량 (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%
측정 환경 참고: Intel Xeon Gold 6230R @ 2.1GHz, DDR4-2933, Mellanox ConnectX-5 100GbE, 패킷 크기 64B, 단방향 TCP 세션 1개, 커널 5.15 LTS. 실제 NGFW 환경은 수만~수십만 동시 세션을 처리하므로 rhashtable 캐시 미스와 메모리 접근 패턴이 성능에 영향을 미칩니다.
# 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로 처리됩니다. 이 조건을 이해하지 못하면 방화벽 정책이 의도치 않게 적용되지 않는 보안 문제가 발생할 수 있습니다.

Flowtable bypass 조건 상세
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 플래그

NAT 관련 flow_offload 플래그
플래그 의미 적용 조건
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를 사용합니다.

이러한 ALG(Application Layer Gateway) 연결은 nfct_help(ct)가 NULL이 아니므로 nft_flow_offload_allow()에서 즉시 거부됩니다. 해당 세션은 항상 slow path에서 처리됩니다.

NAT + Flowtable 연동 패킷 처리 경로 [첫 번째 패킷 — Slow Path] NIC RX eth0 PREROUTING conntrack NEW + DNAT FORWARD chain flow add @ft 평가 POSTROUTING SNAT/masquerade 적용 NIC TX eth1 flow_offload_alloc(ct) NAT 정보 복사: NF_FLOW_SNAT|DNAT 변환 주소/포트를 tuple에 저장 nf_flowtable (rhashtable) ORIGINAL + REPLY 튜플 등록 [이후 패킷 — Fast Path] NIC RX eth0 TC Ingress Hook rhashtable 룩업 → 히트 인라인 NAT 적용 nf_flow_nat_ip() 호출 dev_queue_xmit() dst_cache로 직접 전달 NIC TX eth1 flow 플래그 NF_FLOW_SNAT = BIT(0) NF_FLOW_DNAT = BIT(1) NF_FLOW_DYING = BIT(2) NF_FLOW_HW = BIT(3) 범례: slow path (첫 패킷) fast path (이후 패킷) NAT 정보 등록

역방향 경로 캐싱 메커니즘

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)이 보장됩니다.

VLAN/Bridge 환경에서의 역방향 경로 특수 처리
환경 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
Flowtable GC 플로우 생명주기 NEW 첫 패킷 slow path conntrack 생성 ESTABLISHED flow_offload_add SW_OFFLOADED rhashtable 히트 fast path 포워딩 flags offload ndo_flow_offload HW_OFFLOADED NIC 내부 처리 CPU 개입 없음 GC Worker (2초 주기) nf_flow_offload_gc_step timeout/RST/FIN 검사 HW Stats 폴링 FLOW_CLS_STATS → NIC timeout 갱신 ↓ RST 수신 ↓ 30s idle TEARDOWN flow_offload_teardown() HW 해제 워크 큐잉 ↓ 라우팅 변경 DYING NF_FLOW_DYING 플래그 GC가 rhashtable 제거 DELETED — RCU free 전환 조건: RST/FIN/타임아웃 라우팅 변경/dst 만료 HW stats 폴링

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 vs Linux 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 처리 한계

IPv6 Extension Header별 Flowtable 처리 방침
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은 다음 제약이 있습니다.

# 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/ 디렉터리에 집중되어 있습니다. 주요 파일과 역할을 정리합니다.

Flowtable 관련 커널 소스 파일
파일 경로 역할 주요 함수/심볼
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 플로우 생명주기 신규 연결 SYN / 첫 패킷 slow path 처리 conntrack ESTABLISHED 등록 대기 flow_offload_alloc() flow_offload_add() 등록 완료 SW OFFLOADED rhashtable 히트 fast path 포워딩 30s idle FIN/RST DYING flow_offload_teardown() HW 오프로드 해제 DELETED flow_offload_del() + RCU free HW OFFLOADED NIC 내부 플로우 테이블 CPU 개입 없음 flags offload 만료/FIN 상태: SW fast path 종료/만료 HW 오프로드

진단 및 모니터링

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에서 패킷이 수신되어 전달되기까지의 모든 세부 단계를 분석합니다. 각 단계에서 수행되는 구체적인 연산과 함수 호출을 이해하면 성능 병목 지점을 정확히 파악할 수 있습니다.

SW 오프로드 Fast Path 상세 처리 경로 1. NIC RX (DMA) NAPI poll → sk_buff 생성 netif_receive_skb() 2. TC Ingress Hook sch_handle_ingress() flowtable hook 호출 3. 5-튜플 추출 nf_flow_tuple_ip(skb) src/dst IP, port, proto 파싱 실패 NF_ACCEPT → slow path 4. rhashtable 룩업 flow_offload_lookup() SipHash → RCU 조회 미스 NF_ACCEPT → slow path 히트 5. TCP 상태 검사 nf_flow_tcp_check() FIN/RST → teardown 6. NAT 재작성 nf_flow_nat_ip() IP + L4 체크섬 갱신 7. TTL 감소 ip_decrease_ttl() IP 체크섬 재계산 8. 경로 설정 skb_dst_set_noref() 캐시된 dst_entry 사용 9. MAC 재작성 ether_addr_copy() 캐시된 next-hop MAC 10. TX dev_queue_xmit() NF_STOLEN 반환 timeout 갱신 (매 패킷) flow->timeout = now + 30s Fast Path 성능 이점 (건너뛰는 처리) ✗ NF_HOOK(PREROUTING) — 훅 체인 순회 ✗ nf_conntrack_in() — conntrack 해시 조회 ✗ nft_do_chain() — 규칙 평가 루프 ✗ ip_route_input() — FIB 라우팅 조회 ✗ nf_nat_packet() — NAT 상태 머신 ✗ NF_HOOK(POSTROUTING) — 훅 체인 순회 CPU 사이클 비교 (1 패킷 당, x86_64 기준) 일반 Netfilter: ~3,500 cycles (ip_rcv→conntrack→nft_do_chain→nat→routing→xmit) SW Flowtable: ~800 cycles (tuple_extract→rhashtable_lookup→nat_inline→xmit) HW Flowtable: ~0 CPU cycles (NIC ASIC 내부 처리, DMA interrupt 없음) 범례: fast path 경로 slow path fallback NAT/TTL 변환

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 조회를 완전히 우회합니다.

패킷 전달 경로: 일반 vs Flowtable
단계 일반 경로 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와는 근본적으로 다른 방식으로 동작합니다.

SmartNIC HW 오프로드 내부 파이프라인 PHY RX 100GbE SerDes 패킷 파서 L2/L3/L4 헤더 분석 HW Flow Table TCAM/Hash 매칭 5-tuple exact match 히트 미스 CPU로 DMA 전달 SW slow path 처리 Action Pipeline NAT 변환 (ASIC) TTL 감소 체크섬 재계산 (HW) MAC 재작성 src/dst MAC 교체 PHY TX 출력 포트 전달 통계 카운터 (HW) pkts/bytes/lastused GC 폴링 (2초) SmartNIC 벤더별 HW Flowtable 구현 비교 NVIDIA/Mellanox ConnectX-5/6/7 eSwitch FDB 규칙 최대 4M 플로우 switchdev 모드 필수 Netronome (Corigine) Agilio CX/LX NFP 마이크로엔진 TC flower 매핑 프로그래머블 ASIC Broadcom Memory/Memory/Stingray MEMORY/BCM957xxx switchdev + TC flower 제한적 flowtable 지원 Intel E810 (ice 드라이버) ADQ/TC flower 지원 flowtable 부분 지원 switchdev 모드 제한적 NIC별 HW Flow Table 용량: Mellanox CX-7: ~4,000,000 exact-match 규칙 (FDB) Netronome NFP: ~2,000,000 TC flower 규칙 Broadcom: ~500,000 규칙 (모델 의존) Intel E810: ~16,000 perfect match 규칙 (제한적) HW 오프로드 상태 확인: $ tc filter show dev eth0 ingress ← 오프로드된 TC flower 규칙 $ ethtool -S eth0 | grep offload ← NIC 오프로드 통계 $ conntrack -L | grep HW ← HW 오프로드 세션 범례: NVIDIA/Mellanox Netronome Broadcom Intel

eSwitch 모드와 Flowtable

Mellanox ConnectX NIC에서 HW 오프로드를 사용하려면 eSwitch를 switchdev 모드로 전환해야 합니다. 이 모드에서 NIC의 내장 스위치가 Linux 커널의 bridge/TC 인프라와 통합됩니다.

eSwitch 모드별 Flowtable 동작
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 오프로드 제한사항

HW 오프로드 불가 조건 (SW fallback)
제한 사항 이유 동작
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()이 호출될 때부터 최종 등록/거부까지의 전체 판단 과정을 보여줍니다.

Flowtable 등록/bypass 판단 플로우차트 FORWARD chain: flow add @ft skb_sec_path()? IPSec 경로? Yes bypass (IPSec) No 단편화/멀티캐스트? IP_MF/multicast Yes bypass (파싱 불가) No nfct_help(ct)? NAT helper 존재? Yes bypass (ALG) No ct state == ESTABLISHED? No bypass (비ESTABLISHED) Yes nf_ct_is_dying? conntrack dying? Yes bypass (dying) No Flowtable 등록! flow_offload_alloc() + add() bypass 비율 (일반 환경) IPSec 경로: ~1-2% 단편화: <0.5% 멀티캐스트: ~2-5% NAT helper (FTP/SIP): <1% 비ESTABLISHED: ~5-10% (핸드셰이크) 오프로드 성공: ~80-95% (TCP 위주 환경, UDP는 더 높음) 범례: 조건 검사 bypass 등록 성공
보안 주의: Flowtable에 등록된 세션은 이후 Netfilter 규칙 변경의 영향을 받지 않습니다. 방화벽 규칙을 업데이트한 후에도 기존 오프로드 세션은 새 규칙에 의해 차단되지 않습니다. 즉시 적용이 필요하면 conntrack -F로 전체 세션을 초기화하거나 개별 세션을 삭제해야 합니다.

커널 버전별 Flowtable 진화

Flowtable은 커널 4.16에서 최초 도입된 이후 매 릴리스마다 기능이 확장되고 있습니다. 각 버전에서 추가된 핵심 기능과 변경 사항을 정리합니다.

리눅스 커널 버전별 Flowtable 주요 변경 이력
커널 버전 시기 주요 변경 관련 커밋/패치
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)와 올바르게 연동해야 합니다.

멀티코어 Flowtable: RSS/RPS 연동 아키텍처 NIC (100GbE) RX Queue 0 (RSS) RX Queue 1 (RSS) RX Queue 2 (RSS) RX Queue N (RSS) Toeplitz hash (5-tuple) CPU 0: NAPI poll + Flowtable rhashtable_lookup (RCU read) CPU 1: NAPI poll + Flowtable rhashtable_lookup (RCU read) CPU 2: NAPI poll + Flowtable rhashtable_lookup (RCU read) CPU N: NAPI poll + Flowtable rhashtable_lookup (RCU read) nf_flowtable 공유 rhashtable RCU read: 잠금 없음 쓰기: per-bucket spinlock NIC TX TX Queue 0 TX Queue 1 TX Queue 2 TX Queue N 멀티코어 스케일링 특성 장점: • RCU read-side: CPU 간 잠금 경쟁 없음 → 코어 수에 비례한 선형 확장 • RSS가 같은 세션을 같은 CPU로 매핑 → L1/L2 캐시 히트율 향상 • flow->timeout 갱신: atomic 연산 불필요 (자기 CPU에서만 갱신) 주의점: • rhashtable resize: 잠금 경쟁 발생 (짧은 시간 동안) • flow_offload_add/del: per-bucket spinlock (삽입/삭제 시) • GC 워커: 단일 워크큐에서 실행 → 대규모 환경에서 CPU 편중 최적 설정: • RSS 큐 수 = CPU 코어 수 (ethtool -L eth0 combined N) • IRQ affinity: /proc/irq/NNN/smp_affinity를 코어별 배분 성능 벤치마크 (8코어 기준): • 1코어: ~7.5 Mpps (SW flowtable) • 8코어: ~55 Mpps (선형에 가까운 확장, ~7.3x)

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 간 메모리 접근 레이턴시
 */
NUMA 환경에서의 Flowtable 최적화
최적화 항목 설정 효과
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에서 독립적으로 설정할 수 있지만, 호스트와 컨테이너 간 패킷 경로가 복잡해져 설정에 주의가 필요합니다.

컨테이너 환경에서의 Flowtable 적용 시나리오 호스트 네임스페이스 (init_net) eth0 (WAN) 물리 NIC 호스트 Flowtable devices={eth0, docker0} docker0 (bridge) 172.17.0.1/16 fast path veth pair veth_host_0 NAT (MASQ) POSTROUTING SNAT Kubernetes 환경 kube-proxy iptables/nftables 규칙 Service → Pod DNAT 변환 주의: kube-proxy 규칙 수백~수천 개 Flowtable으로 ESTABLISHED 세션 가속 Cilium: eBPF 기반 (Flowtable 불필요) 컨테이너 네임스페이스 (netns) Container 1 (netns1) eth0 (veth) 172.17.0.2 컨테이너 내부 Flowtable 가능 컨테이너 내 nftables 설정 가능 (CAP_NET_ADMIN 필요) Container 2 (netns2) eth0 (veth) 172.17.0.3 주의: veth 제한 ingress hook 위치 veth ingress에서 flowtable 훅 컨테이너 간 통신 가속 컨테이너 Flowtable 권장 ✓ 호스트 네임스페이스에서 설정 ✓ docker0/br-xxx를 devices에 포함 ✓ 컨테이너→외부 SNAT 세션 가속 ✗ 컨테이너 내부 flowtable 비추천 ✗ macvlan 컨테이너 제한적 지원 Cilium eBPF가 더 효율적

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

Kubernetes CNI별 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
    }
}
PPPoE 주의사항:
  • Flowtable devices에는 ppp0이 아닌 물리 인터페이스(eth0)를 지정합니다.
  • PPPoE MTU는 일반적으로 1492 바이트입니다. PMTU Discovery가 올바르게 동작하는지 확인하세요.
  • PPPoE 세션 재연결 시 flowtable의 기존 플로우는 자동으로 GC됩니다 (dst_entry obsolete).
  • PPPoE + VLAN 조합(ISP 환경)은 커널 5.19+에서 안정적입니다.

DSL/PPPoE MTU와 Flowtable 상호작용

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 보안 위험 시나리오와 대응 위험 1: 규칙 변경 무효화 방화벽 규칙 업데이트 후에도 기존 오프로드 세션은 새 규칙 무시. 대응: conntrack -F 또는 conntrack -D -s 특정IP 위험 2: IDS/IPS 우회 Flowtable fast path는 NFQUEUE, LOG 타겟을 건너뛰므로 IDS가 ESTABLISHED 패킷을 검사 불가. 대응: IDS 대상 트래픽 제외 설정 위험 3: DPI/QoS 우회 L7 DPI(Deep Packet Inspection)나 QoS 분류가 fast path에서 생략됨. 트래픽 셰이핑 정책 무효화 가능. 대응: TC qdisc는 여전히 동작 위험 4: 감사 로깅 누락 nftables log/counter 규칙이 fast path에서 실행되지 않아 패킷별 로깅 불가. 대응: conntrack 이벤트 기반 로깅 위험 5: 세션 하이재킹 오프로드 세션의 30s timeout 내에 같은 5-tuple의 스푸핑 패킷이 들어오면 세션 갱신 가능. 대응: TCP sequence 검증 (제한적) 위험 6: ACL 시간차 공격 정상 요청으로 연결 수립 후 오프로드된 세션을 악용하여 차단 대상 데이터 전송. 대응: 민감 서비스 flowtable 제외 보안 권장 사항 (Best Practices) 1. 민감 서비스(금융, 의료) 트래픽은 Flowtable에서 제외: 별도 chain에서 accept 처리 2. 방화벽 규칙 변경 시 conntrack -F 실행 (또는 nft flush ruleset 후 재적용) 3. conntrack 이벤트 로깅 활성화: conntrack -E -e NEW,DESTROY | logger 4. IDS/IPS가 필요한 환경에서는 해당 트래픽을 flow add 대상에서 제외

선택적 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 변환 전후 주소 확인

흔한 실수 목록

Flowtable 설정 시 흔한 실수와 해결법
실수 증상 해결
devices에 한쪽 인터페이스만 등록 한 방향만 오프로드 양쪽 인터페이스 모두 devices에 추가
flow add 위치가 ct state established accept 뒤에 flow add에 도달하지 않음 flow addct 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

메모리 사용량 계산 및 최적화

Flowtable 메모리 사용량 (세션 수별)
동시 세션 수 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 메모리 ≈ 세션 수 × 1KB
conntrack 메모리 ≈ 세션 수 × 0.5KB
rhashtable 버킷 ≈ 세션 수 / 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) 등 고성능 패킷 처리 메커니즘이 있습니다. 각 기술의 동작 계층, 프로그래밍 모델, 성능 특성을 비교합니다.

패킷 처리 계층별 가속 기술 비교 처리 계층 (낮을수록 빠름) NIC HW ASIC/FPGA HW Flowtable ndo_flow_offload TC HW offload TC flower in_hw XDP HW offload eBPF on NIC XDP 드라이버 레벨 XDP native ~20 Mpps/core XDP generic ~5 Mpps/core TC Ingress sk_buff 레벨 SW Flowtable ~7.5 Mpps/core TC eBPF ~10 Mpps/core TC flower ~3 Mpps/core Netfilter L3/L4 스택 nftables ~1.8 Mpps/core iptables ~1.5 Mpps/core 기술별 장단점 비교 기술 프로그래밍 conntrack NAT 지원 설정 난이도 적합 환경 SW Flowtable nftables 설정 완전 통합 완전 지원 쉬움 라우터/방화벽 XDP eBPF C 코드 별도 구현 직접 구현 어려움 로드밸런서/DDoS TC eBPF eBPF C 코드 접근 가능 직접 구현 보통 CNI/서비스메시 HW Flowtable nftables + NIC 완전 통합 완전 지원 보통 고성능 NGFW DPDK 유저스페이스 C 없음 직접 구현 매우 어려움 통신장비/VNF

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 워크큐의 동기화를 이해하면 성능 특성과 한계를 정확히 파악할 수 있습니다.

RCU 보호 rcu_read_lock() ← 읽기 경로 (fast path) per-bucket spinlock bucket_lock() ← 삽입/삭제 (rhashtable 내부) ~50ns mutex (resize) rhashtable_change() ← 크기 조정 (resize 워크큐에서 실행) ~1ms (드물게 발생) workqueue (GC, HW offload) system_power_efficient_wq, system_unbound_wq 2초 주기
/*
 * 동시성 보장:
 * - 읽기 (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);
}
Flowtable 동기화 연산별 비용
연산 동기화 방식 비용 (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초 주기
timeout 갱신이 atomic이 아닌 이유: flow->timeoutu64 필드로 단순 쓰기입니다. 여러 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 호환성
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
    }
}
WireGuard 성능 팁: WireGuard의 암호화/복호화는 CPU에서 처리되므로 이 구간에서는 Flowtable이 도움이 되지 않습니다. 하지만 복호화 후 LAN으로의 포워딩 구간에서 Flowtable을 적용하면 nftables 규칙 재평가를 건너뛰어 VPN 처리량을 10-30% 향상시킬 수 있습니다.

Netfilter Flowtable과 관련된 다른 주제를 더 깊이 이해하고 싶다면 다음 문서를 참고하세요.