NFQUEUE & DPI 엔진 통합

리눅스 커널 nfnetlink_queue 내부 구조, libnetfilter_queue API, Suricata/nDPI/Snort IPS 통합, NFQUEUE 성능 최적화(fanout/busy polling/batch), eBPF L7 분류, NGFW DPI 역할 종합 가이드. 커널 내부 데이터 경로, 핵심 자료구조/API, 운영 환경 튜닝 포인트와 장애 디버깅 절차까지 실무 관점으로 다룹니다.

전제 조건: Netfilter 프레임워크네트워크 스택 문서를 먼저 읽으세요. NFQUEUE는 Netfilter 훅에서 패킷을 유저스페이스로 보내는 메커니즘이므로 훅 시스템 이해가 필수입니다.
일상 비유: NFQUEUE는 공항 보안 검색대와 같습니다. 일반 방화벽(iptables/nftables)이 X선 스캔(빠른 헤더 검사)이라면, NFQUEUE+DPI는 짐을 열어 내용물을 직접 확인하는 정밀 검사입니다. 시간이 걸리지만 훨씬 정교한 판단이 가능합니다.

핵심 요약

  • NFQUEUE는 커널에서 패킷을 유저스페이스 DPI 엔진으로 전달하는 Netfilter 타겟
  • nfnetlink_queue 커널 모듈이 커널↔유저 간 통신 채널 제공
  • libnetfilter_queue 라이브러리로 유저스페이스에서 패킷 수신/판정
  • Suricata IPS 모드, nDPI, Snort 3가 NFQUEUE를 통해 L7 분류 수행
  • fanout 모드로 다중 큐·다중 스레드 처리로 성능 확장
  • eBPF 소켓 필터로 L7 초고속 분류 가능 (커널 내부에서 처리)
  • DPI는 암호화 트래픽(TLS 1.3)에서 한계 — JA3/SNI/JARM 지문으로 보완
  • NGFW에서 DPI는 IPS/IDS, URL 필터링, 애플리케이션 식별에 핵심
  • Intel Hyperscan(SIMD 정규식 엔진)으로 Aho-Corasick 대비 패턴 매칭 수십 배 가속
  • QUIC/HTTP3는 UDP+TLS 1.3 구조로 기존 DPI 기법 적용이 어려워 별도 파서 필요
  • --queue-balance와 RSS로 다수의 NIC 큐를 여러 Worker 프로세스에 분산하는 클러스터 패턴 지원
  • verdict 실패 시 NFQA_CFG_F_FAIL_OPEN으로 fail-open 또는 fail-closed 정책 선택 가능

단계별 이해

  1. Netfilter 타겟 이해: NFQUEUE는 ACCEPT/DROP 대신 패킷을 유저스페이스 큐로 전달하는 특수 타겟입니다. 커널은 판정을 받을 때까지 패킷을 nf_queue_entry에 보관합니다.
  2. 커널 채널 파악: nfnetlink_queue 모듈이 netlink 소켓을 통해 패킷 데이터를 유저스페이스에 전달합니다. 큐가 가득 차면 drop 또는 fail-open 정책이 적용됩니다.
  3. 유저스페이스 처리: DPI 엔진이 libnetfilter_queue로 패킷을 받아 L7 분류 후 ACCEPT/DROP/MARK 판정을 커널로 반환합니다. nfq_set_verdict2()로 mark 값도 함께 설정합니다.
  4. 성능 고려: 커널↔유저 복사 비용이 있으므로 fanout, batch, bypass 전략을 조합합니다. GSO 플래그로 분할 없이 원본 패킷을 전달하면 CPU 부하가 줄어듭니다.
  5. 고성능 패턴 매칭: Intel Hyperscan은 SIMD 명령어로 수천 개의 정규식을 병렬 매칭합니다. Snort 3와 Suricata가 기본 엔진으로 채택하여 패턴 매칭 속도를 크게 향상시킵니다.
  6. eBPF 보완: 이미 분류된 세션은 eBPF map으로 커널 내에서 고속 처리합니다. TC eBPF로 flow_cache를 조회해 캐시 히트 시 NFQUEUE를 완전히 우회합니다.
  7. QUIC 대응: QUIC Initial Packet의 Client Hello에서 SNI를 추출하고 Connection ID로 세션을 추적합니다. 암호화 이후 페이로드는 분석 불가능합니다.

개요: NGFW와 DPI

차세대 방화벽(NGFW)은 단순한 L3/L4 패킷 필터를 넘어 애플리케이션 계층(L7)까지 검사합니다. 리눅스 커널은 NFQUEUE 메커니즘을 통해 이를 구현합니다.

전통적 방화벽 vs NGFW

구분전통적 방화벽NGFW (DPI 포함)
검사 계층L2~L4 (IP/Port)L2~L7 (애플리케이션까지)
상태 추적5-tuple 기반애플리케이션 컨텍스트 포함
암호화 대응없음TLS 인터셉트 또는 지문 분석
IPS/IDS별도 장비 필요인라인 통합
처리량 오버헤드거의 없음10~30% (DPI 우회 최적화로 감소)
리눅스 구현iptables/nftablesNFQUEUE + Suricata/nDPI

NGFW 패킷 처리 흐름

NIC 수신 Netfilter PREROUTING FORWARD NFQUEUE nfnetlink_queue 패킷 큐잉 판정 대기 DPI 엔진 (유저스페이스) Suricata IPS nDPI 라이브러리 Snort 3 → ACCEPT → DROP → MARK → REDIRECT 정책 적용 계속/차단/로그 netlink 소켓 verdict 반환

NFQUEUE 커널 내부 함수 호출 경로

Kernel Space User Space ── Kernel / User Boundary ── NF_HOOK() nf_hook_slow() nf_queue() nf_queue_entry_get_refs() skb, conntrack 참조 카운트 증가 nfqnl_enqueue_packet() 큐 인스턴스에 패킷 등록 nfqnl_build_packet_message() Netlink skb 구성 (NFQA_* 속성) netlink_unicast() 유저 공간으로 전달 libnetfilter_queue recv() → 패킷 검사 → verdict 전송 DPI Engine (Suricata, nDPI, …) verdict (ACCEPT/DROP/...) nfqnl_recv_verdict() Netlink 콜백 수신 nfqnl_set_verdict() verdict 값 파싱·적용 nf_reinject() Netfilter 훅 체인 복귀 Netfilter Hook Chain 다음 훅 함수 계속 실행 kfree_skb() ← NF_DROP

위 다이어그램은 패킷이 NFQUEUE 타겟을 만났을 때 커널 내부에서 일어나는 전체 함수 호출 경로입니다. NF_HOOK() 매크로에서 시작하여 nf_hook_slow()가 등록된 훅 함수를 순회하다 NFQUEUE verdict를 만나면 nf_queue()가 호출됩니다. nfqnl_enqueue_packet()에서 Netlink 메시지를 구성한 뒤 netlink_unicast()로 유저 공간에 전달합니다. 유저 공간에서 verdict가 돌아오면 nfqnl_recv_verdict()nf_reinject() 경로로 패킷이 Netfilter 훅 체인으로 복귀합니다.

nfnetlink_queue 내부 구조

nfnetlink_queue는 Netfilter의 NFQUEUE 타겟을 구현하는 커널 모듈입니다. 패킷을 유저스페이스로 전달하고 판정(verdict)을 받아 처리합니다. 커널 내부적으로 nfqnl_instance 구조체가 큐 하나를 표현하며, netlink 소켓을 통해 유저스페이스와 비동기로 통신합니다.

핵심 자료구조 — nfqnl_instance 전체 필드

/* net/netfilter/nfnetlink_queue.c */
struct nfqnl_instance {
    struct hlist_node   hlist;          /* 인스턴스 해시 테이블 (queue_num 해시) */
    struct rcu_head     rcu;            /* RCU 해제용 콜백 */
    u_int16_t           queue_num;      /* 큐 번호 (0~65535) — NFQUEUE --queue-num */
    u_int8_t            copy_mode;      /* NFQNL_COPY_NONE/META/PACKET */
    u_int32_t           copy_range;     /* nfq_set_mode() 지정 최대 복사 바이트 */
    u_int32_t           queue_maxlen;   /* 큐 최대 크기 (기본 1024, 최대 65535) */
    atomic_t            queue_total;    /* 현재 큐잉된 패킷 수 */
    atomic_t            queue_dropped;  /* 큐 포화로 드롭된 패킷 수 */
    atomic_t            queue_user_dropped; /* 유저 verdict 오류로 드롭된 수 */
    struct sk_buff_head skb_queue;      /* 대기 중인 netlink 응답 큐 */
    struct sock         *peer_sk;       /* 연결된 유저스페이스 netlink 소켓 */
    u_int32_t           peer_portid;    /* 유저 프로세스 netlink portid */
    u_int32_t           id_sequence;   /* 모노토닉 패킷 ID 생성 카운터 */
    struct list_head    queue_list;     /* nf_queue_entry 판정 대기 목록 */
    spinlock_t          lock;           /* queue_list, queue_total 보호 */
    unsigned int        queue_flags;    /* NFQA_CFG_F_* 비트필드 */
    u_int32_t           flags;          /* 내부 상태 플래그 */
};

/* copy_mode 값 */
#define NFQNL_COPY_NONE   0   /* 패킷 데이터 전달 안 함 (메타만) */
#define NFQNL_COPY_META   1   /* 패킷 헤더만 전달 */
#define NFQNL_COPY_PACKET 2   /* copy_range 바이트까지 전달 */

nfqnl_recv_config() 흐름 — 유저 설정 적용

/* 유저스페이스에서 nfq_set_mode() 호출 시 커널 경로 */
/* nfq_set_mode() → netlink 메시지 → nfqnl_recv_config() */
static int nfqnl_recv_config(struct sock *ctnl, struct sk_buff *skb,
                              const struct nlmsghdr *nlh, ...)
{
    u16 queue_num = ntohs(nfmsg->res_id);
    struct nfqnl_instance *queue = instance_lookup(queue_num);

    if (!queue) {
        /* 큐 없으면 신규 생성 */
        queue = instance_create(queue_num, NETLINK_CB(skb).portid, net);
    }

    if (nfqa[NFQA_CFG_PARAMS]) {
        struct nfqnl_msg_config_params *params =
            nla_data(nfqa[NFQA_CFG_PARAMS]);
        nfqnl_set_mode(queue, params->copy_mode,
                       ntohl(params->copy_range));
    }

    if (nfqa[NFQA_CFG_QUEUE_MAXLEN]) {
        __be32 *v32 = nla_data(nfqa[NFQA_CFG_QUEUE_MAXLEN]);
        queue->queue_maxlen = ntohl(*v32);
    }

    if (nfqa[NFQA_CFG_FLAGS]) {
        /* NFQA_CFG_F_FAIL_OPEN 등 설정 */
        __be32 flags = nla_get_be32(nfqa[NFQA_CFG_FLAGS]);
        __be32 mask  = nla_get_be32(nfqa[NFQA_CFG_MASK]);
        queue->flags = (ntohl(queue->flags) & ~ntohl(mask)) |
                       (ntohl(flags) & ntohl(mask));
    }
}

큐 버퍼 관리 — nfqnl_flush()

/* 유저 프로세스 종료 시 미처리 패킷 일괄 해제 */
static void nfqnl_flush(struct nfqnl_instance *queue,
                         nfqnl_cmpfn cmpfn, unsigned long data)
{
    struct nf_queue_entry *entry, *next;

    spin_lock_bh(&queue->lock);
    list_for_each_entry_safe(entry, next, &queue->queue_list, list) {
        if (!cmpfn || cmpfn(entry, data)) {
            list_del(&entry->list);
            atomic_dec(&queue->queue_total);
            /* 정책: fail-open이면 ACCEPT, 아니면 DROP */
            if (queue->flags & NFQA_CFG_F_FAIL_OPEN)
                nf_reinject(entry, NF_ACCEPT);
            else
                nf_reinject(entry, NF_DROP);
        }
    }
    spin_unlock_bh(&queue->lock);
}

/* skb를 유저스페이스로 복사하는 내부 경로 */
/* nfqnl_build_packet_message():
   1. nlmsg_new()로 netlink 버퍼 할당
   2. nfqnl_put_sk_uidgid() — UID/GID 추가 (NFQA_CFG_F_UID_GID)
   3. nfqnl_put_bridge() — bridge 포트 정보
   4. nfq_put_ct_info() — conntrack 정보 (NFQA_CFG_F_CONNTRACK)
   5. skb_copy_bits() → NFQA_PAYLOAD nlattr에 패킷 데이터 복사
   6. netlink_unicast() → 유저 소켓으로 전송
*/
플래그설명
NFQA_CFG_F_FAIL_OPEN0x01큐 포화 시 DROP 대신 ACCEPT (Fail-open)
NFQA_CFG_F_CONNTRACK0x02conntrack 정보 함께 전달
NFQA_CFG_F_GSO0x04GSO 패킷 분할 없이 원본 전달
NFQA_CFG_F_UID_GID0x08소켓 소유자 UID/GID 포함
NFQA_CFG_F_SECCTX0x10LSM 보안 컨텍스트 포함

nf_queue_entry 구조체 상세

NFQUEUE에 대기 중인 각 패킷은 struct nf_queue_entry로 관리됩니다. 이 구조체는 원본 패킷(skb)뿐 아니라 해당 패킷이 Netfilter 훅 체인의 어느 지점에서 큐에 들어갔는지(state)를 기록하여, verdict 수신 후 정확한 위치에서 처리를 재개할 수 있게 합니다.

/* include/net/netfilter/nf_queue.h */
struct nf_queue_entry {
    struct list_head       list;       /* 큐 연결 리스트 노드 */
    struct sk_buff         *skb;       /* 대기 중인 원본 패킷 버퍼 */
    unsigned int           id;         /* 큐 내 고유 패킷 식별자 (유저 공간 verdict 매칭용) */
    unsigned int           hook;       /* 패킷이 큐에 진입한 Netfilter 훅 포인트 (NF_INET_*) */
    struct nf_hook_state   state;      /* 훅 실행 상태 전체 스냅샷 */
    u16                    size;       /* 유저 공간에 복사할 페이로드 크기 (copy_range) */
    u16                    reason;     /* 큐 진입 사유 (verdict 비트 디코딩) */
    struct nf_hook_entry  *hook_entry; /* 큐 진입 시점의 훅 엔트리 (reinject 복귀 지점) */
};

/* nf_hook_state: 훅 실행 컨텍스트 전체 보존 */
struct nf_hook_state {
    u8                     hook;       /* NF_INET_PRE_ROUTING, NF_INET_LOCAL_IN 등 */
    u8                     pf;         /* 프로토콜 패밀리 (NFPROTO_IPV4/IPV6) */
    struct net_device     *in;         /* 입력 네트워크 디바이스 */
    struct net_device     *out;        /* 출력 네트워크 디바이스 */
    struct sock           *sk;         /* 연관 소켓 (있는 경우) */
    struct net            *net;        /* 네트워크 네임스페이스 */
    int                  (*okfn)(struct net *, struct sock *, struct sk_buff *);
                                        /* 훅 체인 통과 후 호출할 함수 (ip_rcv_finish 등) */
};
참조 카운트 관리: nf_queue_entry_get_refs()는 큐 진입 시 skb, in/out 디바이스, conntrack 엔트리(nf_ct)의 참조 카운트를 모두 증가시킵니다. 이는 패킷이 유저 공간 verdict를 기다리는 동안 관련 커널 객체가 해제되는 것을 방지합니다. verdict 처리 후 nf_queue_entry_free()에서 모든 참조를 정리합니다.
copy_mode와 size 필드: NFQNL_COPY_PACKET 모드에서 size 값이 0이면 전체 패킷을, 그 외에는 지정된 바이트만큼만 유저 공간에 복사합니다. 메타데이터만 필요한 경우 NFQNL_COPY_META를 사용하면 성능이 크게 향상됩니다.

NFQUEUE는 Netlink 소켓(NETLINK_NETFILTER)을 통해 커널과 유저 공간 사이에서 패킷 데이터를 교환합니다. 아래 다이어그램은 하나의 NFQUEUE 메시지가 Netlink 프로토콜 위에서 어떤 바이너리 레이아웃으로 구성되는지 보여줍니다.

NFQUEUE Netlink Message Layout nlmsghdr len | type | flags | seq | pid 16 bytes nfgenmsg family | version | res_id 4 bytes NLA (Netlink Attributes) — TLV 체인 NFQA_PACKET_HDR → NFQA_PAYLOAD → NFQA_CT → NFQA_UID → ... 가변 길이 nlattr TLV 구조 (각 속성 공통 포맷): nla_len 2 bytes (u16) nla_type 2 bytes (u16) nla_data (payload) 가변 길이 (4-byte 정렬 패딩 포함) padding 0~3 bytes NFQUEUE 메시지 내 주요 속성 순서: NFQA_PACKET_HDR id, hw_protocol, hook NFQA_MARK nfmark (u32) NFQA_IFINDEX_* in/out dev index NFQA_PAYLOAD 패킷 데이터 (가변) NFQA_CT conntrack NFQA_UID 소켓 UID NFQA_GID 소켓 GID NFQA_CAP_* 보안 컨텍스트 각 속성은 NLA_ALIGN(nla_len) 바이트 경계로 정렬되며, nla_type의 상위 비트로 nested/byte-order 플래그를 표현합니다. NFQA_CT는 nested 속성으로, 내부에 CTA_TUPLE_ORIG, CTA_TUPLE_REPLY, CTA_STATUS 등 conntrack 하위 속성을 포함합니다. res_id = queue number (0~65535) | nlmsg_type = (NFNL_SUBSYS_QUEUE << 8) | NFQNL_MSG_PACKET
바이트 오더: Netlink 속성의 정수 필드는 네트워크 바이트 오더(Big Endian)를 사용합니다. NFQA_PACKET_HDRpacket_idhw_protocolntohl()/ntohs()로 변환해야 합니다. libnetfilter_queuenfq_get_msg_packet_hdr() 함수가 이 변환을 자동으로 처리합니다.

NFQA_* 속성 전체 목록

NFQUEUE Netlink 프로토콜에서 사용하는 모든 NFQA_* 속성입니다. 커널 소스 include/uapi/linux/netfilter/nfnetlink_queue.h에 정의되어 있습니다.

상수타입방향설명
NFQA_UNSPEC0미사용 (속성 인덱스 시작점)
NFQA_PACKET_HDR1struct nfqnl_msg_packet_hdrK→U패킷 ID, 하드웨어 프로토콜, 훅 번호
NFQA_VERDICT_HDR2struct nfqnl_msg_verdict_hdrU→K판정 결과 (NF_ACCEPT/NF_DROP 등) 및 패킷 ID
NFQA_MARK3__be32양방향패킷의 nfmark 값 (iptables -j MARK 설정값)
NFQA_TIMESTAMP4struct nfqnl_msg_packet_timestampK→U패킷 수신 타임스탬프 (초 + 마이크로초)
NFQA_IFINDEX_INDEV5__be32K→U입력 네트워크 인터페이스 인덱스
NFQA_IFINDEX_OUTDEV6__be32K→U출력 네트워크 인터페이스 인덱스
NFQA_IFINDEX_PHYSINDEV7__be32K→U물리 입력 인터페이스 인덱스 (브릿지 환경)
NFQA_IFINDEX_PHYSOUTDEV8__be32K→U물리 출력 인터페이스 인덱스 (브릿지 환경)
NFQA_HWADDR9struct nfqnl_msg_packet_hwK→U소스 하드웨어(MAC) 주소
NFQA_PAYLOAD10바이너리 데이터양방향패킷 페이로드 (copy_range 크기만큼, verdict 시 수정 데이터)
NFQA_CT11nested (CTA_*)K→Uconntrack 정보 (NFQA_CFG_F_CONNTRACK 설정 시)
NFQA_CT_INFO12__be32K→Uconntrack 상태 (IP_CT_NEW, IP_CT_ESTABLISHED 등)
NFQA_CAP_LEN13__be32K→U원본 패킷의 실제 길이 (copy_range로 잘린 경우)
NFQA_SKB_INFO14__be32K→USKB 메타 정보 (GSO 타입, 체크섬 상태 등 비트 플래그)
NFQA_EXP15nestedK→Uconntrack expectation 정보
NFQA_UID16__be32K→U패킷 소유 소켓의 UID (NFQA_CFG_F_UID_GID 설정 시)
NFQA_GID17__be32K→U패킷 소유 소켓의 GID (NFQA_CFG_F_UID_GID 설정 시)
NFQA_SECCTX18문자열K→USELinux 보안 컨텍스트 (NFQA_CFG_F_SECCTX 설정 시)
NFQA_VLAN19nestedK→UVLAN 태그 정보 (NFQA_VLAN_TCI, NFQA_VLAN_PROTO 포함)
NFQA_L2HDR20바이너리 데이터K→ULayer 2 헤더 (NFQA_CFG_F_GSO 설정 시, Ethernet 헤더 포함)
NFQA_PRIORITY21__be32K→U패킷 우선순위 (skb->priority)
NFQA_CGROUP22__be32K→U소켓의 cgroup classid (net_cls)
선택적 속성 활성화: NFQA_CT, NFQA_UID/NFQA_GID, NFQA_SECCTX 등은 기본적으로 포함되지 않습니다. NFQA_CFG_CMD로 큐 바인딩 후, NFQA_CFG_FLAGSNFQA_CFG_MASK를 사용하여 해당 플래그를 명시적으로 활성화해야 합니다.
NFQA_PAYLOAD 수정 시 주의: verdict 메시지에 NFQA_PAYLOAD를 포함하면 커널이 원본 skb의 데이터를 교체합니다. 이때 IP/TCP 체크섬은 자동 재계산되지 않으므로, 유저 공간에서 직접 체크섬을 갱신하거나 NFQA_SKB_INFONFQA_SKB_CSUMNOTREADY 플래그를 확인하여 하드웨어 체크섬 오프로드를 활용해야 합니다.

libnetfilter_queue API

libnetfilter_queue는 유저스페이스 DPI 엔진이 NFQUEUE와 통신하는 라이브러리입니다. 내부적으로 libmnl 위에 구축되어 있으며, 3개의 핵심 핸들 구조체를 중심으로 동작합니다.

핵심 구조체 관계도

/*
 * nfq_handle      — 라이브러리 전역 핸들 (nfq_open()으로 생성)
 *   └─ nfq_q_handle — 개별 큐 핸들 (nfq_create_queue()로 생성, 큐번호 1:1)
 *        └─ nfq_data  — 개별 패킷 데이터 (콜백 호출 시 전달)
 *
 * nfq_handle: netlink fd 래핑, 프로토콜패밀리(AF_INET/AF_INET6) 바인딩
 * nfq_q_handle: 큐번호, 콜백함수 포인터, copy_mode 보관
 * nfq_data: 해당 패킷의 헤더/페이로드/메타데이터 접근자 묶음
 */

완전한 C DPI 엔진 예제 (수신→파싱→판정)

#include <libnetfilter_queue/libnetfilter_queue.h>
#include <libnetfilter_queue/pktbuff.h>
#include <linux/netfilter.h>
#include <linux/ip.h>
#include <linux/tcp.h>
#include <sys/socket.h>

/* conntrack 메타데이터 출력 */
static void print_conntrack(struct nfq_data *nfa) {
    struct nfqnl_msg_packet_hw *hw;
    if ((hw = nfq_get_packet_hw(nfa))) {
        printf("HW addr: %02x:%02x:%02x:%02x:%02x:%02x\n",
               hw->hw_addr[0], hw->hw_addr[1], hw->hw_addr[2],
               hw->hw_addr[3], hw->hw_addr[4], hw->hw_addr[5]);
    }
}

/* 패킷 판정 콜백 */
static int packet_callback(struct nfq_q_handle *qh,
                             struct nfgenmsg *nfmsg,
                             struct nfq_data *nfa, void *data)
{
    struct nfqnl_msg_packet_hdr *ph = nfq_get_msg_packet_hdr(nfa);
    uint32_t id = ntohl(ph->packet_id);
    unsigned char *payload;
    int len = nfq_get_payload(nfa, &payload);

    /* UID/GID 정보 (NFQA_CFG_F_UID_GID 활성화 시) */
    uint32_t uid, gid;
    if (nfq_get_uid(nfa, &uid) == 0)
        printf("UID=%u GID=%u\n", uid, gid);

    /* IP 헤더 파싱 */
    if (len < (int)sizeof(struct iphdr))
        return nfq_set_verdict(qh, id, NF_ACCEPT, 0, NULL);

    struct iphdr *ip = (struct iphdr *)payload;
    int ip_hlen = ip->ihl * 4;

    uint32_t verdict = NF_ACCEPT;
    if (ip->protocol == IPPROTO_TCP && len > ip_hlen + 20) {
        struct tcphdr *tcp = (struct tcphdr *)(payload + ip_hlen);
        int tcp_hlen = tcp->doff * 4;
        unsigned char *app = payload + ip_hlen + tcp_hlen;
        int app_len = len - ip_hlen - tcp_hlen;

        if (app_len >= 4) {
            if (memcmp(app, "GET ", 4) == 0 ||
                memcmp(app, "POST", 4) == 0) {
                verdict = check_url_policy(app, app_len);
            }
        }
    }

    /* verdict2: mark + verdict 동시 설정 */
    if (verdict == NF_ACCEPT)
        return nfq_set_verdict2(qh, id, NF_ACCEPT, 0x1, 0, NULL);
    else
        return nfq_set_verdict(qh, id, NF_DROP, 0, NULL);
}

int main(void) {
    struct nfq_handle *h = nfq_open();
    nfq_unbind_pf(h, AF_INET);
    nfq_bind_pf(h, AF_INET);

    struct nfq_q_handle *qh = nfq_create_queue(h, 0, &packet_callback, NULL);
    nfq_set_mode(qh, NFQNL_COPY_PACKET, 0xffff);
    nfq_set_queue_flags(qh, NFQA_CFG_F_FAIL_OPEN, NFQA_CFG_F_FAIL_OPEN);
    nfq_set_queue_flags(qh, NFQA_CFG_F_CONNTRACK, NFQA_CFG_F_CONNTRACK);
    nfq_set_queue_flags(qh, NFQA_CFG_F_UID_GID, NFQA_CFG_F_UID_GID);

    int fd = nfq_fd(h);
    /* SO_RCVBUF 튜닝 — 수신 버퍼 확대 */
    int rcvbuf = 4 * 1024 * 1024;
    setsockopt(fd, SOL_SOCKET, SO_RCVBUF, &rcvbuf, sizeof(rcvbuf));

    char buf[65536] __attribute__((aligned(16)));
    while (1) {
        int rv = recv(fd, buf, sizeof(buf), 0);
        if (rv > 0)
            nfq_handle_packet(h, buf, rv);
    }
    nfq_destroy_queue(qh);
    nfq_close(h);
}

Python bindings — scapy + nfqueue

# pip install scapy netfilterqueue
from netfilterqueue import NetfilterQueue
from scapy.all import IP, TCP, Raw

def process_packet(pkt):
    scapy_pkt = IP(pkt.get_payload())

    if TCP in scapy_pkt and Raw in scapy_pkt:
        payload = bytes(scapy_pkt[Raw])
        if payload.startswith(b"GET ") or payload.startswith(b"POST "):
            # HTTP 트래픽 허용, mark 설정
            pkt.set_mark(1)
            pkt.accept()
            return

    pkt.accept()   # 기본: 허용

nfqueue = NetfilterQueue()
nfqueue.bind(0, process_packet, max_len=10000, mode=NetfilterQueue.COPY_PACKET)
try:
    nfqueue.run()
except KeyboardInterrupt:
    pass
nfqueue.unbind()

Rust bindings — nfq crate 예제

// Cargo.toml: nfq = "0.5"
use nfq::{Queue, Verdict};

fn main() -> std::io::Result<()> {
    let mut queue = Queue::open()?;
    queue.bind(0)?;   // 큐 번호 0에 바인딩

    loop {
        let mut msg = queue.recv()?;
        let payload = msg.get_payload();

        // IP 헤더 최소 길이 확인
        let verdict = if payload.len() >= 20 {
            let proto = payload[9];
            if proto == 6 {   // TCP
                let ihl = (payload[0] & 0x0f) as usize * 4;
                let tcp_off = (payload[ihl + 12] >> 4) as usize * 4;
                let app = &payload[ihl + tcp_off..];
                if app.starts_with(b"GET ") || app.starts_with(b"POST") {
                    msg.set_nfmark(1);  // mark 설정
                }
            }
            Verdict::Accept
        } else {
            Verdict::Accept
        };

        msg.set_verdict(verdict);
        queue.verdict(msg)?;
    }
}

verdict 종류

verdict설명
NF_ACCEPT1패킷 계속 전달
NF_DROP0패킷 폐기
NF_STOLEN2패킷 소유권 이전 (재주입 등)
NF_QUEUE3다른 큐로 재전송
NF_REPEAT4Netfilter 훅 재실행
마크 + ACCEPT-nfq_set_verdict2()로 mark 설정 후 ACCEPT

DPI 엔진 통합 (Suricata/nDPI/Snort)

주요 오픈소스 DPI 엔진의 NFQUEUE 통합 방법입니다.

Suricata IPS nDPI 라이브러리 Snort 3 (DAQ) 멀티스레드 Worker Thread 0,1,...N → Queue 0,1,...N --runmode workers --af-packet 규칙 엔진 (signatures) alert http any -> $SERVERS content:"malware.exe" App Layer 파서 HTTP/DNS/TLS/SMTP/FTP JA3/JA3S TLS 지문 라이브러리 형태 임베딩 독립 프로세스 또는 커스텀 앱 내 통합 300+ 프로토콜 감지 Netflix/YouTube/Zoom ML 기반 분류 (DGA) 흐름 단위 추적 NDPI_PROTOCOL_* nDPI_result 구조체 DAQ (Data Acquisition) daq_nfq 플러그인 snort3 -Q --daq nfq Hyperscan 정규식 엔진 PCRE 패턴 매칭 멀티패턴 고속 처리 공통: NFQUEUE (nfnetlink_queue 커널 모듈) libnetfilter_queue | netlink 소켓 | verdict 반환

Suricata IPS 모드 설정

# /etc/suricata/suricata.yaml
nfq:
  mode: repeat        # 재처리 후 verdict 설정
  repeat-mark: 1
  repeat-mask: 1
  bypass-mark: 1       # 이미 검사된 세션 bypass
  bypass-mask: 1
  route-queue: 2       # DROP 대신 다른 큐로 전달
  batchcount: 20       # 배치 판정 (성능 향상)
  fail-open: yes       # 큐 포화 시 허용
# iptables NFQUEUE 규칙 설정 (Suricata IPS)
iptables -I FORWARD -j NFQUEUE --queue-num 0 --queue-bypass
# 이미 처리된 패킷 (mark=1) 건너뜀
iptables -I FORWARD -m mark --mark 1 -j ACCEPT

# nftables로 동일 설정
nft add rule inet filter forward mark 0x1 accept
nft add rule inet filter forward queue num 0 bypass

nDPI 통합 예제

#include <ndpi_api.h>

struct ndpi_detection_module_struct *ndpi_struct;
struct ndpi_flow_struct ndpi_flow;

/* 초기화 */
ndpi_struct = ndpi_init_detection_module(ndpi_no_prefs());
ndpi_set_protocol_detection_bitmask2(ndpi_struct, &all_protocols);

/* 패킷마다 호출 */
ndpi_protocol protocol = ndpi_detection_process_packet(
    ndpi_struct, &ndpi_flow,
    ip_payload, ip_payload_len,
    timestamp_ms, NULL);

if (protocol.master_protocol == NDPI_PROTOCOL_TLS) {
    /* TLS 트래픽 — SNI/JA3로 앱 식별 */
    char *sni = ndpi_flow.protos.tls_quic.client_requested_server_name;
    enforce_tls_policy(sni, &ndpi_flow);
}

Suricata IPS 내부 파이프라인 상세

Suricata가 NFQUEUE로부터 패킷을 수신한 후 내부적으로 거치는 처리 단계를 상세히 살펴본다. 단일 패킷이 verdict를 받기까지 여러 엔진 모듈을 순차적으로 통과하며, 각 단계에서 프로토콜 분석과 시그니처 매칭이 이루어진다.

Suricata IPS 파이프라인 (NFQUEUE 모드) NFQueue Thread (recv) Packet Decoder StreamTCP Reassembly DetectEngine Multi-tenant Rules Verdict NF_ACCEPT/DROP 세부 처리 단계 (Sub-stages) ① L3/L4 Decode IP · TCP · UDP · ICMP ② Stream Reassembly TCP 세그먼트 재조립 ③ App-Layer Detection HTTP · TLS · DNS · SMB … ④ Signature Match Hyperscan MPM ⑤ Output / Logging EVE JSON · fast.log · pcap ⑥ Verdict Decision pass | drop | reject | replace ⑦ nfq_verdict_send() 커널 Netfilter로 verdict 전달 멀티 테넌트 모드: detect-engine-profile: custom, tenant-id 기반 룰셋 분리 NFQUEUE 번호별로 다른 tenant 룰을 적용하여 가상화 환경 또는 고객별 정책 분리 가능

Suricata 멀티 테넌트 NFQUEUE 설정

# /etc/suricata/suricata.yaml — 멀티 테넌트 NFQUEUE 설정

nfq:
  mode: accept           # repeat | route | accept
  fail-open: yes
  batchcount: 20

multi-detect:
  enabled: yes
  default: yes
  selector: direct       # vlan | direct | device
  loaders:
    - id: 1
      yaml: tenant-1.yaml  # 고객 A 룰셋
    - id: 2
      yaml: tenant-2.yaml  # 고객 B 룰셋

  mappings:
    - tenant-id: 1
      device: eth0         # 또는 vlan-id: 100
    - tenant-id: 2
      device: eth1

# 각 tenant별 NFQUEUE 분리 (iptables)
# iptables -I FORWARD -i eth0 -j NFQUEUE --queue-num 0
# iptables -I FORWARD -i eth1 -j NFQUEUE --queue-num 1

Snort 3 DAQ (Data Acquisition) 심화

Snort 3는 Snort 2와 달리 DAQ(Data Acquisition) 추상화 계층을 도입하여 패킷 수집 소스를 모듈화하였다. NFQUEUE 연동 시 daq_nfq 모듈을 사용하며, inspector 체인을 통해 패킷 분석이 이루어진다.

Snort 3 DAQ → Inspector Pipeline DAQ Module daq_nfq daq_pcap daq_afpacket daq_dpdk Codec Decode Manager L2 · L3 · L4 Inspector Pipeline binder → wizard → stream → service inspectors (http_inspect, ssl, dce_smb…) → normalize · port_scan IPS Rules Detection Engine Hyperscan MPM Actions allow / block / reject / log daq_nfq 모듈 상세 • libnetfilter_queue 기반, NFQUEUE verdict 직접 처리 (NF_ACCEPT / NF_DROP / NF_REPEAT) • --daq nfq --daq-var queue=0 --daq-var queue_maxlen=8192 옵션으로 큐 번호 및 길이 지정 • fail-open 지원: daq_nfq가 비정상 종료 시 패킷 자동 ACCEPT (--daq-var fail_open) • 배치 verdict: queue_maxlen과 연동하여 다수 패킷에 대해 일괄 verdict 전송 가능

Snort 3 Inspector 계층 구조

Inspector유형역할처리 순서
binderControl플로우를 적절한 서비스 inspector에 바인딩1단계
wizardControl프로토콜 자동 탐지 (magic pattern 기반)2단계
streamNetworkTCP/UDP 세션 트래킹, 스트림 재조립3단계
stream_tcpNetworkTCP 3-way handshake, 재전송 처리3-1단계
stream_udpNetworkUDP 세션 상태 관리3-2단계
http_inspectServiceHTTP/1.x, HTTP/2 파싱 및 정규화4단계
sslServiceTLS handshake 분석, JA3 추출4단계
dce_smbServiceSMB/DCE-RPC 프로토콜 분석4단계
dnsServiceDNS 쿼리/응답 파싱4단계
normalizePacketIP/TCP 정규화 (TTL, 옵션 제거 등)전처리
port_scanProbe포트 스캔 탐지후처리
appidControl애플리케이션 식별 (AppID 라이브러리)3~4단계

Snort 3 + NFQUEUE 설정 예제

-- /etc/snort/snort.lua — Snort 3 NFQUEUE 모드 설정

-- DAQ 설정: nfq 모듈 사용
daq =
{
    module_dirs =
    {
        '/usr/lib/daq/',
    },
    modules =
    {
        {
            name = 'nfq',
            mode = 'inline',
            variables =
            {
                'queue=0',             -- NFQUEUE 번호
                'queue_maxlen=8192',   -- 큐 최대 길이
                'fail_open',           -- 장애 시 ACCEPT
            },
        },
    },
}

-- Inspector 파이프라인
binder =
{
    {
        when = { proto = 'tcp', ports = '80 8080' },
        use  = { type = 'http_inspect' },
    },
    {
        when = { proto = 'tcp', ports = '443' },
        use  = { type = 'ssl' },
    },
    {
        when = { proto = 'any' },
        use  = { type = 'wizard' },
    },
}

stream = { }
stream_tcp = { policy = 'linux' }
http_inspect = { }
ssl = { }

-- IPS 정책
ips =
{
    enable_builtin_rules = true,
    include = 'rules/snort3-community.rules',
    variables =
    {
        nets = { HOME_NET = '192.168.0.0/16' },
    },
    action_override = 'alert',  -- IPS 모드에서는 'drop' 사용
}
# Snort 3 실행 (NFQUEUE 모드)
sudo snort -c /etc/snort/snort.lua --daq nfq \
  --daq-var queue=0 --daq-var queue_maxlen=8192 \
  -Q -l /var/log/snort/ -k none

# iptables NFQUEUE 룰 (Snort 3용)
sudo iptables -I FORWARD -j NFQUEUE --queue-num 0 \
  --queue-bypass --fail-open

DPI 엔진 상세 비교

NFQUEUE 기반 DPI를 구현할 때 선택할 수 있는 주요 엔진의 기능, 성능, 라이선스를 상세히 비교한다.

항목Suricata 7.xnDPI 4.xSnort 3.x
아키텍처멀티스레드 (worker 모드)라이브러리 (libndpi)모듈형 (inspector 체인)
프로토콜 탐지 수~80+ app-layer 파서~350+ 프로토콜~100+ inspector 기반
TLS JA3 지원JA3 + JA3S 기본 내장JA3 + JA3S + HASSHJA3 내장 (ssl inspector)
TLS JA4 지원JA4+ (7.0.3+, 실험적)JA4 (4.8+)미지원 (플러그인 필요)
QUIC 지원QUIC 파서 내장 (Initial 패킷 분석)QUIC/HTTP3 프로토콜 탐지제한적 (기본 탐지만)
패턴 엔진Hyperscan (Intel), AC 대체 가능Aho-Corasick 내장Hyperscan 기본, Literal Search 대체
룰 포맷Suricata Rules (ET Open/Pro)C API (콜백 기반)Snort Rules (Community/SO)
성능 (10G 기준)~3-5 Mpps (worker 모드)~8-12 Mpps (라이브러리)~2-4 Mpps (inline 모드)
메모리 사용량~2-8 GB (룰셋 크기 비례)~50-200 MB (경량)~1-4 GB (룰셋 비례)
라이선스GPL v2LGPL v3GPL v2
NFQUEUE 연동기본 내장 (nfq runmode)사용자 코드에서 직접 연동DAQ daq_nfq 모듈
Lua 스크립팅지원 (output + detect)미지원미지원 (removed in 3.x)
EVE JSON 로깅기본 내장 (상세 메타데이터)해당 없음 (라이브러리)JSON alert 출력 지원
상용 룰셋ET Pro (Proofpoint)해당 없음Snort Subscriber (Cisco Talos)
주요 사용처IDS/IPS, NSM, NGFW트래픽 분류, QoS, 방화벽 내장IDS/IPS, Cisco 생태계
DPI 엔진 선택 기준: IPS 기능이 필요하면 Suricata 또는 Snort 3를, 경량 프로토콜 분류만 필요하면 nDPI를 선택한다. nDPI는 라이브러리 형태이므로 커스텀 NFQUEUE 데몬에 직접 링크하여 최소 오버헤드로 DPI를 수행할 수 있다. Suricata는 EVE JSON 기반 풍부한 메타데이터 출력이 장점이며, Snort 3는 Cisco Talos 룰셋 생태계가 강점이다.

nftables Flowtable + NFQUEUE 연동

nftables의 flowtable 기능은 커널 내에서 conntrack 기반 패킷 오프로드를 수행하여 netfilter 훅을 우회(bypass)한다. 이를 NFQUEUE DPI와 결합하면 첫 번째 패킷만 DPI 검사를 수행하고, 분류 완료 후 후속 패킷을 flowtable으로 오프로드하여 극적인 성능 향상을 달성할 수 있다.

Flowtable + NFQUEUE DPI 오프로드 흐름 첫 번째 패킷 (DPI 분류 전) Ingress NIC PREROUTING conntrack new FORWARD → NFQUEUE DPI DPI 엔진 분류 verdict + ct mark flowtable 등록 ct mark → flow offload 후속 패킷 (분류 완료, 오프로드) Ingress NIC Flowtable Lookup ingress hook (빠른 경로) HIT! Direct Forward Egress NIC NFQUEUE 완전 우회 핵심 메커니즘 1. 첫 패킷: FORWARD 체인 → NFQUEUE → DPI 분류 → conntrack mark 설정 (예: ct mark set 0x1) 2. 분류 완료: nftables에서 ct mark 확인 → flow add @ft { ip saddr . ip daddr . ... } 3. 후속 패킷: flowtable ingress hook에서 매칭 → netfilter 훅 전체 우회 → 직접 출력 인터페이스로 전달

nftables Flowtable + NFQUEUE 설정

#!/usr/sbin/nft -f
# Flowtable + NFQUEUE DPI 연동 설정

flush ruleset

table inet filter {
    # flowtable 정의: 오프로드 대상 인터페이스
    flowtable ft {
        hook ingress priority 0
        devices = { eth0, eth1 }
        flags offload  # 하드웨어 오프로드 시도 (NIC 지원 시)
    }

    chain forward {
        type filter hook forward priority 0; policy accept;

        # 1단계: 이미 분류된 플로우는 flowtable으로 오프로드
        #   ct mark 0x1 = DPI 허용, 0x2 = DPI 차단
        ct mark 0x1 flow add @ft counter accept
        ct mark 0x2 counter drop

        # 2단계: 새 연결(미분류)만 NFQUEUE로 전달
        ct state new,established ct mark 0x0 \
            counter queue num 0-3 fanout bypass

        # 3단계: DPI가 아직 verdict 내리지 않은 패킷 허용
        ct state established counter accept
    }
}

# DPI 데몬에서 verdict 시 conntrack mark 설정:
# nfq_set_verdict2(qh, id, NF_ACCEPT, htonl(0x1), ...)
#   → ct mark = 0x1 (허용 + 오프로드)
# nfq_set_verdict(qh, id, NF_DROP, ...)
#   → ct mark = 0x2 (차단)

DPI 데몬에서 conntrack mark 설정

/* DPI verdict 시 conntrack mark를 설정하여 flowtable 오프로드 트리거 */
#include <libnetfilter_queue/libnetfilter_queue.h>

static int dpi_verdict_callback(struct nfq_q_handle *qh,
                                struct nfgenmsg *nfmsg,
                                struct nfq_data *nfa,
                                void *data)
{
    struct nfqnl_msg_packet_hdr *ph = nfq_get_msg_packet_hdr(nfa);
    uint32_t id = ntohl(ph->packet_id);
    unsigned char *payload;
    int len = nfq_get_payload(nfa, &payload);

    /* DPI 분류 수행 */
    int classification = perform_dpi(payload, len);

    switch (classification) {
    case DPI_ALLOW:
        /* ct mark = 1: 허용 + flowtable 오프로드 대상 */
        return nfq_set_verdict2(qh, id,
            NF_ACCEPT,
            htonl(0x00000001),  /* conntrack mark */
            0, NULL);

    case DPI_BLOCK:
        /* ct mark = 2: 차단 (flowtable 등록 안 함) */
        return nfq_set_verdict2(qh, id,
            NF_DROP,
            htonl(0x00000002),
            0, NULL);

    case DPI_NEED_MORE:
        /* 아직 분류 미완: mark 0 유지, 다음 패킷도 NFQUEUE */
        return nfq_set_verdict(qh, id, NF_ACCEPT, 0, NULL);
    }

    return nfq_set_verdict(qh, id, NF_ACCEPT, 0, NULL);
}
Flowtable + DPI 연동 시 주의사항:
  • 분류 시점 제한: flowtable에 등록된 후에는 NFQUEUE를 완전히 우회하므로, 중간에 프로토콜이 변경되는 터널링 트래픽(예: HTTP CONNECT 후 TLS)은 오탐 가능성이 있다.
  • 양방향 오프로드: flowtable은 양방향 플로우를 모두 오프로드하므로, 단방향만 검사하던 DPI 규칙이 있으면 우회될 수 있다.
  • NAT 필수 선행: flowtable은 PREROUTING의 NAT 처리 이후에 적용되므로, DNAT/SNAT이 먼저 완료되어야 한다.
  • 하드웨어 오프로드 제약: flags offload 사용 시 NIC 드라이버가 TC flower offload를 지원해야 하며, 미지원 NIC에서는 소프트웨어 flowtable로 폴백된다.
  • conntrack 타임아웃: flowtable 엔트리는 conntrack 타임아웃과 동기화되므로, 장기 연결(예: WebSocket)은 재분류 없이 계속 오프로드된다.
  • 로깅 제한: 오프로드된 패킷은 nftables 카운터에만 집계되고, 개별 패킷 로깅이 불가능하다.

Flowtable 오프로드 성능 비교

구성처리량 (Gbps)패킷률 (Mpps)CPU 사용률레이턴시 (μs)
NFQUEUE DPI만 사용 (매 패킷 검사)2.10.3192%~850
NFQUEUE DPI + SW flowtable8.71.2835%~120
NFQUEUE DPI + HW flowtable offload23.53.478%~15
flowtable만 사용 (DPI 없음, 참고치)25.03.695%~10
측정 환경: Intel Xeon E-2388G, 32GB DDR4, Mellanox ConnectX-6 Dx (25GbE), Linux 6.6, nftables 1.0.9. 64바이트 패킷 기준 최대 패킷률, 1518바이트 패킷 기준 처리량 측정. HW offload는 ConnectX-6의 TC flower offload 활용.

Flowtable 상태 확인 명령

# flowtable에 등록된 오프로드 플로우 확인
sudo conntrack -L -m 1   # mark=1인 플로우 (DPI 허용, 오프로드 대상)

# nftables flowtable 카운터 확인
sudo nft list ruleset | grep -A2 "flow add"

# 하드웨어 오프로드 통계 (ethtool)
sudo ethtool -S eth0 | grep "tc_flower"

# flowtable 엔트리 실시간 모니터링
sudo conntrack -E -m 1   # mark=1 이벤트 실시간 출력

NFQUEUE Fanout 멀티스레딩

단일 큐는 단일 스레드에서만 처리 가능합니다. fanout 기능으로 여러 큐에 로드 밸런싱합니다.

Fanout 설정

# 4개 큐로 fanout (CPU 코어당 1개 큐)
iptables -I FORWARD -j NFQUEUE \
    --queue-num 0:3 \          # 큐 범위 0~3
    --queue-balance \          # 5-tuple 해시로 로드 밸런싱
    --queue-bypass             # 큐 없으면 bypass

# nftables 버전
nft add rule inet filter forward \
    queue num 0-3 flags bypass,fanout
# CPU affinity 설정 — 코어 0~3에 각 스레드 고정
for i in 0 1 2 3; do
    numactl --cpunodebind=0 --physcpubind=$i \
        suricata -c /etc/suricata/suricata.yaml \
        --runmode single -q $i &
done

큐 모드 비교

모드nftables 옵션분배 방식용도
단일 큐queue num 0없음단순 DPI
fanoutqueue num 0-3 flags fanout5-tuple 해시멀티코어 DPI
round-robin--queue-balance (iptables)순차적균등 분배
bypassflags bypass-큐 없으면 ACCEPT

NFQUEUE 성능 최적화

NFQUEUE의 주요 성능 병목은 커널↔유저 컨텍스트 스위칭과 메모리 복사입니다. 단일 큐 단순 구성에서는 약 200~500Kpps 수준이지만, 최적화를 조합하면 1Mpps 이상 달성이 가능합니다.

큐 크기별 성능 수치 (참고 기준)

큐 크기(maxlen)fanout 큐 수처리량 (Kpps)평균 레이턴시 (µs)비고
1024 (기본)1200~30080~120단순 헤더 검사
81921350~50060~90SO_RCVBUF 4MB
81924800~120040~70fanout + CPU affinity
1638481500~200030~50batch verdict 20
16384 + GSO82500~350020~35NFQA_CFG_F_GSO + bypass

최적화 기법 비교

기법설명적용 방법성능 향상
배치 verdict여러 패킷 판정을 묶어서 전송batchcount: 20~30%
Busy polling블로킹 대신 폴링으로 지연 감소SO_BUSY_POLL레이턴시 50%↓
Zero-copy (GSO)GSO 분할 없이 원본 전달NFQA_CFG_F_GSOCPU 20%↓
Fail-open큐 포화 시 ACCEPT → 중단 방지NFQA_CFG_F_FAIL_OPEN안정성
Conntrack bypass기존 세션은 DPI 건너뜀mark + ACCEPT 규칙~70%
eBPF pre-filterL4 이하는 eBPF로 조기 필터링XDP/TC eBPFDPI 부하 60%↓

CPU 코어 fanout 바인딩 — IRQ affinity

# NIC IRQ를 CPU 0~3에 분산 (RSS 4개 큐 기준)
ethtool -l eth0   # RX 큐 수 확인
ethtool -L eth0 combined 4   # 4개 큐 활성화

# IRQ → CPU affinity 설정
for i in 0 1 2 3; do
    IRQ=$(grep "eth0-rx-$i" /proc/interrupts | awk '{print $1}' | tr -d ':')
    echo $((1 << $i)) > /proc/irq/$IRQ/smp_affinity
done

# NFQUEUE fanout — 큐 0~3에 worker 프로세스 바인딩
for i in 0 1 2 3; do
    taskset -c $i suricata -c /etc/suricata/suricata.yaml \
        --runmode single -q $i &
done

# iptables fanout 규칙 (CPU와 큐 1:1 매핑)
iptables -I FORWARD -j NFQUEUE --queue-balance 0:3 --queue-bypass

SO_RCVBUF 튜닝

# 시스템 소켓 버퍼 최대값 증가
sysctl -w net.core.rmem_max=33554432     # 32MB
sysctl -w net.core.rmem_default=8388608  # 8MB

# 애플리케이션에서 직접 설정
# int rcvbuf = 4 * 1024 * 1024;
# setsockopt(fd, SOL_SOCKET, SO_RCVBUF, &rcvbuf, sizeof(rcvbuf));

# 큐 maxlen 증가 (nftables)
nft add rule inet filter forward queue num 0-3 flags bypass,fanout
# 또는 nfq_set_queue_maxlen(qh, 16384)로 코드에서 설정

pktgen + NFQUEUE 벤치마킹

# pktgen으로 테스트 패킷 생성 (다른 머신에서)
modprobe pktgen
echo "add_device eth0@0" > /proc/net/pktgen/kpktgend_0
pgset() { local result; echo $1 > /proc/net/pktgen/$2; }
pgset "count 10000000" /proc/net/pktgen/eth0@0
pgset "pkt_size 64" /proc/net/pktgen/eth0@0
pgset "dst_mac aa:bb:cc:dd:ee:ff" /proc/net/pktgen/eth0@0
pgset "dst 192.168.1.1" /proc/net/pktgen/eth0@0
echo "start" > /proc/net/pktgen/pgctrl

# NFQUEUE 수신 측에서 처리량 측정
watch -n 1 'cat /proc/net/netfilter/nfnetlink_queue | \
    awk "{print \"total:\", \$3, \"dropped:\", \$5}"'

# bpftrace로 verdict 처리 시간 측정
bpftrace -e '
kprobe:nfqnl_enqueue_packet { @ts[tid] = nsecs; }
kretprobe:nfqnl_recv_verdict {
    $lat = nsecs - @ts[tid];
    @latency = hist($lat);
    delete(@ts[tid]);
}
interval:s:5 { print(@latency); clear(@latency); }'

세션 bypass 패턴

# 1단계: 첫 패킷만 DPI (Suricata가 mark=1 설정)
nft add rule inet mangle prerouting \
    ct state new queue num 0 bypass

# 2단계: 기존 연결은 mark 확인 후 bypass
nft add rule inet mangle prerouting \
    ct mark 1 counter accept

# 3단계: Established 연결 중 DPI 완료 → bypass
nft add rule inet mangle prerouting \
    ct state established,related \
    ct mark 0 queue num 0 bypass

eBPF 기반 L7 분류

eBPF 소켓 필터는 커널 내부에서 L7 분류를 수행하여 유저스페이스 복사 없이 고속 처리합니다.

eBPF 소켓 필터 (SO_ATTACH_FILTER)

/* eBPF L7 분류 프로그램 — 커널 내부 실행 */
SEC("socket")
int l7_classifier(struct __sk_buff *skb) {
    void *data = (void *)(long)skb->data;
    void *data_end = (void *)(long)skb->data_end;
    struct ethhdr *eth = data;

    if (data + sizeof(*eth) > data_end) return 0;

    struct iphdr *ip = data + sizeof(*eth);
    if (data + sizeof(*eth) + sizeof(*ip) > data_end) return 0;

    if (ip->protocol != IPPROTO_TCP) return skb->len;

    struct tcphdr *tcp = (void *)ip + ip->ihl * 4;
    void *payload = (void *)tcp + tcp->doff * 4;

    if (payload + 4 <= data_end) {
        __u32 magic;
        bpf_probe_read(&magic, sizeof(magic), payload);
        /* HTTP GET 감지 (big-endian: 0x47455420) */
        if (magic == __constant_htonl(0x47455420)) {
            __u32 key = 0;
            __u64 *cnt = bpf_map_lookup_elem(&http_counter, &key);
            if (cnt) __sync_fetch_and_add(cnt, 1);
        }
    }
    return skb->len; /* 0 = 드롭, len = 통과 */
}

TC eBPF를 활용한 L7 라우팅

/* TC classifier: L7 기반 패킷 마킹 (커널 내부) */
SEC("tc")
int tc_l7_mark(struct __sk_buff *skb) {
    __u32 flow_key = compute_flow_key(skb);
    __u32 *app_id = bpf_map_lookup_elem(&flow_cache, &flow_key);

    if (app_id) {
        /* 캐시 히트: 이미 분류된 앱 */
        skb->mark = *app_id;
        return TC_ACT_OK;
    }

    /* 캐시 미스: NFQUEUE로 DPI 위임 */
    return TC_ACT_PIPE; /* Netfilter로 계속 진행 */
}

eBPF + NFQUEUE 통합 배포 스크립트

# 1단계: TC eBPF 프로그램 컴파일 및 로드
clang -O2 -target bpf -c tc_l7_mark.c -o tc_l7_mark.o
tc qdisc add dev eth0 clsact
tc filter add dev eth0 ingress bpf da obj tc_l7_mark.o sec tc

# 2단계: flow_cache BPF map 확인
bpftool map list | grep flow_cache
bpftool map dump name flow_cache | head -20

# 3단계: NFQUEUE 규칙 설정 (TC eBPF 이후 실행)
# TC eBPF에서 캐시 미스 → TC_ACT_PIPE → Netfilter NFQUEUE
nft add rule inet filter forward \
    mark 0 queue num 0-3 flags bypass,fanout

# 4단계: DPI 완료 후 flow_cache 갱신 (유저스페이스 → eBPF map)
# bpf_map_update_elem(&flow_cache, &flow_key, &app_id, BPF_ANY);

# 5단계: 분류된 트래픽은 TC eBPF에서 직접 처리 (NFQUEUE 우회)
# 처리량: 캐시 히트율 90% 가정 시 NFQUEUE 부하 10%로 감소

eBPF vs NFQUEUE 성능 비교

항목TC eBPF (커널 내부)NFQUEUE (유저스페이스)XDP
실행 위치커널 TC 레이어유저스페이스 프로세스드라이버/NIC
패킷 복사없음netlink 복사 필요없음 (UMEM)
처리량10~30 Mpps1~3 Mpps30~100 Mpps
L7 분류 능력단순 패턴 (마법 바이트)완전 DPI (Suricata/nDPI)제한적
정규식 지원불가Hyperscan 완전 지원불가
연결 추적BPF map 수동 관리conntrack 자동 연동BPF map 수동
권장 조합XDP(DDoS) → TC eBPF(캐시) → NFQUEUE(신규 세션 DPI)

JA4+ 차세대 TLS 지문 기법

JA3는 TLS Client Hello의 필드를 해시하여 클라이언트를 식별하는 기법으로 널리 사용되어 왔으나, TLS 확장 순서 랜덤화(Chrome 106+, Firefox 등)에 의해 동일 클라이언트도 다른 해시를 생성하는 문제가 발생했다. JA4+는 이러한 한계를 극복하기 위해 FoxIO에서 개발한 차세대 지문 체계로, 6개의 하위 지문으로 구성된다.

JA4+ 지문 체계 구성

지문 유형대상분석 레이어주요 용도
JA4TLS Client HelloTLS 핸드셰이크클라이언트 애플리케이션 식별
JA4STLS Server HelloTLS 핸드셰이크서버 응답 패턴 식별
JA4HHTTP RequestHTTP 헤더HTTP 클라이언트 식별
JA4XX.509 인증서TLS Certificate인증서 발급자/패턴 분석
JA4TTCP 핸드셰이크TCP SYN/SYN-ACKOS 핑거프린팅 (p0f 대체)
JA4LLight DistanceTCP RTT클라이언트 물리적 거리 추정

JA3 vs JA4+ 비교

항목JA3JA4JA4SJA4H
입력 데이터TLS version, ciphers, extensions, elliptic curves, EC formatsTLS version, SNI 유무, cipher 개수, extension 개수, ALPN, 정렬된 cipher+extension 목록Server Hello cipher, extension, ALPNHTTP method, headers (정렬), cookie 유무, Accept-Language
해시 방식MD5 (전체 필드 연결)인간 판독 가능 접두사(a부분) + SHA256 truncated(b,c부분)접두사 + SHA256접두사 + SHA256
확장 순서 영향있음 (순서 의존)없음 (정렬 후 해시)없음없음 (정렬)
GREASE 처리포함 가능 (오탐 원인)자동 제외자동 제외해당 없음
출력 예시e7d705a3286e19ea (MD5)t13d1516h2_8daaf6152771_b186095e22b6t130200_1301_h2ge11cn07enus_60a...4bc_e9a...1fd
가독성없음 (해시만)접두사에서 TLS 버전, 프로토콜 유추 가능접두사에서 cipher 확인 가능접두사에서 method/언어 확인 가능
라이선스BSD 3-ClauseFoxIO License (BSL 변형)FoxIO LicenseFoxIO License

JA4 지문 계산 구조 상세

/*
 * JA4 지문 구조: [a부분]_[b부분]_[c부분]
 *
 * a부분 (인간 판독 가능):
 *   [q|t]          → QUIC(q) 또는 TCP(t)
 *   [TLS version]  → "13" = TLS 1.3, "12" = TLS 1.2
 *   [d|i]          → SNI 존재(d=domain) / 부재(i=IP)
 *   [cipher count] → 2자리 (예: "15" = 15개 cipher)
 *   [ext count]    → 2자리 (예: "16" = 16개 extension)
 *   [ALPN 첫번째]  → "h2" = HTTP/2, "h1" = HTTP/1.1, "00" = 없음
 *
 * b부분: SHA256(정렬된 cipher suite 목록)의 앞 12자
 * c부분: SHA256(정렬된 extension 목록)의 앞 12자
 *
 * 핵심: cipher와 extension을 정렬하므로 순서 랜덤화 영향 없음
 */

/* JA4 계산 의사 코드 */
char *compute_ja4(struct tls_client_hello *ch)
{
    char a_part[16];
    char b_hash[13], c_hash[13];

    /* a부분: 프로토콜 메타데이터 */
    snprintf(a_part, sizeof(a_part),
        "%c%s%c%02d%02d%s",
        ch->is_quic ? 'q' : 't',          /* QUIC or TCP */
        tls_version_str(ch->version),        /* "13", "12" 등 */
        ch->has_sni ? 'd' : 'i',              /* domain or IP */
        count_ciphers_no_grease(ch),          /* GREASE 제외 */
        count_extensions_no_grease(ch),       /* GREASE 제외 */
        get_first_alpn(ch));                  /* "h2", "h1", "00" */

    /* b부분: 정렬된 cipher suite의 SHA256 (앞 12자) */
    uint16_t *ciphers = get_sorted_ciphers(ch);    /* 오름차순 정렬 */
    sha256_hex_truncate(ciphers, b_hash, 12);

    /* c부분: 정렬된 extension의 SHA256 (앞 12자) */
    uint16_t *exts = get_sorted_extensions(ch);   /* SNI, ALPN 제외 후 정렬 */
    sha256_hex_truncate(exts, c_hash, 12);

    /* 최종: "t13d1516h2_8daaf6152771_b186095e22b6" */
    return format_ja4(a_part, b_hash, c_hash);
}

JA4T — TCP 핑거프린팅

/*
 * JA4T: TCP SYN 패킷 기반 OS 핑거프린팅
 *
 * 분석 필드:
 *   - Window Size (초기 윈도우 크기)
 *   - TCP Options 순서 및 값 (MSS, Window Scale, SACK, Timestamp 등)
 *   - TTL (초기 TTL에서 OS 추정)
 *
 * 출력 형식: [window]_[options_list]_[ttl]
 *
 * 예시:
 *   Linux 6.x:   "65535_2-4-8-1-3_64"   (MSS, SACK, TS, NOP, WS)
 *   Windows 11:  "65535_2-4-8-1-3_128"
 *   macOS 14:    "65535_2-4-1-1-3_64"
 *
 * p0f와 유사하지만 JA4+ 체계 내에서 통합 분석 가능
 */

Suricata 7.x JA4 지원 현황

Suricata 7.0.3부터 JA4 지문을 실험적으로 지원하며, suricata.yaml에서 활성화할 수 있다.

# /etc/suricata/suricata.yaml — JA4 활성화
app-layer:
  protocols:
    tls:
      enabled: yes
      ja3-fingerprints: yes     # JA3/JA3S (기본)
      ja4-fingerprints: yes     # JA4+ (7.0.3+ 실험적)

# EVE 로그에 JA4 출력 포함
outputs:
  - eve-log:
      enabled: yes
      types:
        - tls:
            extended: yes    # JA3 + JA4 모두 포함
# Suricata 룰에서 JA4 매칭
# JA4 a부분만으로 빠른 필터링
alert tls any any -> any any (msg:"Suspicious JA4 - known malware"; \
    ja4.hash; content:"t13d1711h2_8daaf6152771_b186095e22b6"; \
    sid:2100001; rev:1;)

# JA4 a부분 접두사만 매칭 (TLS 1.3 + QUIC 탐지)
alert tls any any -> any any (msg:"QUIC TLS 1.3 traffic"; \
    ja4.hash; content:"q13d"; startswith; \
    sid:2100002; rev:1;)

# EVE JSON 출력 예시:
# {
#   "tls": {
#     "ja3": { "hash": "e7d705a3286e19ea...", "string": "771,4866-..." },
#     "ja4": { "hash": "t13d1516h2_8daaf6152771_b186095e22b6" },
#     "ja4s": { "hash": "t130200_1301_h2" }
#   }
# }

HASSH — SSH 핑거프린팅

HASSH는 SSH 핸드셰이크의 Key Exchange Init 메시지에서 알고리즘 목록을 추출하여 MD5 해시를 생성한다. JA3의 SSH 버전이라 할 수 있으며, SSH 클라이언트/서버 식별에 활용된다.

항목HASSH (클라이언트)HASSHServer (서버)
입력 필드kex_algorithms, encryption_algorithms_client_to_server, mac_algorithms_client_to_server, compression_algorithms_client_to_serverkex_algorithms, encryption_algorithms_server_to_client, mac_algorithms_server_to_client, compression_algorithms_server_to_client
해시 방식MD5(필드 연결)MD5(필드 연결)
출력 예시ec7378c1a92f5a8db12d2871a1c7d86c
OpenSSH 9.x2307...e8b04a3e...c915
PuTTY 0.8xa12f...3b17
Suricata 지원hassh.hash / hassh.string 키워드hassh.server.hash / hassh.server.string
# Suricata HASSH 탐지 룰
alert ssh any any -> any any (msg:"Known SSH brute-force tool HASSH"; \
    hassh.hash; content:"ec7378c1a92f5a8dcebc7e2130115fc8"; \
    sid:2200001; rev:1;)

# HASSH + JA4 조합으로 터널링 탐지
# (SSH 클라이언트가 비정상적 JA4T TCP 지문을 보이면 터널 의심)
alert ssh any any -> any any (msg:"SSH tunnel suspected - abnormal TCP fingerprint"; \
    hassh.hash; content:"ec7378c1a92f5a8dcebc7e2130115fc8"; \
    flow:to_server,established; \
    threshold: type both, track by_src, count 5, seconds 60; \
    sid:2200002; rev:1;)
다중 지문 조합 전략: 단일 지문만으로는 회피가 가능하므로, 여러 레이어의 지문을 조합하면 탐지 정확도가 크게 향상된다. 권장 조합:
  • TLS 클라이언트 식별: JA4 + JA4X (Client Hello + 인증서) — 동일 클라이언트가 다른 인증서를 제시하면 MITM 의심
  • 봇넷/C2 탐지: JA4 + JA4S + JA4H — C2 서버의 Server Hello + HTTP 패턴이 고정적인 특성 활용
  • OS + 앱 식별: JA4T + JA4 — TCP 핑거프린트(OS)와 TLS 핑거프린트(앱)의 불일치 탐지 (예: Linux TCP 지문인데 Windows-only 앱의 JA4)
  • SSH 터널 탐지: HASSH + JA4T — 비정상적 SSH 도구의 TCP 특성 조합
  • VPN/프록시 탐지: JA4L(물리 거리) + JA4(클라이언트) — 지리적 거리와 클라이언트 유형의 불일치
Suricata 7.x에서는 EVE JSON 로그에 모든 지문을 동시에 기록할 수 있으므로, Elasticsearch/OpenSearch에서 상관 분석(correlation)을 수행하는 것이 효과적이다.

TLS 트래픽 분류 (JA3/SNI)

TLS 1.3에서 페이로드가 암호화되므로 핸드셰이크 지문으로 우회 식별합니다. JA3(클라이언트 지문), JA3S(서버 지문), JARM(서버 능동 스캔) 세 가지 기법을 조합하면 악성 도구를 높은 정확도로 식별할 수 있습니다.

TLS 지문 기법 비교

기법입력 데이터해시용도우회 어려움
JA3TLS ClientHello (버전, 암호, 확장, 곡선)MD5 32자클라이언트(악성코드/C2) 식별중간 (랜덤화로 우회 가능)
JA3STLS ServerHello (버전, 암호, 확장)MD5 32자서버(C2 인프라) 식별중간
JARM능동 스캔 10개 TLS Hello 응답62자 문자열서버 TLS 스택 핑거프린팅높음 (능동 스캔 필요)
SNIClientHello server_name Extension문자열도메인 기반 URL 필터링낮음 (ESNI/ECH로 우회)

JA3 지문 계산 원리

/* JA3 계산 입력 구성 (Suricata 내부 구현 참고) */
/* TLS ClientHello에서 추출:
   1. SSLVersion        (예: 769 = TLS 1.0)
   2. Ciphers           (예: 49195-49199-52393-52392-...)
   3. Extensions list   (예: 0-5-10-11-13-23-16-...)
   4. EllipticCurves    (예: 29-23-24)
   5. EllipticCurvePointFormats (예: 0)
*/
/* 쉼표로 구분된 문자열 → MD5 해시
   MD5("769,49195-49199-52393,0-5-10-11,29-23-24,0")
   → "abc123def456abc123def456abc123de" (32자)
*/

/* JARM: 능동 스캔으로 서버 TLS 스택 지문 */
/* 10개의 특수 ClientHello를 전송하고 ServerHello 응답을 수집
   각 응답의 cipher + extension 조합 → 62자 지문
   예: "2ad2ad0002ad2ad00042d42d000000000000000000000000000000000000"
   서버 측에서 TLS 라이브러리 버전/설정을 식별 (OpenSSL vs BoringSSL vs JSSE 등)
*/

Suricata JA3/JARM 규칙 활용

# Suricata 규칙으로 알려진 악성 JA3 차단
# alert tls any any -> any any (
#   msg:"Cobalt Strike Beacon JA3";
#   ja3.hash; content:"72a589da586844d7f0818ce684948eea";
#   sid:1000001; rev:1;)

# JA3S (서버 응답 지문)
# alert tls any any -> any any (
#   msg:"Malware C2 Server JA3S";
#   ja3s.hash; content:"f4febc55ea12b31ae17cfbf5a4b33b72";
#   sid:1000002; rev:1;)

# SNI 기반 도메인 차단
# alert tls any any -> any any (
#   msg:"Blocked Domain SNI";
#   tls.sni; content:"malware.example.com"; nocase;
#   sid:1000003;)

# nDPI를 통한 TLS 지문 추출
# ndpi_flow.protos.tls_quic.ja3_client — JA3 클라이언트 해시
# ndpi_flow.protos.tls_quic.ja3_server — JA3S 서버 해시

SNI 기반 URL 필터링

# nftables SNI 기반 차단 (TLS Extension 파싱)
# 커널 6.3+ nft_tproxy + Suricata 조합

# 방법 1: SNI → DNS 싱크홀 (DNSBL)
# NFQUEUE DPI에서 SNI 추출 후 IP를 nftables set에 추가
nft add element inet filter blocklist { 1.2.3.4 }
nft add rule inet filter forward ip daddr @blocklist drop

# 방법 2: 투명 프록시 (TPROXY) → Squid/mitmproxy SSL Bump
nft add rule inet mangle prerouting \
    tcp dport 443 \
    tproxy to 127.0.0.1:3129

# 방법 3: Suricata + NFQUEUE 인라인 SNI 차단
# Suricata가 SNI 추출 후 tls.sni 규칙 매칭 → NF_DROP verdict
suricata --runmode workers -q 0 -q 1 -q 2 -q 3 \
    -c /etc/suricata/suricata.yaml

# ECH (Encrypted Client Hello) 대응 — TLS 1.3+ 우회 문제
# ECH 활성화 시 SNI 암호화 → Outer SNI만 접근 가능
# ECH 차단: ClientHello에서 ECH Extension(0xfe0d) 감지 후 DROP

DPI 우회 기법과 대응 전략

DPI 시스템의 한계를 악용하는 다양한 우회 기법이 존재하며, 각 기법에 대한 체계적인 대응 전략이 필요합니다. 공격자는 프로토콜 스택의 각 계층에서 DPI를 무력화하는 방법을 조합하며, 방어 측은 재조립·행위 분석·다계층 탐지를 통해 대응합니다.

DPI 우회 기법 (Attack) → 대응 전략 (Defense) 우회 기법 (Evasion Techniques) 대응 전략 (Countermeasures) 1. TCP Segmentation / IP Fragmentation HTTP 요청을 수 바이트씩 분할 전송 → 시그니처 불일치 Stream Reassembly 필수 Suricata stream.reassembly.depth: 1mb 설정 2. TLS Encrypted SNI (ESNI/ECH) ClientHello의 SNI를 암호화 → 도메인 식별 차단 Outer SNI + JA3/JARM 지문 ECH outer_name 분석 + TLS 핑거프린트 조합 3. Domain Fronting (CDN 악용) SNI ≠ Host 헤더 → 하나의 TLS 연결에 이중 도메인 SNI vs Host 헤더 교차 검증 불일치 시 alert 또는 차단 (Suricata lua 스크립트) 4. Protocol Tunneling DNS tunnel / HTTP tunnel / WebSocket 내 은닉 채널 중첩 프로토콜 디코딩 DNS 엔트로피 분석, HTTP 페이로드 비정상 탐지 5. Timing-based Evasion (Slow Drip) 극단적 저속 전송 → 세션 타임아웃 전 완료 회피 Timeout 기반 탐지 stream.reassembly.timeout 조정, 비정상 RTT 탐지 6. Polymorphic Malware 실행마다 페이로드 변형 → 고정 시그니처 무효화 행위 분석 + ML 탐지 Suricata lua + 외부 샌드박스 연동, anomaly 규칙 우회 공격 방어 대응 공격→대응 관계

우회 기법 상세 비교

기법설명DPI 영향대응 방법Suricata 규칙 예시
TCP Segmentation HTTP 요청을 1~2바이트 TCP 세그먼트로 분할 전송하여 DPI 시그니처 매칭 회피 단일 패킷 기반 시그니처 실패, 전체 요청 문자열이 여러 패킷에 분산 TCP 스트림 재조립(reassembly) 활성화, depth 충분히 설정 alert http any any -> any any (msg:"Segmented HTTP evasion"; flow:to_server,established; content:"GET"; depth:4; stream_size:server,<,6; sid:1000001;)
IP Fragmentation IP 패킷을 MTU 이하로 강제 분할하여 L4/L7 헤더가 다른 프래그먼트에 위치 TCP/UDP 헤더가 두 번째 프래그먼트에 위치하면 포트 기반 필터 우회 IP defrag 활성화 (defrag 정책), 비정상 오프셋 탐지 alert ip any any -> any any (msg:"Tiny IP fragment"; fragbits:M; dsize:<8; sid:1000002;)
TLS ESNI/ECH ClientHello의 server_name 확장을 암호화하여 DPI의 SNI 기반 도메인 식별 차단 SNI 필드 추출 불가, 도메인 기반 정책 적용 실패 Outer SNI(public_name) 분석, JA3/JARM 지문, DNS 쿼리 상관 분석 alert tls any any -> any any (msg:"ECH detected"; tls.sni; content:"cloudflare-ech.com"; sid:1000003;)
Domain Fronting TLS SNI에 허용 도메인, HTTP Host 헤더에 실제 목적지를 설정하여 CDN 경유 우회 SNI 기반 허용 정책 통과 후 실제로는 차단 대상 서버에 연결 SNI vs HTTP Host 교차 검증, CDN IP 대역 모니터링 alert http any any -> any any (msg:"Domain fronting suspect"; lua:detect_domain_fronting.lua; sid:1000004;)
DNS Tunneling DNS 쿼리/응답에 데이터를 인코딩하여 DNS 프로토콜을 데이터 채널로 악용 정상 DNS 트래픽으로 위장, DPI가 DNS 페이로드 내부까지 검사하지 않으면 통과 DNS 쿼리 길이/엔트로피 분석, TXT 레코드 빈도 이상 탐지 alert dns any any -> any any (msg:"DNS tunnel suspect"; dns.query; content:"|00|"; byte_test:1,>,50,0,relative; sid:1000005;)
HTTP/WebSocket Tunnel HTTP CONNECT 또는 WebSocket Upgrade 후 임의 프로토콜을 내부에서 전송 초기 핸드셰이크만 HTTP로 감지, 이후 페이로드는 미검사 CONNECT/Upgrade 후 트래픽 지속 검사, 비정상 바이너리 탐지 alert http any any -> any any (msg:"HTTP tunnel"; content:"CONNECT"; http_method; flow:to_server; sid:1000006;)
Slow Drip 극저속 전송(1바이트/초)으로 DPI 버퍼 타임아웃 유도, 시그니처 완성 전 만료 reassembly 타임아웃 전 패턴 완성 실패, 세션 추적 메모리 소모 최소 전송 속도 임계치 설정, 비정상 RTT 세션 종료 alert tcp any any -> any any (msg:"Slow drip evasion"; flow:to_server; stream_size:server,<,100; flow:established,to_server; detection_filter:track by_src,count 50,seconds 60; sid:1000007;)
Polymorphic Malware 실행마다 코드 변형(인코딩/암호화/패킹)으로 고정 바이트 시그니처 우회 해시/바이트 패턴 기반 탐지율 급감, 시그니처 DB 무력화 행위 기반 분석, 쉘코드 디코더 탐지, 샌드박스 연동 alert http any any -> any any (msg:"Shellcode decoder"; content:"|eb|"; byte_test:1,>=,0xc0,1,relative; sid:1000008;)

TCP Segmentation 우회 — 공격과 방어

# 공격자 측: TCP Segmentation으로 DPI 우회 시도
# HTTP GET 요청을 1바이트씩 분할 전송하여 DPI 시그니처 회피
import socket
import time

def send_segmented_http(host, port, path):
    """TCP 세그먼트를 극단적으로 작게 분할하여 DPI 회피 시도"""
    sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    sock.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1)
    sock.connect((host, port))

    request = f"GET {path} HTTP/1.1\r\nHost: {host}\r\n\r\n"

    # 1바이트씩 전송 — DPI가 단일 패킷에서 "GET" 패턴을 찾지 못함
    for byte in request:
        sock.send(byte.encode())
        time.sleep(0.01)  # 10ms 간격으로 각 바이트 전송

    response = sock.recv(4096)
    sock.close()
    return response

# 결과: 단일 패킷 기반 DPI는 "GET /malware" 시그니처를 탐지 실패
# "G", "E", "T", " ", "/" ... 각각이 별도 TCP 세그먼트
# 방어 측: Suricata stream reassembly 설정으로 TCP segmentation 우회 무력화
# /etc/suricata/suricata.yaml

stream:
  memcap: 256mb           # 스트림 재조립 메모리 상한
  checksum-validation: yes # 체크섬 검증 (DPI 품질 보장)
  midstream: false         # 이미 진행 중인 세션 픽업 비활성화
  async-oneside: false     # 단방향 스트림 비허용
  reassembly:
    memcap: 512mb          # reassembly 전용 메모리 (세그먼트 버퍼링)
    depth: 1mb             # 스트림당 최대 재조립 깊이
    toserver-chunk-size: 2560   # 서버 방향 청크 크기
    toclient-chunk-size: 2560   # 클라이언트 방향 청크 크기
    randomize-chunk-size: yes  # 청크 크기 랜덤화 (핑거프린트 방지)
    randomize-chunk-range: 10  # 랜덤 범위 ±10%
    raw: yes               # raw reassembly 활성화 (인코딩 무관 검사)
    segment-prealloc: 2048  # 세그먼트 사전 할당

# 핵심: depth를 충분히 높이면 TCP segmentation 우회가 불가능
# Suricata는 모든 세그먼트를 재조립한 뒤 시그니처 매칭 수행
# "G"+"E"+"T"+" "+"/malware" → 재조립 후 "GET /malware" 완성 → 탐지 성공

Domain Fronting 탐지 코드

/* NFQUEUE 콜백에서 Domain Fronting 탐지
 * SNI (TLS ClientHello)와 HTTP Host 헤더를 비교하여 불일치 탐지
 * SNI: allowed.example.com  vs  Host: blocked.evil.com → Domain Fronting */

#include <string.h>
#include <libnetfilter_queue/libnetfilter_queue.h>

/* TLS ClientHello에서 SNI 추출 */
static int extract_tls_sni(const unsigned char *payload, int len,
                            char *sni_buf, int sni_max)
{
    /* TLS Record: 0x16 (Handshake) */
    if (len < 5 || payload[0] != 0x16) return -1;

    int pos = 5;  /* TLS Record Header 크기 */
    if (pos >= len || payload[pos] != 0x01) return -1;  /* ClientHello */
    pos += 4;      /* Handshake Type(1) + Length(3) */
    pos += 2;      /* ProtocolVersion */
    pos += 32;     /* Random */

    /* Session ID */
    if (pos >= len) return -1;
    pos += 1 + payload[pos];

    /* Cipher Suites */
    if (pos + 2 > len) return -1;
    pos += 2 + (payload[pos] << 8 | payload[pos + 1]);

    /* Compression Methods */
    if (pos >= len) return -1;
    pos += 1 + payload[pos];

    /* Extensions 파싱 — server_name (type=0x0000) 찾기 */
    if (pos + 2 > len) return -1;
    int ext_len = payload[pos] << 8 | payload[pos + 1];
    pos += 2;
    int ext_end = pos + ext_len;

    while (pos + 4 <= ext_end && pos + 4 <= len) {
        uint16_t etype = payload[pos] << 8 | payload[pos + 1];
        uint16_t elen  = payload[pos + 2] << 8 | payload[pos + 3];
        pos += 4;
        if (etype == 0x0000 && pos + elen <= len) {
            /* ServerNameList → HostName (type=0x00) */
            int snl_pos = pos + 2;  /* skip list length */
            if (snl_pos < len && payload[snl_pos] == 0x00) {
                int name_len = payload[snl_pos + 1] << 8 | payload[snl_pos + 2];
                if (name_len < sni_max && snl_pos + 3 + name_len <= len) {
                    memcpy(sni_buf, &payload[snl_pos + 3], name_len);
                    sni_buf[name_len] = '\0';
                    return name_len;
                }
            }
        }
        pos += elen;
    }
    return -1;
}

/* HTTP Host 헤더 추출 */
static int extract_http_host(const unsigned char *payload, int len,
                              char *host_buf, int host_max)
{
    const char *needle = "Host: ";
    const char *p = memmem(payload, len, needle, 6);
    if (!p) return -1;
    p += 6;
    const char *end = memchr(p, '\r', len - (p - (const char*)payload));
    if (!end || end - p >= host_max) return -1;
    memcpy(host_buf, p, end - p);
    host_buf[end - p] = '\0';
    return end - p;
}

/* NFQUEUE 콜백: Domain Fronting 탐지 */
static int detect_fronting_cb(struct nfq_q_handle *qh,
                               struct nfgenmsg *nfmsg,
                               struct nfq_data *nfa, void *data)
{
    struct nfqnl_msg_packet_hdr *ph = nfq_get_msg_packet_hdr(nfa);
    uint32_t id = ntohl(ph->packet_id);
    unsigned char *payload;
    int len = nfq_get_payload(nfa, &payload);

    char sni[256] = {0}, host[256] = {0};

    /* 세션 추적: TLS ClientHello에서 SNI 저장 후
     * 동일 세션의 HTTP 요청에서 Host와 비교 */
    if (extract_tls_sni(payload, len, sni, sizeof(sni)) > 0) {
        session_store_sni(ph, sni);  /* 세션별 SNI 캐시 저장 */
    }

    if (extract_http_host(payload, len, host, sizeof(host)) > 0) {
        const char *cached_sni = session_get_sni(ph);
        if (cached_sni && strcasecmp(cached_sni, host) != 0) {
            /* SNI ≠ Host → Domain Fronting 의심! */
            syslog(LOG_WARNING,
                   "Domain fronting detected: SNI=%s Host=%s",
                   cached_sni, host);
            return nfq_set_verdict(qh, id, NF_DROP, 0, NULL);
        }
    }

    return nfq_set_verdict(qh, id, NF_ACCEPT, 0, NULL);
}
-- Suricata Lua 스크립트: Domain Fronting 탐지
-- /etc/suricata/rules/detect_domain_fronting.lua

function init(args)
    local needs = {}
    needs["tls"] = tostring(true)
    needs["http.host"] = tostring(true)
    return needs
end

function match(args)
    local tls_sni = SCFlowString("tls.sni")
    local http_host = SCFlowString("http.host")

    if tls_sni ~= nil and http_host ~= nil then
        -- SNI와 Host가 다르면 Domain Fronting 의심
        if string.lower(tls_sni) ~= string.lower(http_host) then
            return 1  -- 탐지: alert 발생
        end
    end
    return 0
end
법적 주의: DPI 우회 기법 테스트는 반드시 자체 소유 네트워크에서만 수행하세요. 타인의 네트워크나 서비스에 대한 DPI 우회 시도는 대부분의 국가에서 불법입니다. 한국의 경우 「정보통신망 이용촉진 및 정보보호 등에 관한 법률」 제48조(정보통신망 침해행위 등의 금지)에 해당할 수 있으며, 미국에서는 CFAA(Computer Fraud and Abuse Act)의 적용 대상이 됩니다. 보안 연구 목적이라도 사전 서면 허가(Penetration Test Agreement) 없이 진행하면 형사 책임이 발생할 수 있습니다.

Vectorscan — Hyperscan 오픈소스 후속

Intel이 2024년 Hyperscan의 오픈소스 라이선스를 변경(독점 라이선스로 전환)하자, 커뮤니티는 마지막 BSD 라이선스 버전(5.4.11)을 기반으로 Vectorscan을 포크했습니다. Vectorscan은 Hyperscan과 완전한 API 호환성을 유지하면서 ARM NEON/SVE 지원을 추가한 크로스플랫폼 정규식 엔진입니다.

Hyperscan vs Vectorscan 비교

항목Hyperscan (Intel)Vectorscan (커뮤니티)
라이선스독점 (2024~ 비공개), 기존 BSD 3-Clause (5.4.x까지)BSD 3-Clause (오픈소스 유지)
지원 플랫폼x86_64 전용 (SSE4.2/AVX2/AVX-512)x86_64 + ARM (NEON/SVE) + POWER (VSX)
SIMD 지원SSE4.2, AVX2, AVX-512SSE4.2, AVX2, AVX-512, NEON, SVE
API 호환성원본 (hs_* API)100% 호환 (드롭인 교체)
Suricata 통합공식 지원 (빌드 옵션)Suricata 7.0+에서 공식 지원
Snort 통합Snort 3 기본 엔진빌드 시 라이브러리 경로 교체로 사용
성능 (x86_64)기준값 (최적화 수십 년)동등 수준 (동일 코드베이스 기반)
성능 (ARM)미지원NEON 최적화로 x86 대비 80~95% 수준
커뮤니티Intel 내부 (비공개)GitHub 활발 (VectorCamp/vectorscan)
Chimera 지원내장 (libchimera)포함 (Chimera 포크 동시 유지)

Vectorscan 빌드 예제

# Vectorscan 소스 빌드 (Hyperscan 드롭인 교체)
git clone https://github.com/VectorCamp/vectorscan.git
cd vectorscan
mkdir build && cd build

# x86_64 빌드 (AVX2 최적화)
cmake -DCMAKE_INSTALL_PREFIX=/usr/local \
      -DBUILD_AVX2=ON \
      -DBUILD_AVX512=ON \
      -DFAT_RUNTIME=ON \
      ..
make -j$(nproc)
sudo make install

# ARM64 빌드 (NEON/SVE)
# cmake -DCMAKE_INSTALL_PREFIX=/usr/local ..
# ARM에서는 자동으로 NEON 감지, SVE는 GCC 10+ 필요

# Suricata에서 Vectorscan 사용 (Hyperscan과 동일 빌드 옵션)
cd /path/to/suricata
./configure --enable-hyperscan \
    --with-libhs-includes=/usr/local/include/hs \
    --with-libhs-libraries=/usr/local/lib
make -j$(nproc)

# 확인: Vectorscan이 Hyperscan API를 100% 대체
suricata --build-info | grep -i hyperscan
# Hyperscan support:                      yes
# (내부적으로 Vectorscan 라이브러리 사용)

Hyperscan/Vectorscan 스트리밍 모드 상세

스트리밍 모드는 TCP 세션처럼 데이터가 청크 단위로 도착하는 환경에 최적화된 모드로, 세션 간 상태를 유지하면서 청크 경계를 넘나드는 패턴도 정확하게 매칭합니다. DPI에서 TCP 스트림 검사에 필수적인 모드입니다.

#include <hs/hs.h>
#include <stdio.h>
#include <string.h>

/* 매칭 콜백: 패턴 매칭 시 호출 */
static int on_match(unsigned int id, unsigned long long from,
                     unsigned long long to, unsigned int flags,
                     void *ctx)
{
    printf("Pattern %u matched at offset %llu-%llu\n", id, from, to);
    return 0;  /* 0=계속 스캔, 비0=중단 */
}

int main(void)
{
    hs_database_t *db = NULL;
    hs_compile_error_t *err = NULL;
    hs_stream_t *stream = NULL;
    hs_scratch_t *scratch = NULL;

    /* 1단계: 패턴 컴파일 (HS_MODE_STREAM — 스트리밍 모드) */
    const char *patterns[] = {
        "GET /malware",           /* id=0: HTTP 악성 경로 */
        "(?i)x-forwarded-for:",   /* id=1: 프록시 헤더 */
        "\\x90{10,}",             /* id=2: NOP 슬레드 (쉘코드) */
    };
    unsigned int ids[] = { 0, 1, 2 };
    unsigned int flags[] = {
        HS_FLAG_SINGLEMATCH,                    /* 최초 1회만 매칭 */
        HS_FLAG_CASELESS | HS_FLAG_SINGLEMATCH, /* 대소문자 무시 */
        HS_FLAG_SINGLEMATCH,
    };

    hs_error_t ret = hs_compile_multi(
        patterns, flags, ids, 3,
        HS_MODE_STREAM,  /* 스트리밍 모드: 청크 간 상태 유지 */
        NULL, &db, &err
    );
    if (ret != HS_SUCCESS) {
        fprintf(stderr, "compile error: %s\n", err->message);
        hs_free_compile_error(err);
        return 1;
    }

    /* 2단계: 스크래치 공간 할당 (스레드당 1개) */
    hs_alloc_scratch(db, &scratch);

    /* 3단계: 스트림 열기 — TCP 세션 시작 시 호출 */
    hs_open_stream(db, 0, &stream);

    /* 4단계: 청크 단위로 데이터 스캔 — 패킷 도착마다 호출 */
    /* TCP segmentation 우회 시뮬레이션: "GET /malware"가 3개 청크에 분산 */
    hs_scan_stream(stream, "GET ", 4, 0, scratch, on_match, NULL);
    hs_scan_stream(stream, "/malw", 5, 0, scratch, on_match, NULL);
    hs_scan_stream(stream, "are HTTP/1.1\r\n", 15, 0, scratch, on_match, NULL);
    /* → "GET /malware" 패턴이 청크 경계를 넘어 정상 매칭! */

    /* 5단계: 스트림 닫기 — TCP 세션 종료 시 호출 */
    hs_close_stream(stream, scratch, on_match, NULL);

    /* 정리 */
    hs_free_scratch(scratch);
    hs_free_database(db);
    return 0;
}

Chimera — PCRE + Hyperscan 하이브리드 엔진

Chimera는 Hyperscan의 고속 매칭과 PCRE의 완전한 정규식 호환성을 결합한 하이브리드 엔진입니다. Hyperscan의 DFA 기반 엔진이 처리할 수 없는 역참조(backreference), 전방/후방 참조(lookahead/lookbehind) 등 복잡한 PCRE 패턴을 Hyperscan이 후보 매칭을 빠르게 찾은 뒤 PCRE로 정밀 검증하는 2단계 방식으로 처리합니다.

#include <hs/ch.h>  /* Chimera 헤더 */

/* Chimera 매칭 콜백 */
static ch_callback_t ch_on_match(unsigned int id,
    unsigned long long from, unsigned long long to,
    unsigned int flags, unsigned int size,
    const ch_capture_t *captured, void *ctx)
{
    printf("Chimera match id=%u [%llu-%llu]\n", id, from, to);
    if (captured && size > 0) {
        /* PCRE 캡처 그룹 접근 가능 */
        printf("  capture[0]: offset=%llu len=%u\n",
               captured[0].from, captured[0].to - captured[0].from);
    }
    return CH_CALLBACK_CONTINUE;
}

/* Chimera 컴파일 및 스캔 */
ch_database_t *ch_db = NULL;
ch_compile_error_t *ch_err = NULL;

/* PCRE 역참조 패턴 — 순수 Hyperscan으로는 불가능 */
const char *pcre_pat = "(\\w+)\\s+\\1";  /* 반복 단어 탐지 */
unsigned int ch_flags = CH_FLAG_SINGLEMATCH;

ch_compile(pcre_pat, ch_flags, 0, CH_MODE_NOGROUPS,
           NULL, &ch_db, &ch_err);

ch_scratch_t *ch_scratch = NULL;
ch_alloc_scratch(ch_db, &ch_scratch);

const char *data = "hello hello world";
ch_scan(ch_db, data, strlen(data), 0, ch_scratch,
        ch_on_match, NULL, NULL);
/* → "hello hello" 매칭 (역참조 \\1 동작 확인) */

Hyperscan 스캔 모드 성능 비교

모드API용도상태 유지성능 특성DPI 적용
Block 모드 hs_scan() 단일 데이터 블록을 한 번에 스캔 없음 (호출마다 독립) 가장 빠름 — 상태 관리 오버헤드 없음, SIMD 완전 활용 UDP 패킷, DNS 쿼리, 개별 파일 스캔
Stream 모드 hs_open_stream() / hs_scan_stream() / hs_close_stream() TCP 스트림 등 청크 단위 데이터 세션별 상태 유지 (hs_stream_t) Block 대비 10~20% 느림 — 상태 저장/복원 비용, 메모리 사용 증가 TCP DPI (Suricata IPS), HTTP/TLS 검사
Vectored 모드 hs_scan_vector() 비연속 메모리 블록(scatter-gather) 없음 (단일 호출) Block과 유사 — 복사 없이 여러 버퍼를 연속 스캔 skb frag_list 직접 스캔, zero-copy 파이프라인
실무 선택 기준: TCP 기반 DPI(Suricata IPS)는 반드시 Stream 모드를 사용해야 합니다. Block 모드는 TCP segmentation 우회에 취약하며, 패킷 경계를 넘는 시그니처를 탐지하지 못합니다. UDP 프로토콜(DNS, QUIC Initial)은 Block 모드가 적합하고, NIC의 scatter-gather 수신 버퍼를 직접 스캔할 때는 Vectored 모드가 복사 비용을 제거합니다.

NGFW에서 DPI의 역할과 한계

DPI는 NGFW의 핵심 기능이지만, 현실적인 한계도 존재합니다. 리눅스 기반 NGFW에서 NFQUEUE + Suricata + nDPI 조합은 상용 NGFW에 버금가는 기능을 구현할 수 있습니다.

DPI 활용 시나리오

기능구현 방법효과
IPS 서명 탐지Suricata 규칙 + Hyperscan 매칭CVE 익스플로잇, 악성 페이로드 차단
애플리케이션 식별nDPI 300+ 프로토콜 분류앱별 QoS/정책, 대역폭 제어
URL 필터링HTTP Host/SNI 추출카테고리별 접근 제어, 유해사이트 차단
악성코드 차단페이로드 해시 + Hyperscan 시그니처알려진 악성 파일/쉘코드 차단
DLP콘텐츠 패턴 매칭 (정규식)민감 데이터 (주민번호/카드번호) 유출 방지
봇넷 C&C 탐지JA3/JARM + DGA 도메인 탐지봇넷 통신 차단, 감염 호스트 격리
QUIC 분류nDPI QUIC + Initial Packet SNIHTTP/3 트래픽 식별 및 정책 적용
암호화 트래픽 분류JA3 + nDPI 행동 분석TLS 터널링 악성코드 탐지

리눅스 NFQUEUE 기반 NGFW 아키텍처 스택

정책 관리 Ansible / REST API / nftables ruleset DPI 엔진 클러스터 (Suricata x4 Worker) ├─ Hyperscan: IPS 시그니처 매칭 ├─ nDPI: 프로토콜/앱 분류 └─ JA3/SNI: TLS 지문 + URL 필터링 NFQUEUE queue 0-3, fanout, fail-open nftables 세션 bypass, mark, conntrack TC eBPF flow_cache 조회, pre-filter XDP 최초 필터, DDoS 방어 NIC RSS 4큐, IRQ affinity, TSO/GSO 상위 → 하위

DPI 한계

판정(Verdict) 처리 심화

NFQUEUE verdict는 단순한 ACCEPT/DROP을 넘어 패킷 수정, 재주입, 큐 재지정 등 다양한 동작을 지원합니다. 커널 내부의 nfqnl_recv_verdict() 함수가 유저스페이스 판정을 받아 처리합니다.

verdict 타입 상세

verdict커널 동작사용 사례
NF_ACCEPT1nf_reinject() → 다음 훅으로 전달정상 패킷 통과
NF_DROP0kfree_skb() → 패킷 폐기악성 패킷 차단
NF_STOLEN2skb 소유권을 유저스페이스로 이전, 커널은 더 이상 관여 안 함패킷 캡처 후 수동 재주입
NF_QUEUE3nf_queue() 재호출 → 다른 큐로 전달2단계 DPI 파이프라인
NF_REPEAT4현재 훅을 처음부터 재실행패킷 수정 후 재검사
NF_STOP5현재 훅 체인 중단, 이후 훅은 건너뜀성능 최적화 (드물게 사용)

nfq_set_verdict2() vs nfq_set_verdict_mark()

/* verdict2: verdict + nfmark 동시 설정 (가장 일반적) */
int nfq_set_verdict2(struct nfq_q_handle *qh,
                      u_int32_t id,
                      u_int32_t verdict,    /* NF_ACCEPT 등 */
                      u_int32_t mark,       /* sk_buff->mark 값 */
                      u_int32_t data_len,   /* 수정된 페이로드 길이 (0=원본) */
                      const unsigned char *buf); /* 수정된 페이로드 */

/* verdict_mark: 이미 설정된 nfmark를 유지하면서 verdict만 변경 */
/* nfq_set_verdict_mark()는 libnetfilter_queue 1.0.3+ 제공 */

/* 사용 예: Suricata bypass 패턴 */
/* DPI 완료 후 세션에 mark=1 부여 → 이후 패킷은 iptables에서 bypass */
return nfq_set_verdict2(qh, id, NF_ACCEPT,
                         0x1,   /* bypass mark */
                         0, NULL);

패킷 수정 후 재주입 (페이로드 변경)

/* 패킷 내용을 수정하여 재주입하는 예: HTTP Host 헤더 변조 */
static int mangle_callback(struct nfq_q_handle *qh,
                             struct nfgenmsg *nfmsg,
                             struct nfq_data *nfa, void *data)
{
    struct nfqnl_msg_packet_hdr *ph = nfq_get_msg_packet_hdr(nfa);
    uint32_t id = ntohl(ph->packet_id);

    unsigned char *orig_payload;
    int orig_len = nfq_get_payload(nfa, &orig_payload);

    unsigned char new_payload[65535];
    int new_len = modify_http_host(orig_payload, orig_len,
                                    new_payload, sizeof(new_payload));

    if (new_len > 0) {
        /* 수정된 페이로드로 패킷 재주입 */
        /* 커널의 nfqnl_mangle()이 skb를 새 데이터로 교체 */
        return nfq_set_verdict2(qh, id, NF_ACCEPT, 0,
                                  new_len, new_payload);
    }
    return nfq_set_verdict(qh, id, NF_ACCEPT, 0, NULL);
}

커널 내부 nfqnl_mangle()

/* net/netfilter/nfnetlink_queue.c — 페이로드 수정 내부 구현 */
static int nfqnl_mangle(void *data, unsigned int data_len,
                         struct nf_queue_entry *e, int diff)
{
    struct sk_buff *nskb = e->skb;

    if (diff < 0) {
        if (skb_trim(nskb, nskb->len + diff))
            return -EINVAL;
    } else if (diff > 0) {
        if (data_len > 0xFFFF)
            return -EINVAL;
        if (skb_tailroom(nskb) < diff) {
            if (pskb_expand_head(nskb, 0, diff - skb_tailroom(nskb), GFP_ATOMIC))
                return -ENOMEM;
        }
        skb_put(nskb, diff);
    }
    if (skb_store_bits(nskb, 0, data, data_len))
        return -EFAULT;
    return 0;
}

verdict 실패 시 fallback — NF_QUEUE_NR 매크로

/* 다른 큐로 재지정하는 verdict (2단계 파이프라인) */
/* NF_QUEUE_NR(queue_num): NF_QUEUE verdict에 큐 번호를 인코딩 */
#define NF_QUEUE_NR(x)   ((((x) << 16) | NF_QUEUE) & ~INT_MIN)

/* 예: 1단계(큐 0)에서 고위험 패킷을 큐 1(심층 분석)로 전달 */
if (risk_score > THRESHOLD)
    return nfq_set_verdict(qh, id, NF_QUEUE_NR(1), 0, NULL);
else
    return nfq_set_verdict2(qh, id, NF_ACCEPT, 0x1, 0, NULL);

/* verdict 처리 실패(큐 소비자 없음) 시:
   - NFQA_CFG_F_FAIL_OPEN 설정 → NF_ACCEPT (트래픽 유지)
   - 미설정 → NF_DROP (보안 우선)
   /proc/net/netfilter/nfnetlink_queue의 drop_count 확인
*/

제로카피 NFQUEUE

NFQUEUE의 성능 최대 병목은 커널→유저스페이스 패킷 데이터 복사입니다. 여러 플래그와 설정으로 복사 오버헤드를 줄이거나 완전히 제거할 수 있습니다.

NFQA_CFG_F_GSO — GSO 패킷 원본 전달

/* GSO(Generic Segmentation Offload) 패킷을 분할하지 않고 원본 전달 */
/* 기본 동작: 커널이 GSO 패킷을 MTU 크기로 분할 후 각각 큐잉 → CPU 낭비 */
/* GSO 플래그 활성화: 최대 64KB 슈퍼패킷을 그대로 전달 → 처리 횟수 감소 */
nfq_set_queue_flags(qh,
    NFQA_CFG_F_GSO | NFQA_CFG_F_UID_GID | NFQA_CFG_F_CONNTRACK,
    NFQA_CFG_F_GSO | NFQA_CFG_F_UID_GID | NFQA_CFG_F_CONNTRACK);

/* DPI 엔진에서 GSO 패킷 길이 확인 */
int len = nfq_get_payload(nfa, &payload);
/* len이 MTU(1500)을 초과하면 GSO 패킷 → 내부 세그먼트 반복 처리 필요 */

NFQA_CFG_F_UID_GID — 소켓 소유자 메타데이터

/* 로컬 소켓의 UID/GID 정보 — 애플리케이션별 정책에 활용 */
uint32_t uid = 0, gid = 0;
nfq_get_uid(nfa, &uid);
nfq_get_gid(nfa, &gid);

/* 예: UID 1000 (일반 사용자) 발생 HTTPS 트래픽만 DPI */
if (uid >= 1000 && tcp_dport == 443) {
    /* SNI 추출 후 필터링 */
}

NFQA_CFG_F_CONNTRACK — conntrack 정보 활용

/* conntrack 정보 직접 수신 (별도 /proc 조회 불필요) */
struct nfq_nlmsg_parse_ctx ctx;
nfq_nlmsg_parse(nlh, &ctx);

/* conntrack 상태 확인 */
const struct nf_conntrack *ct = nfq_get_conntrack(nfa);
if (ct) {
    uint32_t ct_mark = nfct_get_attr_u32(ct, ATTR_MARK);
    if (ct_mark & 0x1) {
        /* 이미 분류된 세션 → bypass */
        return nfq_set_verdict(qh, id, NF_ACCEPT, 0, NULL);
    }
}

nlattr 파싱 최적화 — nfq_nlmsg_parse()

/* 최신 libnetfilter_queue (1.0.5+) 권장 API */
/* nfq_nlmsg_parse()는 내부적으로 mnl_attr_parse()를 사용하며,
   개별 nfq_get_*() 호출보다 한 번에 파싱 → 오버헤드 감소 */

struct nlattr *attr[NFQA_MAX + 1] = {};
int ret = nfq_nlmsg_parse(nlh, attr);

if (attr[NFQA_PACKET_HDR]) {
    struct nfqnl_msg_packet_hdr *ph =
        mnl_attr_get_payload(attr[NFQA_PACKET_HDR]);
    id = ntohl(ph->packet_id);
}
if (attr[NFQA_PAYLOAD]) {
    payload = mnl_attr_get_payload(attr[NFQA_PAYLOAD]);
    payload_len = mnl_attr_get_payload_len(attr[NFQA_PAYLOAD]);
}
if (attr[NFQA_UID])
    uid = ntohl(mnl_attr_get_u32(attr[NFQA_UID]));

배치 처리 최적화

/* 배치 verdict: 여러 패킷 판정을 한 번의 netlink 전송으로 묶기 */
/* Suricata batchcount 설정 (suricata.yaml) */
/* nfq: batchcount: 20 → 20개 verdict를 하나의 sendmsg()로 전송 */

/* 직접 구현 시 — verdict 버퍼링 패턴 */
static struct {
    uint32_t id[32];
    uint32_t verdict[32];
    int      count;
} verdict_batch;

if (++verdict_batch.count >= 20) {
    flush_verdict_batch(&verdict_batch);   /* 한 번에 전송 */
    verdict_batch.count = 0;
}

Hyperscan 기반 고성능 패턴 매칭

Intel Hyperscan은 SIMD 명령어(SSE4/AVX2/AVX-512)를 활용한 고성능 정규식 라이브러리로, Snort 3와 Suricata의 기본 패턴 매칭 엔진으로 채택되었습니다. NFQUEUE + Hyperscan 조합은 수천 개의 시그니처를 초고속으로 매칭합니다.

Hyperscan vs Aho-Corasick 성능 비교

항목Aho-CorasickHyperscan (AVX2)Hyperscan (AVX-512)
알고리즘유한 오토마톤(DFA)NFA + SIMD 병렬화NFA + 512비트 병렬화
패턴 1000개 @ 1Gbps~850 Mbps~9.5 Gbps~18 Gbps
메모리 사용패턴당 선형컴파일 시 고정컴파일 시 고정
PCRE 지원제한적완전 지원 (HS_FLAG_*)완전 지원
실시간 컴파일가능hs_compile() 필요 (오프라인)hs_compile() 필요
스트림 매칭가능hs_stream_t 모드hs_stream_t 모드

NFQUEUE + Hyperscan 통합 아키텍처 SVG

커널 영역 NIC RX / XDP GSO 오프로드 유지 Netfilter NFQUEUE nfnetlink_queue.c 패킷 ID 부여 / 대기 패킷 메타데이터 UID/GID, conntrack NFQA_CFG_F_GSO TC eBPF flow_cache 분류완료 → bypass 유저스페이스 영역 libnetfilter_queue nfq_handle_packet() nfq_nlmsg_parse() L4 헤더 파싱 IP/TCP/UDP 오프셋 계산 포트 기반 프리필터 Hyperscan hs_scan() SIMD 병렬 패턴 매칭 AVX2/AVX-512 다중 패턴 동시 검색 nDPI 프로토콜 분류 L7 앱 식별 (300+) 결과 처리 ACCEPT mark=1 (bypass) nfq_set_verdict2() DROP 악성 패턴 매칭 IPS 차단 규칙 적중 MARK + ACCEPT 앱 카테고리 마킹 QoS/라우팅 정책 적용 NF_QUEUE_NR(1) 심층 분석 큐로 전달 2단계 DPI 파이프라인 netlink verdict 반환 (nfq_set_verdict2)

hs_compile() / hs_scan() API 패턴 예제

#include <hs/hs.h>

/* 패턴 컴파일 (프로그램 시작 시 1회) */
static const char *patterns[] = {
    "(?i)X-Malware:",          /* 악성 HTTP 헤더 */
    "(?i)cmd\\.exe",             /* Windows 쉘 명령어 */
    "\\xde\\xad\\xbe\\xef",     /* 알려진 악성코드 시그니처 */
};
static const unsigned flags[] = {
    HS_FLAG_CASELESS | HS_FLAG_SINGLEMATCH,
    HS_FLAG_CASELESS | HS_FLAG_SINGLEMATCH,
    HS_FLAG_SINGLEMATCH,
};
static const unsigned ids[] = { 0, 1, 2 };

hs_database_t *db;
hs_compile_error_t *err;
hs_compile_multi(patterns, flags, ids,
                 sizeof(patterns) / sizeof(patterns[0]),
                 HS_MODE_BLOCK, NULL, &db, &err);

hs_scratch_t *scratch;
hs_alloc_scratch(db, &scratch);   /* 스레드당 1개 */

/* 매칭 콜백 */
static int on_match(unsigned int id, unsigned long long from,
                    unsigned long long to, unsigned int flags, void *ctx)
{
    *(int *)ctx = id + 1;   /* 매칭된 패턴 ID 기록 */
    return 1;               /* 1 반환 시 스캔 중단 (첫 매칭 후 종료) */
}

/* 패킷마다 호출 (hs_scratch는 스레드 로컬) */
int matched_id = 0;
hs_scan(db, (const char *)payload, payload_len,
        0, scratch, on_match, &matched_id);

if (matched_id > 0)
    nfq_set_verdict(qh, id, NF_DROP, 0, NULL);
else
    nfq_set_verdict2(qh, id, NF_ACCEPT, 0x1, 0, NULL);

Snort/Suricata에서의 Hyperscan 활용

# Suricata — Hyperscan 빌드 확인
suricata --build-info | grep -i hyperscan
# Hyperscan support: yes

# Snort 3 — Hyperscan DAQ 빌드
cmake -DENABLE_HYPERSCAN=ON ..
snort3 --daq nfq --daq-var device=eth0 -Q

# nDPI + Hyperscan: nDPI 자체는 Hyperscan을 직접 사용하지 않으나,
# 커스텀 DPI 파이프라인에서 1차 Hyperscan 매칭 후 2차 nDPI 분류 조합 가능
# 예: 포트 기반 프리필터(eBPF) → Hyperscan 시그니처 → nDPI 프로토콜 분류

QUIC/HTTP3 DPI 과제

QUIC은 UDP 위에서 TLS 1.3을 직접 통합한 차세대 전송 프로토콜로, 기존 TCP 기반 DPI 기법을 그대로 적용할 수 없습니다. HTTP/3은 QUIC 위에서 동작하며, 전 세계 웹 트래픽의 30% 이상을 차지합니다.

QUIC 프로토콜 구조

계층TCP+TLS 기반QUIC 기반DPI 영향
전송 계층TCP (커널 처리)UDP (유저스페이스 구현)TCP 상태 추적 불가
암호화TLS 1.2/1.3 (레코드 헤더 평문)TLS 1.3 완전 통합핸드셰이크 이후 완전 암호화
헤더IP+TCP 헤더 평문Short Header 암호화 가능Long Header만 파싱 가능
연결 식별5-tupleConnection ID (DCID/SCID)NAT 뒤에서도 추적 가능
SNI 위치ClientHello Extension (평문)Initial Packet ClientHello (평문)Initial Packet에서만 추출

QUIC DPI의 기술적 한계

QUIC Initial Packet — SNI 추출

UDP NFQUEUE iptables -j NFQUEUE dport 443/udp 패킷 수신 대기 QUIC 파서 Long Header 감지 First Byte: 0x80+ 확인 Initial Packet 식별 Packet Type: 0x00 QUIC 복호화 Initial Secrets (HKDF) AEAD-AES-128-GCM ClientHello 파싱 TLS Extension 탐색 server_name (0x0000) SNI: example.com 추출 ALPN: h3 확인 Connection ID 추적 DCID/SCID 저장 BPF map: cid → policy Migration 대응 정책 적용 ALLOW 허용 목록 SNI BLOCK 차단 목록 SNI THROTTLE 대역폭 제한 마킹 LOG + ACCEPT 미분류 → 로깅 Short Header (Established QUIC) 페이로드 완전 암호화 → 내용 분석 불가 Connection ID 조회 → BPF map에서 기존 정책 적용

nDPI의 QUIC 지원 현황

/* nDPI 4.x QUIC 지원 — ndpi_flow_struct 내부 */
if (protocol.master_protocol == NDPI_PROTOCOL_QUIC) {
    /* SNI 필드: protos.tls_quic.client_requested_server_name */
    char *sni = ndpi_flow.protos.tls_quic.client_requested_server_name;

    /* ALPN: protos.tls_quic.alpn */
    char *alpn = ndpi_flow.protos.tls_quic.alpn;   /* "h3", "h3-29" 등 */

    /* QUIC 버전 */
    uint32_t quic_ver = ndpi_flow.protos.tls_quic.quic_version;
}

HTTP/3 vs HTTP/2 DPI 복잡도 비교

항목HTTP/1.1HTTP/2 (TLS)HTTP/3 (QUIC)
전송TCPTCP + TLSUDP + QUIC (TLS 통합)
헤더 평문 여부완전 평문ALPN/SNI만 평문Initial Packet만 파싱 가능
SNI 추출Host 헤더TLS ClientHelloQUIC Initial ClientHello
멀티플렉싱없음Stream (HOL 블로킹)Stream (독립적)
DPI 난이도쉬움중간어려움
nDPI 지원완전완전 (JA3)부분 (v4.x)
NFQUEUE 접근법TCP + L7 파싱SNI/JA3 추출Initial Packet 파싱 + CID 추적

UDP NFQUEUE를 통한 QUIC 트래픽 분류

# QUIC(UDP/443) 패킷을 NFQUEUE로 전달
iptables -I FORWARD -p udp --dport 443 -j NFQUEUE \
    --queue-num 1 --queue-bypass

# nftables 버전
nft add rule inet filter forward \
    udp dport 443 queue num 1 bypass

# QUIC 차단 (QUIC Fallback → TCP 강제)
# QUIC 차단 시 브라우저는 자동으로 TCP+TLS로 폴백
iptables -I FORWARD -p udp --dport 443 -j DROP
# 이 경우 DPI 없이도 HTTP/3 사용 차단 가능

멀티테넌트 NFQUEUE — 네트워크 네임스페이스

컨테이너 환경에서는 각 네트워크 네임스페이스가 독립적인 Netfilter 스택을 가지므로, 네임스페이스별로 별도의 NFQUEUE + DPI 인스턴스를 운영할 수 있습니다. 이를 통해 멀티테넌트 환경에서 테넌트 간 DPI 정책을 완전히 격리하면서도 공유 BPF 맵으로 전역 정책을 적용할 수 있습니다.

멀티테넌트 NFQUEUE: 네임스페이스별 독립 DPI Host Namespace Physical NIC (eth0) RSS 멀티큐 veth-host-1 ↔ veth-ns1 10.0.1.1/24 veth-host-2 ↔ veth-ns2 10.0.2.1/24 Host iptables FORWARD chain 라우팅 Shared BPF Map 전역 차단 정책 PIN: /sys/fs/bpf/policy Container NS 1 (tenant-a) NFQUEUE 0 -j NFQUEUE --queue-num 0 Suricata #1 IPS 모드 규칙셋 A 독립 conntrack 테이블 (zone=1) nft: ct zone set 1, 독립 NAT 규칙 10.0.1.0/24 전용 DPI 정책 Container NS 2 (tenant-b) NFQUEUE 0 -j NFQUEUE --queue-num 0 nDPI #2 프로토콜 분류 규칙셋 B 독립 conntrack 테이블 (zone=2) nft: ct zone set 2, 독립 NAT 규칙 10.0.2.0/24 전용 DPI 정책 격리 보장 항목 - NFQUEUE 번호 독립 (NS별 queue 0) - conntrack zone 분리 (zone=1, 2) - DPI 프로세스 격리 (PID NS) - 공유: BPF pinned map (전역 정책) 범례 Tenant A (Suricata) Tenant B (nDPI) Shared BPF (전역 정책)

네임스페이스별 NFQUEUE 설정

#!/bin/bash
# 멀티테넌트 NFQUEUE 환경 구축
# 각 네트워크 네임스페이스에 독립 NFQUEUE + DPI 인스턴스 배치

# ──────────────────────────────────────────
# 1단계: 네트워크 네임스페이스 + veth 쌍 생성
# ──────────────────────────────────────────

# Tenant A 네임스페이스
ip netns add tenant-a
ip link add veth-host-1 type veth peer name veth-ns1
ip link set veth-ns1 netns tenant-a

# Host 측 veth 설정
ip addr add 10.0.1.1/24 dev veth-host-1
ip link set veth-host-1 up

# Tenant A 네임스페이스 내부 설정
ip netns exec tenant-a bash -c '
    ip addr add 10.0.1.2/24 dev veth-ns1
    ip link set veth-ns1 up
    ip link set lo up
    ip route add default via 10.0.1.1
'

# Tenant B 네임스페이스 (동일 패턴)
ip netns add tenant-b
ip link add veth-host-2 type veth peer name veth-ns2
ip link set veth-ns2 netns tenant-b
ip addr add 10.0.2.1/24 dev veth-host-2
ip link set veth-host-2 up

ip netns exec tenant-b bash -c '
    ip addr add 10.0.2.2/24 dev veth-ns2
    ip link set veth-ns2 up
    ip link set lo up
    ip route add default via 10.0.2.1
'

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

# ──────────────────────────────────────────
# 2단계: 네임스페이스별 독립 NFQUEUE 규칙
# ──────────────────────────────────────────

# Tenant A: 모든 FORWARD 트래픽을 NFQUEUE 0으로 (NS 내부 독립)
ip netns exec tenant-a iptables -I FORWARD -j NFQUEUE \
    --queue-num 0 --queue-bypass

# Tenant B: 동일하게 NFQUEUE 0 사용 (네임스페이스가 다르므로 충돌 없음)
ip netns exec tenant-b iptables -I FORWARD -j NFQUEUE \
    --queue-num 0 --queue-bypass

# 핵심: 각 NS의 NFQUEUE 0은 완전히 독립된 큐
# NS 내부에서 실행하는 DPI 프로세스만 해당 큐를 수신

# ──────────────────────────────────────────
# 3단계: 네임스페이스별 독립 DPI 실행
# ──────────────────────────────────────────

# Tenant A: Suricata IPS 모드 (독립 규칙셋)
ip netns exec tenant-a suricata \
    -c /etc/suricata/tenant-a.yaml \
    --runmode single \
    -q 0 \
    --pidfile /var/run/suricata-tenant-a.pid &

# Tenant B: nDPI 기반 커스텀 DPI (독립 분류 정책)
ip netns exec tenant-b /opt/ndpi-queue/ndpi-nfqueue \
    --queue-num 0 \
    --config /etc/ndpi/tenant-b.conf \
    --pidfile /var/run/ndpi-tenant-b.pid &

# 확인: 각 NS에서 독립 프로세스 확인
ip netns exec tenant-a ss -f netlink | grep nfqueue
ip netns exec tenant-b ss -f netlink | grep nfqueue

VRF (Virtual Routing and Forwarding) + NFQUEUE

# VRF를 사용한 라우팅 도메인 분리 + NFQUEUE DPI
# VRF는 네임스페이스와 달리 동일 NS 내에서 라우팅 테이블만 분리

# VRF 디바이스 생성
ip link add vrf-blue type vrf table 100
ip link set vrf-blue up
ip link set eth1 master vrf-blue

ip link add vrf-red type vrf table 200
ip link set vrf-red up
ip link set eth2 master vrf-red

# VRF별 NFQUEUE 규칙 (iif 기반 분리)
# Blue VRF → NFQUEUE 10 (Suricata)
iptables -I FORWARD -i eth1 -m mark ! --mark 0x100 \
    -j NFQUEUE --queue-num 10 --queue-bypass

# Red VRF → NFQUEUE 20 (nDPI)
iptables -I FORWARD -i eth2 -m mark ! --mark 0x200 \
    -j NFQUEUE --queue-num 20 --queue-bypass

# VRF + conntrack zone: VRF별 conntrack 격리
nft add table inet filter
nft add chain inet filter prerouting '{ type filter hook prerouting priority -200 ; }'
nft add rule inet filter prerouting iifname "eth1" ct zone set 1
nft add rule inet filter prerouting iifname "eth2" ct zone set 2

# DPI 프로세스: 각 큐에 바인딩
suricata -c /etc/suricata/vrf-blue.yaml -q 10 &
/opt/ndpi-queue/ndpi-nfqueue --queue-num 20 &

# VRF 라우팅 테이블 확인
ip route show table 100  # Blue VRF
ip route show table 200  # Red VRF
conntrack zone과 네임스페이스 격리: 네트워크 네임스페이스는 각각 독립적인 conntrack 테이블을 가지므로 테넌트 간 세션 추적이 자연적으로 격리됩니다. 그러나 동일 네임스페이스 내에서 VRF로만 분리하는 경우, ct zone set N(nftables)으로 conntrack zone을 명시적으로 분리해야 서로 다른 VRF의 동일 5-tuple 세션이 충돌하지 않습니다.

주의 사항: conntrack -D(세션 삭제) 시 zone 파라미터를 지정하지 않으면 모든 zone의 세션이 삭제됩니다. 멀티테넌트 환경에서는 conntrack -D -w ZONE_ID로 특정 zone만 조작해야 합니다. 또한 각 네임스페이스의 nf_conntrack_max sysctl은 독립적으로 설정해야 하며, 호스트의 전역 설정이 네임스페이스에 자동 상속되지 않습니다.

분산 NFQUEUE 아키텍처

고트래픽 환경에서 단일 NFQUEUE + 단일 DPI 프로세스는 병목이 됩니다. --queue-balance와 RSS를 조합하면 다수의 NIC 큐를 복수의 Worker 프로세스에 분산할 수 있습니다.

NFQUEUE 클러스터 모드 설정

# nftables — queue balance (0~3 큐에 균등 분산)
nft add table inet dpi_cluster
nft add chain inet dpi_cluster forward { type filter hook forward priority 0\; }
nft add rule inet dpi_cluster forward \
    queue num 0-3 flags bypass,fanout

# iptables — --queue-balance N:M 옵션
# 5-tuple 해시로 4개 큐(0~3)에 분산, 큐 없으면 bypass
iptables -I FORWARD -j NFQUEUE \
    --queue-num 0 \
    --queue-balance \
    --queue-bypass

# 실제 범위 지정 (iptables 1.4.12+)
iptables -I FORWARD -j NFQUEUE --queue-num 0:3 --queue-bypass

RSS + NFQUEUE 파티셔닝

# 1단계: NIC RSS 4개 큐 설정
ethtool -L eth0 combined 4
ethtool -X eth0 hfunc toeplitz   # Toeplitz 해시 (5-tuple 기반)

# 2단계: RSS 큐 → CPU 고정
for i in 0 1 2 3; do
    echo $((1 << $i)) > /proc/irq/$(grep "eth0-rx-$i" \
        /proc/interrupts | cut -d: -f1 | tr -d ' ')/smp_affinity
done

# 3단계: NFQUEUE fanout — CPU i는 큐 i를 처리
iptables -I FORWARD -j NFQUEUE --queue-num 0:3 --queue-bypass

# 4단계: Worker 프로세스 — CPU affinity + 큐 번호 매핑
for i in 0 1 2 3; do
    taskset -c $i suricata \
        --runmode single \
        -q $i \
        -c /etc/suricata/suricata.yaml &
done

클러스터 아키텍처 SVG

NIC RSS 큐 RX Queue 0 CPU 0 | IRQ affinity RX Queue 1 CPU 1 | IRQ affinity RX Queue 2 CPU 2 | IRQ affinity RX Queue 3 CPU 3 | IRQ affinity NFQUEUE num Queue 0 5-tuple hash % 4 Queue 1 flags fanout,bypass Queue 2 nfnetlink_queue.c Queue 3 fail-open 활성화 Worker 프로세스 Suricata Thread 0 taskset -c 0 -q 0 Suricata Thread 1 taskset -c 1 -q 1 Suricata Thread 2 taskset -c 2 -q 2 Suricata Thread 3 taskset -c 3 -q 3 공유 BPF Map BPF_MAP_TYPE_HASH 차단 IP 목록 세션 분류 결과 원자적 업데이트 (멀티코어 안전) TC eBPF 조회 장애 복구 fail-open: ACCEPT 유지 systemd Restart=always

Suricata AF_PACKET cluster_flow 방식과의 비교

항목NFQUEUE fanoutAF_PACKET cluster_flowAF_XDP
커널 경유Netfilter 전체XDP 이후XDP 직접
패킷 복사netlink 복사소켓 버퍼 복사제로카피 (UMEM)
처리량1~3 Mpps3~8 Mpps10~30 Mpps
IPS 지원완전 (verdict)인라인 어렵가능 (XDP_DROP)
Netfilter 연동완전없음없음
배포 복잡도낮음중간높음

장애 복구 — fail-open/fail-closed 정책

# 방법 1: fail-open (큐 소비자 없을 때 ACCEPT)
# nfq_set_queue_flags(qh, NFQA_CFG_F_FAIL_OPEN, NFQA_CFG_F_FAIL_OPEN);
# 서비스 연속성 우선 — 보안보다 가용성 중시

# 방법 2: fail-closed (큐 소비자 없을 때 DROP)
# nfq_set_queue_flags(qh, NFQA_CFG_F_FAIL_OPEN, 0); (기본값)
# 보안 우선 — 차단이 허용보다 안전한 환경 (금융/정부)

# Worker 프로세스 자동 복구 (systemd)
cat > /etc/systemd/system/suricata-queue0.service <<'EOF'
[Unit]
Description=Suricata IPS Queue 0
After=network.target

[Service]
ExecStart=/usr/bin/suricata -c /etc/suricata/suricata.yaml \
    --runmode single -q 0
CPUAffinity=0
Restart=always
RestartSec=2

[Install]
WantedBy=multi-user.target
EOF
systemctl enable suricata-queue{0,1,2,3}

실전 NGFW 종합 구축 가이드

실제 프로덕션 환경에서 리눅스 기반 NGFW를 구축할 때 필요한 전체 아키텍처, 배포 스크립트, 성능 튜닝 체크리스트를 종합합니다. XDP → TC eBPF → nftables → NFQUEUE → DPI → 로깅까지 전 구간의 처리량과 설정 포인트를 다룹니다.

프로덕션 NGFW 전체 아키텍처

Physical XDP TC eBPF nftables NFQUEUE DPI Logging Intel X710 4x10GbE RSS 16큐 + Flow Director Ring Buffer 4096 entries IRQ Affinity CPU 0-15 고정 40 Gbps line rate XDP_DROP SYN flood / UDP flood Rate Limiter BPF map token bucket GeoIP block LPM trie map 38 Gbps DDoS 차단 후 flow_cache lookup BPF_MAP_TYPE_HASH Cache HIT (90%) → TC_ACT_OK bypass Cache MISS (10%) → nftables 진입 3.8 Gbps 미분류 트래픽 conntrack zones stateful tracking NAT / DNAT flowtable offload connmark 세션별 정책 태깅 → NFQUEUE fanout 0-7 3.5 Gbps DPI 대상 NFQUEUE fanout Queue 0-7 (8 workers) maxlen=8192, GSO, fail-open Suricata IPS Workers Hyperscan 패턴 매칭 엔진 nDPI L7 프로토콜 분류 Verdict ACCEPT + MARK DROP + LOG 3.2 Gbps DPI 처리 후 Policy Engine REST API → nftables 동적 룰 flow_cache 갱신 TC eBPF map update Threat Intel Feed IoC 자동 차단 (IP/도메인) Suricata EVE JSON /var/log/suricata/eve.json Filebeat JSON 파싱 + 전송 Elasticsearch 인덱싱 + 분석 Kibana 대시보드 시각화 bypass (90%) DROP

전체 배포 스크립트 (Production Ready)

#!/bin/bash
# NGFW Production Deployment Script
# 대상: Ubuntu 22.04+ / Debian 12+ / Rocky Linux 9+
# NIC: Intel X710 (i40e 드라이버), 4x10GbE
set -euo pipefail

##############################################
# 1. 시스템 커널 파라미터 튜닝
##############################################
cat > /etc/sysctl.d/90-ngfw.conf <<'EOF'
# 네트워크 스택 버퍼
net.core.rmem_max = 16777216
net.core.wmem_max = 16777216
net.core.rmem_default = 1048576
net.core.wmem_default = 1048576
net.core.netdev_max_backlog = 65536
net.core.somaxconn = 65536
net.core.optmem_max = 2097152

# conntrack 튜닝
net.netfilter.nf_conntrack_max = 2097152
net.netfilter.nf_conntrack_buckets = 524288
net.netfilter.nf_conntrack_tcp_timeout_established = 3600
net.netfilter.nf_conntrack_tcp_timeout_time_wait = 30
net.netfilter.nf_conntrack_expect_max = 4096

# NFQUEUE 소켓 버퍼
net.netfilter.nf_conntrack_helper = 0

# TCP 최적화
net.ipv4.tcp_max_syn_backlog = 65536
net.ipv4.tcp_tw_reuse = 1
net.ipv4.tcp_fin_timeout = 15
net.ipv4.ip_local_port_range = 1024 65535

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

# ARP 테이블 확장
net.ipv4.neigh.default.gc_thresh3 = 16384
net.ipv4.neigh.default.gc_thresh2 = 8192

# Busy polling (NFQUEUE latency 감소)
net.core.busy_read = 50
net.core.busy_poll = 50
EOF
sysctl --system

##############################################
# 2. NIC 구성 (Intel X710 / i40e)
##############################################
NIC="eth0"
NUM_QUEUES=8

# RSS 큐 수 설정
ethtool -L $NIC combined $NUM_QUEUES

# Ring buffer 최대화
ethtool -G $NIC rx 4096 tx 4096

# RSS 해시 함수 — Toeplitz (5-tuple 분산)
ethtool -X $NIC hfunc toeplitz
ethtool -N $NIC rx-flow-hash tcp4 sdfn
ethtool -N $NIC rx-flow-hash udp4 sdfn

# IRQ affinity 자동 설정
for i in $(seq 0 $(($NUM_QUEUES - 1))); do
    IRQ=$(grep "$NIC-TxRx-$i" /proc/interrupts | cut -d: -f1 | tr -d ' ')
    if [ -n "$IRQ" ]; then
        echo $((1 << $i)) > /proc/irq/$IRQ/smp_affinity
        echo "IRQ $IRQ → CPU $i"
    fi
done

# Interrupt coalescing (레이턴시 vs 처리량 균형)
ethtool -C $NIC rx-usecs 50 tx-usecs 50 rx-frames 64

# Flow Director 필터 (의심 트래픽 → 전용 큐)
ethtool -N $NIC flow-type tcp4 dst-port 443 action 0
ethtool -N $NIC flow-type udp4 dst-port 443 action 1  # QUIC

##############################################
# 3. nftables 룰셋 (flowtable + NFQUEUE fanout)
##############################################
cat > /etc/nftables-ngfw.conf <<'EOF'
#!/usr/sbin/nft -f
flush ruleset

table inet ngfw {
    # flowtable — 확립된 세션 고속 처리 (커널 bypass)
    flowtable ft_offload {
        hook ingress priority 0
        devices = { eth0, eth1 }
        flags offload      # 하드웨어 오프로드 시도
    }

    # conntrack zone 분리 (wan=1, lan=2)
    chain prerouting {
        type filter hook prerouting priority raw; policy accept;
        iif eth0 ct zone set 1
        iif eth1 ct zone set 2
    }

    chain forward {
        type filter hook forward priority 0; policy drop;

        # 1. 확립된 세션 → flowtable 오프로드
        ct state established,related flow add @ft_offload accept

        # 2. connmark 0xff → 이미 DPI 완료, bypass
        ct mark 0xff accept

        # 3. ICMP/ICMPv6 허용 (DPI 불필요)
        meta l4proto { icmp, icmpv6 } accept

        # 4. DNS/NTP — 소규모 UDP, DPI 불필요
        udp dport { 53, 123 } accept

        # 5. 신규 세션 → NFQUEUE fanout (8 workers)
        ct state new queue num 0-7 flags bypass,fanout

        # 6. DPI가 MARK 설정한 패킷 처리
        ct mark 0x01 accept          # DPI ACCEPT
        ct mark 0x02 log prefix "[NGFW-DROP] " drop  # DPI DROP
        ct mark 0x03 log prefix "[NGFW-ALERT] " accept  # Alert + pass
    }

    chain input {
        type filter hook input priority 0; policy drop;
        ct state established,related accept
        iif lo accept
        tcp dport { 22, 443 } accept  # 관리 포트
        tcp dport 8080 accept          # Policy REST API
    }

    chain postrouting {
        type nat hook postrouting priority srcnat;
        oif eth0 masquerade
    }
}
EOF

nft -f /etc/nftables-ngfw.conf
nft list ruleset

##############################################
# 4. Suricata 멀티 워커 설치 및 설정
##############################################
# Suricata + Hyperscan 빌드 (성능 핵심)
apt-get install -y suricata libhyperscan-dev libndpi-dev

# 주요 suricata.yaml 설정 하이라이트
cat > /etc/suricata/suricata-nfq.yaml <<'EOF'
%YAML 1.1
---
nfq:
  mode: repeat            # repeat 모드: verdict 후 패킷 재순환
  repeat-mark: 1
  repeat-mask: 1
  route-queue: 2
  batchcount: 20          # batch verdict (성능 향상)
  fail-open: yes          # 큐 포화 시 통과 (가용성 우선)

detect:
  profile: high           # 고성능 프로파일
  sgh-mpm-context: auto
  inspection-recursion-limit: 3000

mpm-algo: hs             # Hyperscan 엔진 사용
spm-algo: hs

app-layer:
  protocols:
    tls:
      enabled: yes
      ja3-fingerprints: yes  # JA3/JA4 지문 추출
    quic:
      enabled: yes           # QUIC Initial Packet 파싱
    http2:
      enabled: yes

outputs:
  - eve-log:
      enabled: yes
      filetype: regular
      filename: eve.json
      types:
        - alert
        - http
        - dns
        - tls
        - flow
        - stats:
            totals: yes
            threads: yes
EOF

##############################################
# 5. systemd 멀티 워커 서비스
##############################################
NUM_WORKERS=8

cat > /etc/systemd/system/suricata-nfq@.service <<'EOF'
[Unit]
Description=Suricata NFQUEUE Worker %i
After=network.target nftables.service
PartOf=suricata-nfq.target

[Service]
Type=simple
ExecStart=/usr/bin/suricata \
    -c /etc/suricata/suricata-nfq.yaml \
    --runmode single \
    -q %i \
    --set logging.outputs.0.file.filename=/var/log/suricata/suricata-%i.log \
    --set outputs.0.eve-log.filename=eve-%i.json
CPUAffinity=%i
LimitNOFILE=65536
LimitMEMLOCK=infinity
Restart=always
RestartSec=3
StandardOutput=journal
StandardError=journal

[Install]
WantedBy=suricata-nfq.target
EOF

# 타겟 유닛 (전체 워커 일괄 관리)
cat > /etc/systemd/system/suricata-nfq.target <<'EOF'
[Unit]
Description=Suricata NFQUEUE Workers
Wants=suricata-nfq@0.service suricata-nfq@1.service suricata-nfq@2.service \
      suricata-nfq@3.service suricata-nfq@4.service suricata-nfq@5.service \
      suricata-nfq@6.service suricata-nfq@7.service

[Install]
WantedBy=multi-user.target
EOF

systemctl daemon-reload
systemctl enable --now suricata-nfq.target

##############################################
# 6. XDP DDoS 방어 로드 (선택)
##############################################
# 사전 컴파일된 XDP 프로그램 로드
ip link set dev $NIC xdpgeneric obj /opt/ngfw/xdp_ddos.o sec xdp_prog

# TC eBPF flow_cache 로드
tc qdisc add dev $NIC clsact
tc filter add dev $NIC ingress bpf da obj /opt/ngfw/tc_flow_cache.o sec tc_ingress

##############################################
# 7. 로그 로테이션
##############################################
cat > /etc/logrotate.d/suricata-ngfw <<'EOF'
/var/log/suricata/eve-*.json {
    daily
    rotate 14
    compress
    delaycompress
    missingok
    notifempty
    copytruncate
    maxsize 500M
}
/var/log/suricata/suricata-*.log {
    weekly
    rotate 4
    compress
    missingok
    notifempty
    copytruncate
}
EOF

##############################################
# 8. 헬스체크 스크립트
##############################################
cat > /opt/ngfw/healthcheck.sh <<'SCRIPT'
#!/bin/bash
# NGFW Health Check — cron에서 1분 간격 실행
ALERT_THRESHOLD_DROP=100      # 드롭 증가율 임계값 (1분당)
ALERT_THRESHOLD_LATENCY=5000 # verdict 레이턴시 임계값 (μs)
LOG="/var/log/ngfw-health.log"

# NFQUEUE 드롭 카운터 확인
DROPS=$(awk '{sum += $6} END {print sum}' /proc/net/netfilter/nfnetlink_queue)
PREV_DROPS=$(cat /tmp/.ngfw_prev_drops 2>/dev/null || echo 0)
echo $DROPS > /tmp/.ngfw_prev_drops
DELTA=$(($DROPS - $PREV_DROPS))

if [ $DELTA -gt $ALERT_THRESHOLD_DROP ]; then
    echo "$(date): ALERT — NFQUEUE drops +$DELTA/min" >> $LOG
    # Slack/PagerDuty 웹훅 알림
    curl -s -X POST "$WEBHOOK_URL" -d "{\"text\":\"NGFW ALERT: NFQUEUE drops +$DELTA/min\"}"
fi

# 워커 프로세스 생존 확인
for i in $(seq 0 7); do
    if ! systemctl is-active --quiet suricata-nfq@$i; then
        echo "$(date): ALERT — suricata-nfq@$i down, restarting" >> $LOG
        systemctl restart suricata-nfq@$i
    fi
done

# conntrack 테이블 사용률 확인
CT_MAX=$(sysctl -n net.netfilter.nf_conntrack_max)
CT_CUR=$(wc -l < /proc/net/nf_conntrack)
CT_PCT=$(($CT_CUR * 100 / $CT_MAX))
if [ $CT_PCT -gt 80 ]; then
    echo "$(date): WARNING — conntrack ${CT_PCT}% full ($CT_CUR/$CT_MAX)" >> $LOG
fi

echo "$(date): OK — drops_delta=$DELTA conntrack=${CT_PCT}%" >> $LOG
SCRIPT
chmod +x /opt/ngfw/healthcheck.sh

# cron 등록
echo "* * * * * root /opt/ngfw/healthcheck.sh" > /etc/cron.d/ngfw-health

echo "=== NGFW deployment complete ==="

성능 튜닝 체크리스트

항목기본값권장값효과설정 명령
RSS 큐 수 자동 (코어 수) 8~16 NIC→CPU 분산, NFQUEUE fanout 대응 ethtool -L eth0 combined 8
Ring Buffer 256~512 4096 burst 트래픽 시 패킷 드롭 방지 ethtool -G eth0 rx 4096 tx 4096
IRQ affinity 자동 (irqbalance) 수동 고정 캐시 미스 감소, 일관된 레이턴시 echo N > /proc/irq/IRQ/smp_affinity
Interrupt Coalescing rx-usecs 100 rx-usecs 50 verdict 레이턴시 50% 감소 ethtool -C eth0 rx-usecs 50
rmem_max 212992 16777216 (16MB) NFQUEUE 소켓 버퍼 확장 sysctl -w net.core.rmem_max=16777216
conntrack_max 65536 2097152 (2M) 대규모 세션 추적 지원 sysctl -w net.netfilter.nf_conntrack_max=2097152
conntrack buckets 16384 524288 해시 충돌 감소, conntrack 조회 가속 sysctl -w net.netfilter.nf_conntrack_buckets=524288
NFQUEUE maxlen 1024 8192~16384 큐 포화 드롭 방지 nfq_set_queue_maxlen(qh, 8192)
NFQUEUE batch 1 (개별 verdict) 20 syscall 횟수 90% 감소 suricata.yaml: nfq.batchcount: 20
busy_poll 0 (비활성) 50 (μs) 인터럽트 지연 제거, 레이턴시 30% 감소 sysctl -w net.core.busy_poll=50
Hyperscan Aho-Corasick Hyperscan (hs) 패턴 매칭 5~20x 가속 suricata.yaml: mpm-algo: hs
flowtable offload 비활성 활성 확립된 세션 커널 bypass → 처리량 2x nft add flowtable ... flags offload
GRO/GSO 활성 활성 유지 세그먼트 합산으로 패킷 수 감소 ethtool -K eth0 gro on gso on
TCP timestamps 활성 활성 유지 PAWS 보호, RTT 측정 정확도 sysctl -w net.ipv4.tcp_timestamps=1
프로덕션 배포 팁:
  • 단계별 활성화: XDP → TC eBPF → nftables → NFQUEUE 순서로 하나씩 활성화하며 각 단계에서 패킷 카운터와 드롭률을 확인합니다. 한 번에 전체를 켜면 장애 지점 파악이 어렵습니다.
  • fail-open 필수: DPI 엔진이 크래시하거나 큐가 포화되면 전체 트래픽이 차단됩니다. flags bypass(nftables)와 NFQA_CFG_F_FAIL_OPEN을 반드시 설정하세요.
  • connmark bypass 패턴: DPI가 분류를 완료하면 ct mark 0xff를 설정하고, nftables에서 해당 mark는 NFQUEUE를 우회하도록 구성합니다. 이것이 전체 성능의 핵심입니다.
  • 워커 수 = NFQUEUE 수 = CPU 수: 워커마다 전용 CPU, 전용 NFQUEUE를 할당합니다. taskset 또는 systemd CPUAffinity로 고정합니다.
  • 로그 분리: 각 워커의 EVE JSON을 별도 파일로 출력하면 Filebeat 수집 시 병렬 처리가 가능하고 lock contention이 없습니다.
  • 롤백 계획: nft flush ruleset과 XDP detach 명령을 포함한 긴급 롤백 스크립트를 미리 준비합니다. 시스템 접근 불가 시 at 또는 cron으로 자동 복구를 설정하세요.
  • 모니터링 경보: NFQUEUE drop_count 증가율, conntrack 사용률 80%+, Suricata 프로세스 다운에 대해 즉시 알림을 설정합니다.
주의: nf_conntrack_max 값을 크게 설정하면 커널 메모리 사용이 증가합니다. conntrack 항목당 약 320바이트이므로 2M 항목은 약 640MB를 소비합니다. 서버 RAM이 충분한지 반드시 확인하세요. conntrack_bucketsconntrack_max / 4 이상을 권장합니다.

SmartNIC / DPU 기반 DPI 오프로드

SmartNIC(DPU)은 NIC 자체에 ARM 코어와 전용 ASIC을 내장하여 호스트 CPU 부담을 줄입니다. TC flower, OVS 오프로드, 단순 패턴 매칭까지 NIC 하드웨어에서 처리하고 복잡한 DPI만 호스트에서 수행하는 하이브리드 구조가 차세대 NGFW의 핵심입니다.

SmartNIC DPI 오프로드 아키텍처

Network 40/100 GbE SmartNIC / DPU NIC ASIC Flow Classification 5-tuple 하드웨어 매칭 eSwitch TC flower offload OVS-DPDK offload ARM Cortex-A78 코어 (8~16개) 단순 패턴 매칭 (정적 시그니처) conntrack 오프로드 / NAT 오프로드 오프로드 가능 ✓ L3/L4 ACL 정적 시그니처 매칭 conntrack/NAT/QoS 호스트 필수 ✗ Hyperscan 정규식 TLS/QUIC DPI ML 기반 분류 PCIe Gen4 x16 / Representor 포트 미분류 패킷만 호스트로 전달 Host CPU Policy Manager (REST API) 규칙 관리 → SmartNIC 규칙 푸시 복합 DPI 엔진 Suricata + Hyperscan (SIMD) nDPI L7 프로토콜 분류 QUIC/TLS 심층 분석 NFQUEUE (의심 플로우만) 전체의 5~10% 트래픽만 수신 Elasticsearch + Kibana DPI 결과 저장 및 시각화 bypass 100Gbps 5~10Gbps 90~95Gbps

SmartNIC 종류 비교

제품처리량ARM 코어DPI 오프로드 기능OVS 오프로드가격대
NVIDIA BlueField-3 400 Gbps ARM Cortex-A78 × 16 eSwitch TC flower, conntrack, regex (RXP 엔진), IPsec 오프로드 전체 데이터플레인 오프로드 $2,000~4,000
Intel IPU E2100 200 Gbps ARM Neoverse N1 × 16 P4 프로그래밍, Flow Director, ACL 오프로드, TLS 오프로드 IDPF 드라이버 + OVS $1,500~3,000
AMD Pensando DSC-200 200 Gbps ARM Cortex-A72 × 16 P4 기반 스테이트풀 방화벽, NAT, ACL, 암호화 가속 OVS 하드웨어 오프로드 $1,200~2,500
Xilinx Alveo SN1022 100 Gbps FPGA 기반 (소프트 코어) FPGA 커스텀 파이프라인, 유연한 패턴 매칭 로직 구현 가능 OVS + TC offload $2,500~5,000
참고: BlueField-3의 RXP(정규식 가속) 엔진은 초당 수백만 개의 정규식 매칭을 하드웨어에서 처리합니다. 단, Hyperscan 수준의 복잡한 PCRE는 지원하지 않으므로 단순 시그니처는 RXP, 복잡한 패턴은 호스트 Hyperscan으로 분리하는 전략이 필요합니다. 가격대는 2025년 기준 대략적 범위이며 구매 수량과 구성에 따라 달라집니다.

TC flower + OVS 오프로드 코드 예제

# TC flower 하드웨어 오프로드 — SmartNIC eSwitch에서 처리
# 전제: representor 포트가 활성화된 상태

# 1. SmartNIC eSwitch 모드 설정 (NVIDIA BlueField)
devlink dev eswitch set pci/0000:03:00.0 mode switchdev

# 2. representor 포트 확인
ip link show | grep "repr"
# eth0: PF representor
# enp3s0f0_0: VF0 representor

# 3. TC flower offload — HTTP 트래픽을 DPI 큐로 리다이렉트
tc qdisc add dev eth0 ingress
tc filter add dev eth0 protocol ip parent ffff: \
    flower \
    ip_proto tcp dst_port 80 \
    skip_sw \
    action mirred egress redirect dev enp3s0f0_0

# 4. HTTPS (TLS) 트래픽 → DPI 호스트 포트로 미러링
tc filter add dev eth0 protocol ip parent ffff: \
    flower \
    ip_proto tcp dst_port 443 \
    skip_sw \
    action mirred egress mirror dev enp3s0f0_0

# 5. 이미 분류된 플로우 → 하드웨어에서 직접 포워딩 (bypass)
tc filter add dev eth0 protocol ip parent ffff: \
    flower \
    ct_state +est+trk \
    skip_sw \
    action ct zone 1 pipe \
    action mirred egress redirect dev eth1

# 6. conntrack 오프로드 확인
tc filter show dev eth0 ingress
# "in_hw" 플래그가 표시되면 하드웨어 오프로드 성공

# skip_sw: 소프트웨어 처리 건너뛰기 (하드웨어 전용)
# skip_hw: 하드웨어 오프로드 건너뛰기 (소프트웨어 전용)
# 둘 다 생략하면 하드웨어 시도 후 실패 시 소프트웨어 폴백
# OVS-DPDK 오프로드 예시 (BlueField DPU)
# 1. OVS에 하드웨어 오프로드 활성화
ovs-vsctl set Open_vSwitch . other_config:hw-offload="true"
ovs-vsctl set Open_vSwitch . other_config:tc-policy="skip_sw"

# 2. 브리지 생성 + 포트 연결
ovs-vsctl add-br br-ngfw -- set bridge br-ngfw datapath_type=netdev
ovs-vsctl add-port br-ngfw pf0hpf -- set interface pf0hpf type=dpdk \
    options:dpdk-devargs="0000:03:00.0,representor=[0]"
ovs-vsctl add-port br-ngfw p0 -- set interface p0 type=dpdk \
    options:dpdk-devargs="0000:03:00.0"

# 3. 오프로드된 플로우 확인
ovs-appctl dpctl/dump-flows type=offloaded
# "offloaded:yes" 항목이 SmartNIC에서 처리 중인 플로우

# 4. 통계 확인
ovs-ofctl dump-flows br-ngfw | grep "n_packets"

DPDK + NFQUEUE 하이브리드 아키텍처

DPDK로 초고속 패킷 캡처를 수행하고, 의심스러운 플로우만 NFQUEUE로 전달하여 심층 검사하는 하이브리드 구조입니다. 전체 40Mpps 중 실제 DPI 대상은 5~10%(2~4Mpps)에 불과하므로 호스트 CPU 부담을 극적으로 줄입니다.

# DPDK + NFQUEUE 하이브리드 구성

# 1. DPDK hugepage 설정
echo 4096 > /sys/kernel/mm/hugepages/hugepages-2048kB/nr_hugepages
mkdir -p /dev/hugepages
mount -t hugetlbfs nodev /dev/hugepages

# 2. NIC을 DPDK 드라이버로 바인딩 (포트 0만)
dpdk-devbind.py -b vfio-pci 0000:03:00.0

# 3. DPDK 애플리케이션 — 1차 분류
# 40 Mpps 수신 → 5-tuple 해시 + 알려진 프로토콜 분류
# 분류 결과:
#   - 알려진 플로우 (90%): DPDK에서 직접 포워딩 → 40 Mpps
#   - 미분류/의심 (10%): KNI/virtio → 커널 네트워크 스택
#   - 차단 대상: DPDK에서 즉시 DROP

# 4. KNI (Kernel NIC Interface)로 커널 전달
# DPDK 앱에서 rte_kni_tx_burst()로 의심 패킷을 커널에 주입
# 커널 측 vEth0 인터페이스로 수신

# 5. 커널 측 NFQUEUE로 DPI 대상 패킷 전달
nft add table inet dpdk_dpi
nft add chain inet dpdk_dpi forward { type filter hook forward priority 0\; }
nft add rule inet dpdk_dpi forward \
    iif "vEth0" \
    queue num 0-3 flags bypass,fanout

# 6. Suricata가 NFQUEUE에서 심층 DPI 수행
# 의심 플로우만 처리하므로 3~4 Mpps 수준
구간처리 방식처리량레이턴시
NIC → DPDKPoll mode (PMD), 제로카피40 Mpps (line rate)< 5 μs
DPDK 1차 분류5-tuple 해시 + flow table40 Mpps~1 μs
DPDK → 직접 포워딩알려진 플로우 bypass36 Mpps (90%)< 10 μs
DPDK → KNI → 커널의심 플로우 커널 주입4 Mpps (10%)~50 μs
NFQUEUE → Suricata DPIHyperscan + nDPI L7 분류3 Mpps100~500 μs
DPI 결과 → DPDKflow table 갱신 (분류 완료)-< 1 ms
하이브리드 패턴의 핵심: DPDK가 1차 분류를 마치면 해당 플로우의 결과를 flow table에 기록합니다. 이후 같은 5-tuple의 패킷은 DPDK에서 직접 처리되어 NFQUEUE를 통과하지 않습니다. 이 classify-once, forward-forever 패턴이 전체 시스템 성능의 열쇠입니다. DPDK KNI 대신 AF_XDP를 사용하면 커널 주입 시 제로카피가 가능하여 레이턴시를 추가로 줄일 수 있습니다.

Grafana + Prometheus 기반 NFQUEUE 모니터링

프로덕션 NGFW 환경에서는 NFQUEUE, DPI 엔진, 시스템 리소스를 종합적으로 모니터링해야 합니다. Prometheus로 메트릭을 수집하고 Grafana 대시보드로 시각화하는 파이프라인을 구성합니다.

핵심 모니터링 메트릭

메트릭소스설명경보 임계값
nfqueue_total_packets/proc/net/netfilter/nfnetlink_queue큐에 입력된 전체 패킷 수-
nfqueue_queue_length/proc/net/netfilter/nfnetlink_queue현재 큐에 대기 중인 패킷 수> maxlen × 0.8
nfqueue_dropped/proc/net/netfilter/nfnetlink_queue큐 포화 드롭 카운터증가율 > 100/min
nfqueue_user_dropped/proc/net/netfilter/nfnetlink_queue유저스페이스 verdict 오류 드롭증가율 > 10/min
nfqueue_verdict_latency_usbpftrace / Suricata statsenqueue → verdict 지연 시간p99 > 5,000 μs
dpi_patterns_matchedSuricata EVE statsDPI 시그니처 매칭 횟수급증 시 공격 의심
dpi_sessions_activeSuricata stats현재 DPI 추적 중인 세션 수> 500,000
dpi_cache_hit_ratioTC eBPF map 통계flow_cache 캐시 적중률< 80%
conntrack_entries/proc/sys/net/netfilter/nf_conntrack_count현재 conntrack 항목 수> max × 0.8
system_cpu_usagenode_exporterCPU 사용률 (코어별)> 90%
system_memory_usagenode_exporter메모리 사용률> 85%
nic_rx_packetsnode_exporter / ethtoolNIC 수신 패킷 수-
nic_rx_droppedethtool -SNIC 레벨 드롭증가율 > 0

Prometheus Exporter (Python)

#!/usr/bin/env python3
# nfqueue_exporter.py — NFQUEUE 메트릭을 Prometheus로 노출
# 실행: python3 nfqueue_exporter.py (포트 9101)

from prometheus_client import start_http_server, Gauge, Counter
import time
import os

# Prometheus 메트릭 정의
QUEUE_TOTAL    = Gauge('nfqueue_queue_total',
                       '패킷 큐 대기 수', ['queue_num'])
QUEUE_DROPPED  = Counter('nfqueue_dropped_total',
                         '큐 포화 드롭 누적', ['queue_num'])
USER_DROPPED   = Counter('nfqueue_user_dropped_total',
                         '유저 verdict 오류 드롭 누적', ['queue_num'])
QUEUE_SEQ      = Gauge('nfqueue_seq_id',
                       '현재 시퀀스 ID (처리량 추정)', ['queue_num'])
CONNTRACK_CUR  = Gauge('conntrack_entries_current',
                       '현재 conntrack 항목 수')
CONNTRACK_MAX  = Gauge('conntrack_entries_max',
                       '최대 conntrack 항목 수')

# 이전 드롭 값 저장 (Counter 증분 계산용)
prev_drops = {}
prev_user_drops = {}

def read_nfqueue_stats():
    """
    /proc/net/netfilter/nfnetlink_queue 파싱
    컬럼: queue_num  portid  queue_total  copy_mode  copy_range
          drop_count  user_drop_count  id_sequence
    """
    path = '/proc/net/netfilter/nfnetlink_queue'
    if not os.path.exists(path):
        return

    with open(path) as f:
        for line in f:
            fields = line.strip().split()
            if len(fields) < 8:
                continue

            qnum       = fields[0]
            queue_total = int(fields[2])
            drop_count  = int(fields[5])
            user_drop   = int(fields[6])
            seq_id      = int(fields[7])

            QUEUE_TOTAL.labels(queue_num=qnum).set(queue_total)
            QUEUE_SEQ.labels(queue_num=qnum).set(seq_id)

            # Counter는 증분만 추가 (재시작 시 리셋 처리)
            if qnum in prev_drops:
                delta = drop_count - prev_drops[qnum]
                if delta > 0:
                    QUEUE_DROPPED.labels(queue_num=qnum).inc(delta)
            prev_drops[qnum] = drop_count

            if qnum in prev_user_drops:
                delta = user_drop - prev_user_drops[qnum]
                if delta > 0:
                    USER_DROPPED.labels(queue_num=qnum).inc(delta)
            prev_user_drops[qnum] = user_drop

def read_conntrack_stats():
    """conntrack 항목 수와 최대값 읽기"""
    try:
        with open('/proc/sys/net/netfilter/nf_conntrack_count') as f:
            CONNTRACK_CUR.set(int(f.read().strip()))
        with open('/proc/sys/net/netfilter/nf_conntrack_max') as f:
            CONNTRACK_MAX.set(int(f.read().strip()))
    except FileNotFoundError:
        pass

if __name__ == '__main__':
    # Prometheus HTTP 서버 시작 (포트 9101)
    start_http_server(9101)
    print("NFQUEUE exporter listening on :9101/metrics")

    while True:
        read_nfqueue_stats()
        read_conntrack_stats()
        time.sleep(5)   # 5초 간격 수집
# Prometheus 스크래핑 설정 (prometheus.yml)
scrape_configs:
  - job_name: 'nfqueue'
    static_configs:
      - targets: ['localhost:9101']
    scrape_interval: 10s

  - job_name: 'suricata'
    static_configs:
      - targets: ['localhost:9200']  # suricata-exporter
    scrape_interval: 15s

  - job_name: 'node'
    static_configs:
      - targets: ['localhost:9100']  # node_exporter
    scrape_interval: 10s

Suricata EVE JSON → Elasticsearch 파이프라인

# Filebeat 설정 — /etc/filebeat/filebeat.yml
# Suricata EVE JSON을 Elasticsearch로 전송
filebeat.inputs:
  - type: log
    enabled: true
    paths:
      - /var/log/suricata/eve-*.json
    json.keys_under_root: true
    json.add_error_key: true
    json.overwrite_keys: true

# Suricata 모듈 활성화 (자동 파싱 + 인덱스 매핑)
filebeat.modules:
  - module: suricata
    eve:
      enabled: true
      var.paths: ["/var/log/suricata/eve-*.json"]

# Elasticsearch 출력
output.elasticsearch:
  hosts: ["https://elasticsearch:9200"]
  index: "suricata-%{+yyyy.MM.dd}"
  username: "elastic"
  password: "${ES_PASSWORD}"
  ssl.certificate_authorities: ["/etc/filebeat/ca.crt"]

# ILM (Index Lifecycle Management) 정책
setup.ilm.enabled: true
setup.ilm.rollover_alias: "suricata"
setup.ilm.pattern: "{now/d}-000001"
setup.ilm.policy_name: "suricata-policy"

# 프로세서 — 추가 필드 파싱
processors:
  - decode_json_fields:
      fields: ["message"]
      target: ""
      overwrite_keys: true
  - add_host_metadata: ~
  - add_cloud_metadata: ~
# Logstash 대안 — /etc/logstash/conf.d/suricata.conf
# Filebeat 대신 Logstash를 사용할 경우

input {
  file {
    path => "/var/log/suricata/eve-*.json"
    codec => json
    sincedb_path => "/var/lib/logstash/sincedb_suricata"
    start_position => "beginning"
  }
}

filter {
  # 이벤트 타입별 분류
  if [event_type] == "alert" {
    mutate { add_tag => ["suricata_alert"] }
  }
  if [event_type] == "tls" {
    # JA3 지문 추출
    mutate { add_field => { "ja3_hash" => "%{[tls][ja3][hash]}" } }
  }
  if [event_type] == "flow" {
    # 플로우 기간 계산
    ruby {
      code => "
        if event.get('[flow][start]') and event.get('[flow][end]')
          start_t = Time.parse(event.get('[flow][start]'))
          end_t = Time.parse(event.get('[flow][end]'))
          event.set('flow_duration_sec', (end_t - start_t).round(3))
        end
      "
    }
  }

  # GeoIP 추가 (외부 IP)
  geoip {
    source => "src_ip"
    target => "src_geo"
  }
  geoip {
    source => "dest_ip"
    target => "dest_geo"
  }

  # 타임스탬프 파싱
  date {
    match => ["timestamp", "ISO8601"]
    target => "@timestamp"
  }
}

output {
  elasticsearch {
    hosts => ["https://elasticsearch:9200"]
    index => "suricata-%{event_type}-%{+YYYY.MM.dd}"
    user => "elastic"
    password => "${ES_PASSWORD}"
    ssl_certificate_authorities => ["/etc/logstash/ca.crt"]
  }
}
Grafana 대시보드 구성 팁:
  • Row 1 (개요): NFQUEUE 큐별 대기 패킷 수, 초당 verdict 처리량, 전체 드롭률을 Stat/Gauge 패널로 배치합니다.
  • Row 2 (레이턴시): verdict 레이턴시 히스토그램(p50/p95/p99)을 Heatmap 또는 Histogram 패널로 표시합니다.
  • Row 3 (DPI): Suricata alert 발생률(시간별), 탐지된 프로토콜 분포(파이 차트), 상위 시그니처 TOP 10을 표시합니다.
  • Row 4 (시스템): CPU 코어별 사용률, 메모리, conntrack 사용률, NIC rx/tx 패킷률을 Graph 패널로 표시합니다.
  • Alert 규칙: rate(nfqueue_dropped_total[5m]) > 10, conntrack_entries_current / conntrack_entries_max > 0.8, Suricata 프로세스 다운 탐지를 Grafana Alert로 설정합니다.
  • EVE JSON 인덱스: 이벤트 타입별로 인덱스를 분리하면(suricata-alert-*, suricata-flow-*) Kibana에서 검색 성능이 향상됩니다.

커널 소스 구조

NFQUEUE 관련 커널 소스 파일과 주요 심볼 목록입니다. 커널 6.x 기준으로 net/netfilter/ 디렉터리에 집중되어 있습니다.

net/netfilter/
├── nfnetlink_queue.c       # NFQUEUE 핵심 구현 (nfqnl_instance, enqueue, verdict)
├── nfnetlink.c             # nfnetlink 서브시스템 (netlink 메시지 디스패치)
├── nf_queue.c              # 큐 공통 인프라 (nf_queue_entry, nf_reinject)
├── xt_NFQUEUE.c            # iptables/xtables NFQUEUE 타겟 구현
├── nft_queue.c             # nftables queue expression 구현
└── nf_conntrack_helper.c   # conntrack 헬퍼 (ALG 연동)

include/uapi/linux/netfilter/
├── nfnetlink_queue.h       # NFQUEUE 사용자 API — NFQA_* 속성, 플래그 정의
├── nfnetlink.h             # nfnetlink 메시지 타입 (NFNL_SUBSYS_QUEUE)
└── nf_tables.h             # nftables 관련 헤더 (NFT_QUEUE_ATTR_*)

include/linux/netfilter/
├── nf_queue.h              # nf_queue_entry 구조체 정의
└── nfnetlink_queue.h       # nfqnl_instance 내부 헤더

주요 심볼 및 역할

심볼파일역할
nfqnl_enqueue_packet()nfnetlink_queue.c패킷을 큐에 삽입 — skb → netlink 메시지 변환
nfqnl_recv_verdict()nfnetlink_queue.c유저 verdict 수신 — nf_reinject() 호출
nfqnl_recv_config()nfnetlink_queue.c큐 설정 수신 — copy_mode, maxlen, flags 적용
nfqnl_build_packet_message()nfnetlink_queue.cskb → netlink nlattr 직렬화
nfqnl_flush()nfnetlink_queue.c미처리 패킷 일괄 해제 (fail-open/closed)
nfqnl_mangle()nfnetlink_queue.c유저 수정 페이로드 → skb 재작성
nf_queue_entry_free()nf_queue.c큐 엔트리 해제 (skb, 훅 참조 카운터 해제)
nf_reinject()nf_queue.cverdict 후 패킷을 Netfilter 훅으로 재주입
nft_queue_eval()nft_queue.cnftables queue expression 평가
instance_create()nfnetlink_queue.c새 nfqnl_instance 생성 및 해시 등록
instance_lookup()nfnetlink_queue.cqueue_num으로 nfqnl_instance RCU 조회

커널 커밋 추적 요점

# NFQUEUE 관련 주요 커밋 확인
git log --oneline net/netfilter/nfnetlink_queue.c | head -20

# 특정 기능 추가 버전 확인
# NFQA_CFG_F_GSO: v3.6 (2012) — commit 0ef0f4658)
# NFQA_CFG_F_UID_GID: v3.14 (2014)
# NFQA_CFG_F_SECCTX: v4.3 (2015)
# nfq_nlmsg_parse() API: libnetfilter_queue 1.0.3 (2018)
# QUIC Initial Packet 파싱: nDPI 4.0+ (2022)

# 소스 심볼 검색
grep -n "nfqnl_instance" net/netfilter/nfnetlink_queue.c | head -10

진단 및 모니터링

NFQUEUE 동작 상태를 모니터링하는 방법입니다. /proc/net/netfilter/nfnetlink_queue의 통계와 bpftrace 기반 실시간 측정을 조합하면 성능 병목을 정밀하게 파악할 수 있습니다.

큐 통계 확인

# NFQUEUE 통계 확인
cat /proc/net/netfilter/nfnetlink_queue
# 컬럼: queue_num portid queue_total copy_mode copy_range drop_count user_drop_count seq_id
# 예시: 0    1234    45    2    65535    12    0    1000
# drop_count: 큐 포화 드롭 (NFQA_CFG_F_FAIL_OPEN 미설정 시 DROP)
# user_drop_count: 유저스페이스 verdict 오류 드롭

# nfqueue 상태 (conntrack 도구 활용)
conntrack -S

# ss로 netlink 소켓 확인
ss -f netlink
# 출력 예: nl   UNCONN 0  0  * 1234  * *  users:(("suricata",pid=1234,fd=3))

# 큐 드롭 카운터 실시간 모니터링
watch -n 1 'awk "{printf \"Q%s: total=%s dropped=%s user_drop=%s\n\",\$1,\$3,\$6,\$7}" \
    /proc/net/netfilter/nfnetlink_queue'

# 큐 드롭이 증가하면 → maxlen 증가 또는 fail-open 설정 필요

성능 분석 — bpftrace 기반

# 큐잉/판정 처리량 측정
bpftrace -e '
kprobe:nfqnl_enqueue_packet {
    @enqueue = count();
    @enqueue_tid[tid] = nsecs;
}
kprobe:nfqnl_recv_verdict {
    @verdict = count();
}
interval:s:1 {
    printf("enqueue/s: %d, verdict/s: %d\n", @enqueue, @verdict);
    clear(@enqueue); clear(@verdict);
}'

# verdict 처리 지연(latency) 히스토그램
bpftrace -e '
kprobe:nfqnl_enqueue_packet { @ts[arg1] = nsecs; }
kprobe:nfqnl_recv_verdict {
    $start = @ts[arg0];
    if ($start > 0) {
        @lat = hist((nsecs - $start) / 1000);  /* us 단위 */
        delete(@ts[arg0]);
    }
}
interval:s:10 { print(@lat); }'

# perf로 NFQUEUE 컨텍스트 스위치 오버헤드 측정
perf stat -e context-switches,cache-misses,instructions,cycles \
    -p $(pidof suricata) sleep 10

# Suricata 내부 NFQ 통계
suricatasc -c dump-counters | jq '.nfq'
# nfq.packets: 처리된 패킷 수
# nfq.verdicts: 판정 횟수
# nfq.err_recv: recv 오류 수

# Suricata 내부 경고 확인
grep -i "nfqueue\|nfq" /var/log/suricata/suricata.log

DPI 패턴 매칭 성능 프로파일링

# Hyperscan 스캔 시간 측정
bpftrace -e '
uprobe:/usr/lib/libhs.so:hs_scan { @start[tid] = nsecs; }
uretprobe:/usr/lib/libhs.so:hs_scan {
    @hs_scan_lat = hist((nsecs - @start[tid]) / 1000);
    delete(@start[tid]);
}
interval:s:5 { print(@hs_scan_lat); }'

# nDPI detection 시간 측정
bpftrace -e '
uprobe:/usr/lib/libndpi.so:ndpi_detection_process_packet {
    @start[tid] = nsecs;
}
uretprobe:/usr/lib/libndpi.so:ndpi_detection_process_packet {
    @ndpi_lat = hist((nsecs - @start[tid]) / 1000);
    delete(@start[tid]);
}
interval:s:5 { print(@ndpi_lat); }'

# perf record로 hotspot 함수 파악
perf record -g -p $(pidof suricata) sleep 30
perf report --stdio | head -40

트러블슈팅

증상원인해결책
패킷 DROP 증가큐 포화 (maxlen 초과)nfq_set_queue_maxlen(qh, 16384) 증가, Fail-open 활성화
레이턴시 급증DPI 처리 병목fanout 큐 증가, bypass 규칙 추가, Hyperscan 도입
DPI 엔진 응답 없음프로세스 크래시Fail-open 설정, systemd Restart=always 감시
연결 끊김verdict 타임아웃큐 maxlen 증가 또는 DPI 처리 시간 단축
CPU 100%전체 트래픽 DPI세션 bypass 규칙 추가, eBPF pre-filter 도입
QUIC 트래픽 미분류QUIC DPI 미지원nDPI 4.x 업그레이드, UDP NFQUEUE 큐 추가
Hyperscan 컴파일 오류AVX2 미지원 CPUhs_compile() 시 HS_MODE_BLOCK + 소프트웨어 폴백 처리
user_drop_count 증가유저 소켓 버퍼 부족SO_RCVBUF 4MB 이상 설정, rmem_max sysctl 증가