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 반환

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 보안 컨텍스트 포함

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);
}

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)

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

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 아키텍처

고트래픽 환경에서 단일 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}

커널 소스 구조

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 증가