Text Search (lib/textsearch)

리눅스 커널의 텍스트 검색 프레임워크(lib/textsearch.c)를 심층 분석합니다. ts_config/ts_state/ts_ops 3-계층 구조 설계, KMP(Knuth-Morris-Pratt)·Boyer-Moore·FSM(유한 상태 기계) 3가지 플러그인 알고리즘의 내부 구현과 전처리 비용, skb 분산 데이터에 대한 선형화 없는 검색, netfilter xt_string 모듈과 nf_conntrack 헬퍼에서의 실전 활용, 알고리즘 선택 기준과 성능 벤치마크까지 포괄합니다.

전제 조건: Netfilter, 네트워킹 스택 문서를 먼저 읽으세요. 텍스트 검색 프레임워크의 주요 사용처는 netfilter이므로, 패킷(Packet) 필터링과 skb 구조의 기본 개념을 이해하면 도움이 됩니다.
일상 비유: 텍스트 검색 프레임워크는 문서에서 Ctrl+F 찾기와 같습니다. 사용자가 검색어(패턴)를 입력하면, 텍스트 검색 엔진이 문서(데이터) 전체를 훑으며 일치하는 위치를 찾아줍니다. 커널에서는 이 "문서"가 네트워크 패킷 페이로드(Payload)이고, "검색어"가 특정 프로토콜 문자열이나 악성 패턴입니다. KMP·Boyer-Moore·FSM은 찾기 엔진의 서로 다른 알고리즘 옵션입니다.

핵심 요약

  • 플러그인 아키텍처ts_ops 인터페이스를 통해 검색 알고리즘을 모듈로 교체할 수 있습니다. 현재 KMP, Boyer-Moore, FSM 3가지가 내장되어 있습니다.
  • 설정/상태 분리ts_config(불변 설정)와 ts_state(가변 상태)를 분리하여, 하나의 패턴 설정을 여러 CPU에서 동시에 검색할 수 있습니다.
  • 분산 데이터 검색skb_find_text()는 skb의 비선형 fragments를 선형화하지 않고 순회하며 패턴을 검색합니다.
  • netfilter 통합xt_string 모듈이 iptables/nftables -m string 매칭에 이 프레임워크를 사용합니다.
  • O(n+m) 보장 — KMP와 Boyer-Moore 모두 최악 경우에도 선형 시간을 보장하여 커널 내 DoS 공격에 대한 안전성을 확보합니다.

단계별 이해

  1. 패턴 준비textsearch_prepare()로 알고리즘을 선택하고 패턴 문자열을 전처리합니다. KMP는 실패 함수 테이블을, Boyer-Moore는 시프트 테이블을 미리 구축합니다.
  2. 검색 실행textsearch_find()에 데이터 소스(skb 또는 일반 버퍼(Buffer))를 넘기면, 등록된 알고리즘이 패턴 위치를 찾아 반환합니다.
  3. 연속 검색textsearch_next()로 동일 데이터에서 다음 매칭 위치를 계속 찾을 수 있습니다. ts_state가 검색 진행 상태를 유지합니다.
  4. 정리textsearch_destroy()로 전처리 테이블과 설정 메모리를 해제합니다.

텍스트 검색 프레임워크 개요

리눅스 커널의 텍스트 검색 프레임워크는 lib/textsearch.c에 구현된 범용 패턴 매칭 인프라입니다. 2004년 Thomas Graf가 netfilter의 문자열 매칭 요구를 일반화하여 도입했으며, 핵심 설계 목표는 다음 세 가지입니다.

소스 위치:
  • 프레임워크 코어: lib/textsearch.c, include/linux/textsearch.h
  • KMP 알고리즘: lib/ts_kmp.c
  • Boyer-Moore: lib/ts_bm.c
  • FSM: lib/ts_fsm.c
  • skb 통합: net/core/utils.c (skb_find_text())
  • netfilter 사용: net/netfilter/xt_string.c
Text Search Framework Architecture xt_string nf_conntrack helper skb_find_text() Custom callers textsearch API textsearch_prepare() / textsearch_find() / textsearch_next() / textsearch_destroy() ts_config pattern + ops + flags ts_state offset + per-call state ts_ops init / find / destroy ts_kmp (KMP) ts_bm (Boyer-Moore) ts_fsm (FSM) ts_ops를 통해 알고리즘 교체 가능 (플러그인 패턴)

프레임워크는 3계층 구조로 동작합니다. 최상위의 호출자(xt_string, conntrack helper 등)가 textsearch API를 통해 검색을 요청하면, API 계층은 ts_config에 바인딩된 ts_ops의 콜백(Callback)을 호출하여 실제 알고리즘을 실행합니다. 이 구조 덕분에 호출자는 어떤 알고리즘이 사용되는지 알 필요가 없습니다.

프레임워크 도입 배경

텍스트 검색 프레임워크 도입 전, netfilter의 문자열 매칭은 xt_string 모듈 내부에 직접 구현된 단순 brute-force 알고리즘을 사용했습니다. 이 방식에는 몇 가지 문제가 있었습니다.

프레임워크 도입 후, 이 모든 문제가 해결되었습니다. KMP/Boyer-Moore의 O(n+m) 보장으로 DoS 위험이 감소하고, 플러그인 아키텍처로 알고리즘을 자유롭게 선택할 수 있으며, get_next_block 콜백으로 skb의 비선형 데이터를 선형화 없이 검색할 수 있게 되었습니다.

커널 버전별 변화

버전변경 사항
2.6.14텍스트 검색 프레임워크 최초 도입 (Thomas Graf)
2.6.14ts_kmp, ts_bm, ts_fsm 3개 알고리즘 동시 추가
2.6.14xt_string이 프레임워크 기반으로 전환
3.xTS_IGNORECASE 플래그 추가 (대소문자 무시)
4.xskb_seq_read 최적화로 fragment 순회 성능 개선
5.xnftables payload 매칭과의 통합 강화
설계 패턴: 텍스트 검색 프레임워크는 커널에서 자주 사용되는 전략 패턴(Strategy Pattern)의 전형적 예입니다. ts_ops가 전략 인터페이스이고, ts_kmp/ts_bm/ts_fsm이 구체적 전략 구현입니다. ts_config는 컨텍스트(context) 역할을 합니다.

ts_config / ts_state 구조체(Struct)

텍스트 검색 프레임워크의 핵심은 설정(config)상태(state)의 명확한 분리입니다. 이 설계는 SMP 환경에서 동시 검색을 가능하게 하는 핵심 요소입니다.

ts_config — 불변 검색 설정

/* include/linux/textsearch.h */
struct ts_config {
    struct ts_ops       *ops;       /* 검색 알고리즘 (KMP/BM/FSM) */
    int                 flags;      /* TS_AUTOLOAD 등 */

    /**
     * get_next_block - 다음 데이터 블록을 가져오는 콜백
     * @consumed: 지금까지 소비한 바이트 수
     * @dst:      블록 포인터 반환 위치
     * @conf:     설정 구조체
     * @state:    검색 상태
     *
     * skb처럼 분산된 데이터에서 블록 단위 순회 가능
     */
    unsigned int        (*get_next_block)(unsigned int consumed,
                            const u8 **dst,
                            struct ts_config *conf,
                            struct ts_state *state);
    void                (*finish)(struct ts_config *conf,
                            struct ts_state *state);
};
설계 포인트: ts_configtextsearch_prepare()에서 한 번 생성된 후 변경되지 않습니다. 패턴 문자열과 전처리 테이블이 이 구조체의 확장 영역(알고리즘별)에 저장됩니다. 불변이므로 여러 CPU에서 동시에 참조해도 안전합니다.

ts_state — per-call 가변 상태

/* include/linux/textsearch.h */
struct ts_state {
    unsigned int        offset;     /* 현재 검색 오프셋 */
    char                cb[40];     /* 알고리즘별 콜백 데이터 */
};

ts_state는 각 검색 호출마다 스택에 할당됩니다. offset 필드는 현재까지 검색을 진행한 위치를 추적하고, cb[] 배열은 알고리즘이 자유롭게 사용할 수 있는 40바이트 스크래치 공간입니다. 스택 할당이므로 별도의 메모리 할당/해제가 필요 없습니다.

cb[] 크기 제한: 40바이트는 struct skb_seq_state보다 커야 합니다. 이는 BUILD_BUG_ON(sizeof(struct skb_seq_state) > sizeof(state.cb))로 컴파일 시 검증됩니다. 커스텀 알고리즘 구현 시 40바이트를 초과하는 상태가 필요하면, 별도로 힙 할당해야 합니다.
속성ts_configts_state
수명prepare ~ destroyfind/next 호출 동안
할당kmalloc (힙)스택 또는 호출자 지역 변수
변경 가능성불변 (read-only)매 호출마다 갱신
공유 가능성여러 CPU 공유 가능호출자 로컬 전용
내용패턴, ops, 전처리 테이블현재 오프셋(Offset), 알고리즘 임시 데이터
SMP 동시 검색: 하나의 ts_config(패턴 "GET /admin")를 netfilter의 여러 CPU에서 동시에 사용할 수 있습니다. 각 CPU는 자신만의 ts_state를 스택에 생성하므로 락 없이 동시 검색이 가능합니다.

ts_ops 인터페이스

ts_ops는 텍스트 검색 알고리즘이 구현해야 하는 인터페이스입니다. 이 구조체를 등록하면 프레임워크가 해당 알고리즘을 이름으로 찾아 사용할 수 있습니다.

/* include/linux/textsearch.h */
struct ts_ops {
    const char         *name;       /* "kmp", "bm", "fsm" */
    struct ts_config * (*init)(const void *pattern,
                                unsigned int len,
                                gfp_t gfp_mask,
                                int flags);
    unsigned int       (*find)(struct ts_config *conf,
                                struct ts_state *state);
    void               (*destroy)(struct ts_config *conf);
    struct ts_config * (*get_pattern)(struct ts_config *conf);
    unsigned int       (*get_pattern_len)(struct ts_config *conf);
    struct module      *owner;      /* THIS_MODULE */
    struct list_head   list;        /* 전역 알고리즘 리스트 */
};
ts_ops Callback Flow textsearch_register(ops) ts_ops global list textsearch_prepare("kmp", ...) lookup ops->init(pattern, len) ts_config * textsearch_find(conf, state) ops->find(conf, state) position (UINT_MAX=miss) textsearch_destroy(conf) ops->destroy(conf) 모든 API 호출은 ts_ops 콜백을 통해 알고리즘에 위임됨

알고리즘 등록/해제

/* lib/textsearch.c */
int textsearch_register(struct ts_ops *ops)
{
    int err = -EEXIST;
    struct ts_ops *o;

    if (ops->name == NULL || ops->find == NULL
        || ops->init == NULL)
        return -EINVAL;

    spin_lock(&ts_mod_lock);
    list_for_each_entry(o, &ts_ops_list, list) {
        if (!strcmp(o->name, ops->name))
            goto errout;
    }
    list_add_tail_rcu(&ops->list, &ts_ops_list);
    err = 0;
errout:
    spin_unlock(&ts_mod_lock);
    return err;
}
EXPORT_SYMBOL(textsearch_register);
모듈 자동 로드: textsearch_prepare()에서 TS_AUTOLOAD 플래그가 설정되어 있으면, 요청한 알고리즘 모듈이 로드되지 않은 경우 request_module("ts_%s", name)으로 자동 로드를 시도합니다. 이는 iptables 규칙 추가 시 커널 모듈(Kernel Module)이 자동으로 로드되는 메커니즘입니다.

KMP 알고리즘 (ts_kmp)

Knuth-Morris-Pratt(KMP) 알고리즘은 텍스트 검색에서 가장 널리 사용되는 정확 매칭 알고리즘입니다. 핵심 아이디어는 실패 함수(failure function)를 전처리하여, 불일치 발생 시 이미 매칭된 접두사 정보를 재활용(Recycling)하는 것입니다.

실패 함수(Failure Function) 구축

실패 함수 F[i]는 패턴의 처음 i+1개 문자로 구성된 부분 문자열에서 접두사이면서 접미사인 최대 길이를 저장합니다. 이 테이블을 통해 불일치 시 패턴을 얼마나 이동시킬지 결정합니다.

/* lib/ts_kmp.c — 실패 함수 구축 (simplified) */
static void compute_prefix_tbl(const u8 *pattern, unsigned int len,
                                unsigned int *prefix_tbl)
{
    unsigned int k = 0;
    prefix_tbl[0] = 0;

    for (unsigned int q = 1; q < len; q++) {
        while (k > 0 && pattern[k] != pattern[q])
            k = prefix_tbl[k - 1];
        if (pattern[k] == pattern[q])
            k++;
        prefix_tbl[q] = k;
    }
}
KMP Failure Function: pattern = "ABCABD" Index: Pattern: F[i]: 0 1 2 3 4 5 A B C A B D 0 0 0 1 2 0 F[3]=1: "ABCA" prefix "A" = suffix "A" F[4]=2: "ABCAB" prefix "AB" = suffix "AB" Matching: text = "XABCABCABD" Step 1: X A B C A B C A B D A X mismatch at i=0, j=0, shift j to F[-1]=0 Step 2: X A B C A B C A B D A B C A B D match 5 chars, mismatch at j=5 F[4]=2, jump j to 2 (skip "AB") Step 3: X A B C A B C A B D C A B D j starts at 2 (reuse "AB") MATCH at position 4! 실패 함수로 이미 매칭된 접두사를 재활용하여 역추적 없이 O(n+m) 달성

커널 KMP 검색 구현

/* lib/ts_kmp.c — kmp_find() 핵심 루프 */
static unsigned int kmp_find(struct ts_config *conf,
                             struct ts_state *state)
{
    struct ts_kmp *kmp = ts_config_priv(conf);
    unsigned int text_len, consumed = state->offset;
    const u8 *text;
    unsigned int q = 0;  /* 패턴 내 현재 위치 */

    while ((text_len = conf->get_next_block(consumed, &text,
                                            conf, state)) > 0) {
        for (unsigned int i = 0; i < text_len; i++) {
            while (q > 0 && kmp->pattern[q] != text[i])
                q = kmp->prefix_tbl[q - 1];
            if (kmp->pattern[q] == text[i])
                q++;
            if (q == kmp->pattern_len) {
                state->offset = consumed + i + 1;
                return state->offset - kmp->pattern_len;
            }
        }
        consumed += text_len;
    }
    return UINT_MAX;  /* 매칭 실패 */
}
특성
전처리 시간O(m) — 실패 함수 구축
전처리 공간O(m) — prefix_tbl[m] 배열
검색 시간O(n) — 텍스트를 한 번만 순회
총 시간O(n + m)
역추적(Backtrace)없음 — 텍스트 포인터가 절대 뒤로 가지 않음
적합 조건분산 데이터(skb), 짧은 패턴, 스트리밍 입력
커널에서 KMP가 기본인 이유: KMP의 "역추적 없음" 특성은 skb fragment 경계를 넘나드는 검색에 이상적입니다. get_next_block() 콜백으로 블록 단위로 데이터를 받아오는 구조에서, 이미 소비한 블록을 다시 읽을 필요가 없기 때문입니다.

KMP 대소문자 무시 (TS_IGNORECASE)

TS_IGNORECASE 플래그가 설정된 경우, KMP는 전처리 단계에서 패턴을 소문자로 변환하고, 검색 시에도 텍스트를 소문자로 변환하여 비교합니다.

/* ts_kmp.c — 대소문자 무시 비교 */
static inline int kmp_char_eq(u8 a, u8 b, int icase)
{
    if (icase)
        return tolower(a) == tolower(b);
    return a == b;
}

KMP 연속 검색 패턴

하나의 ts_config로 여러 매칭을 찾으려면 textsearch_find()textsearch_next()를 반복 호출합니다. ts_state.offset이 자동으로 갱신되어 이전 매칭 위치 이후부터 검색합니다.

/* 모든 매칭 위치 출력 */
unsigned int pos;
pos = textsearch_find(conf, &state);
while (pos != UINT_MAX) {
    pr_info("match at offset %u\n", pos);
    pos = textsearch_next(conf, &state);
}
중첩 매칭: KMP는 textsearch_next() 호출 시 이전 매칭의 다음 위치부터 검색을 재개합니다. 패턴 "ABA"로 텍스트 "ABABA"를 검색하면 위치 0과 2에서 두 번 매칭됩니다. 이는 실패 함수가 중첩된 접두사를 자동으로 처리하기 때문입니다.

Boyer-Moore 알고리즘 (ts_bm)

Boyer-Moore 알고리즘은 패턴을 오른쪽에서 왼쪽으로 비교하면서, 불일치 시 두 가지 시프트 규칙을 적용하여 큰 폭으로 건너뛰는 알고리즘입니다. 긴 패턴과 큰 알파벳에서 KMP보다 빠른 평균 성능을 보입니다.

나쁜 문자 시프트 (Bad Character Shift)

텍스트에서 불일치가 발생한 문자가 패턴에 없으면 패턴 전체를 건너뛰고, 있으면 해당 문자의 패턴 내 마지막 위치에 맞춰 이동합니다.

좋은 접미사 시프트 (Good Suffix Shift)

이미 매칭된 접미사가 패턴의 다른 위치에도 나타나면, 그 위치에 맞춰 이동합니다.

Boyer-Moore Shift Rules Bad Character Shift Text: G O O D _ F O O D Pattern: F O O D compare right-to-left: D=D, O=O, O=O, G!=F 'G' not in pattern shift entire pattern length (4) Text: G O O D _ F O O D F O O D MATCH at position 5! Good Suffix Shift Text: A B C B A B A B C Pattern: B A B A B C compare right-to-left: C=C, B=B, A=A, B=B, C!=A matched suffix "BABC" suffix "BAB" reappears at pos 0 Text: A B C B A B A B C B A B A B C shift by 2 (good suffix alignment)

커널 Boyer-Moore 구현

/* lib/ts_bm.c — 주요 구조체 */
struct ts_bm {
    u8              *pattern;
    unsigned int    patlen;
    unsigned int    bad_shift[ASIZE];  /* 256 엔트리 (바이트 알파벳) */
    unsigned int    good_shift[];     /* patlen 크기 */
};
/* lib/ts_bm.c — bm_find() 핵심 루프 (simplified) */
static unsigned int bm_find(struct ts_config *conf,
                            struct ts_state *state)
{
    struct ts_bm *bm = ts_config_priv(conf);
    unsigned int i = bm->patlen - 1;  /* 텍스트 시작 위치 */
    const u8 *text;
    unsigned int text_len, consumed = state->offset;

    while ((text_len = conf->get_next_block(consumed, &text,
                                            conf, state)) > 0) {
        while (i < text_len) {
            int j = bm->patlen - 1;
            while (j >= 0 && text[i] == bm->pattern[j]) {
                i--; j--;
            }
            if (j < 0) {
                state->offset = consumed + i + 1;
                return state->offset;
            }
            i += max(bm->bad_shift[text[i]],
                     bm->good_shift[j]);
        }
        consumed += text_len;
    }
    return UINT_MAX;
}
특성
전처리 시간O(m + |Σ|) — bad_shift(256) + good_shift(m)
전처리 공간O(m + 256) — 두 시프트 테이블
최선 검색O(n/m) — 긴 패턴에서 큰 폭 건너뛰기
최악 검색O(n*m) — Galil 변형 없는 경우
적합 조건긴 패턴, 연속 메모리 버퍼, 바이너리 데이터
skb 분산 데이터 주의: Boyer-Moore는 텍스트 포인터가 뒤로 이동할 수 있어(i--), skb fragment 경계에서 이전 블록을 다시 참조해야 할 수 있습니다. 커널 구현에서는 이를 처리하지만, 성능 저하가 발생할 수 있으므로 분산 데이터에서는 KMP가 선호됩니다.

Boyer-Moore 전처리 상세

Boyer-Moore의 전처리는 두 테이블을 구축합니다. bad_shift[256] 테이블은 모든 바이트 값에 대해 시프트 거리를 저장하며, O(|Σ|) = O(256)에 구축됩니다. good_shift[m] 테이블은 패턴의 각 위치에서 매칭된 접미사에 대한 시프트를 저장합니다.

/* lib/ts_bm.c — bad_shift 테이블 구축 */
static void compute_bad_shift(struct ts_bm *bm)
{
    int i;

    /* 기본값: 패턴에 없는 문자는 패턴 길이만큼 시프트 */
    for (i = 0; i < ASIZE; i++)
        bm->bad_shift[i] = bm->patlen;

    /* 패턴에 있는 문자: 마지막 출현 위치 기반 시프트 */
    for (i = 0; i < bm->patlen - 1; i++)
        bm->bad_shift[bm->pattern[i]] = bm->patlen - 1 - i;
}
bad_shift 직관: 패턴 "EXAMPLE"(7바이트)에서 bad_shift['X'] = 5 (끝에서 5번째), bad_shift['Z'] = 7 (패턴에 없으므로 전체 길이). 텍스트에서 'Z'를 만나면 패턴 7바이트를 통째로 건너뛸 수 있습니다. 이것이 Boyer-Moore의 sub-linear 성능의 핵심입니다.

FSM 알고리즘 (ts_fsm)

FSM(Finite State Machine) 알고리즘은 단순 문자열이 아닌 토큰 기반 패턴을 매칭할 수 있는 유한 상태 기계입니다. 정규식의 간소화된 버전으로, 문자 클래스·반복·와일드카드를 지원합니다.

FSM Token Matching: "GET /[alpha]+ HTTP" S0 start S1 "GET " S2 "/" S3 [alpha]+ S4 accept "GET " "/" [a-z] " HTTP" [a-z]+ Token Types: TS_FSM_SPECIFIC exact char match TS_FSM_ALPHA alphabetic class TS_FSM_DIGIT numeric class TS_FSM_WILDCARD any character

FSM 토큰 정의

/* include/linux/textsearch_fsm.h */
enum ts_fsm_token_type {
    TS_FSM_SPECIFIC,    /* 특정 문자 매칭 */
    TS_FSM_WILDCARD,    /* 임의 문자 매칭 (.) */
    TS_FSM_CNTRL,       /* 제어 문자 */
    TS_FSM_LOWER,       /* 소문자 [a-z] */
    TS_FSM_UPPER,       /* 대문자 [A-Z] */
    TS_FSM_PUNCT,       /* 구두점 */
    TS_FSM_SPACE,       /* 공백 */
    TS_FSM_DIGIT,       /* 숫자 [0-9] */
    TS_FSM_ALPHA,       /* 알파벳 [a-zA-Z] */
    TS_FSM_ALNUM,       /* 알파벳+숫자 */
    TS_FSM_ASCII,       /* ASCII 문자 */
    __TS_FSM_TYPE_MAX,
};

enum ts_fsm_token_recur {
    TS_FSM_SINGLE,      /* 정확히 1번 */
    TS_FSM_PERHAPS,     /* 0 또는 1번 (?) */
    TS_FSM_ANY,         /* 0회 이상 (*) */
    TS_FSM_MULTI,       /* 1회 이상 (+) */
    TS_FSM_HEAD_IGNORE, /* 앞부분 무시 */
};

struct ts_fsm_token {
    u16     type;       /* ts_fsm_token_type */
    u8      recur;      /* ts_fsm_token_recur */
    u8      value;      /* TS_FSM_SPECIFIC일 때 매칭할 문자 */
};
FSM vs 정규식: FSM은 전체 정규식을 지원하지 않지만, 문자 클래스와 반복 한정자를 통해 프로토콜 파싱에 충분한 패턴 매칭을 제공합니다. NFA/DFA 변환 오버헤드(Overhead)가 없어 커널 공간(Kernel Space)에서 안전합니다.

FSM 매칭 사용 예시

/* FSM으로 "GET /[alpha]+ HTTP/1." 패턴 구성 */
struct ts_fsm_token http_get_pattern[] = {
    { .type = TS_FSM_SPECIFIC, .recur = TS_FSM_SINGLE,
      .value = 'G' },
    { .type = TS_FSM_SPECIFIC, .recur = TS_FSM_SINGLE,
      .value = 'E' },
    { .type = TS_FSM_SPECIFIC, .recur = TS_FSM_SINGLE,
      .value = 'T' },
    { .type = TS_FSM_SPACE,    .recur = TS_FSM_SINGLE },
    { .type = TS_FSM_SPECIFIC, .recur = TS_FSM_SINGLE,
      .value = '/' },
    { .type = TS_FSM_ALPHA,    .recur = TS_FSM_MULTI },
    /* TS_FSM_MULTI = 1회 이상 반복 (+) */
    { .type = TS_FSM_SPACE,    .recur = TS_FSM_SINGLE },
    { .type = TS_FSM_SPECIFIC, .recur = TS_FSM_SINGLE,
      .value = 'H' },
    { .type = TS_FSM_SPECIFIC, .recur = TS_FSM_SINGLE,
      .value = 'T' },
    { .type = TS_FSM_SPECIFIC, .recur = TS_FSM_SINGLE,
      .value = 'T' },
    { .type = TS_FSM_SPECIFIC, .recur = TS_FSM_SINGLE,
      .value = 'P' },
    { .type = TS_FSM_SPECIFIC, .recur = TS_FSM_SINGLE,
      .value = '/' },
    { .type = TS_FSM_DIGIT,   .recur = TS_FSM_SINGLE },
    { .type = TS_FSM_SPECIFIC, .recur = TS_FSM_SINGLE,
      .value = '.' },
};

struct ts_config *conf;
conf = textsearch_prepare("fsm",
                          http_get_pattern,
                          ARRAY_SIZE(http_get_pattern),
                          GFP_KERNEL,
                          TS_AUTOLOAD);
FSM 패턴 설계 팁: TS_FSM_HEAD_IGNORE recur 타입을 첫 번째 토큰에 사용하면 텍스트 시작 부분의 임의 데이터를 건너뛸 수 있습니다. 이는 패킷 페이로드에서 특정 프로토콜 명령이 임의 위치에 나타날 수 있는 경우에 유용합니다.
특성
전처리 시간O(t) — 토큰 배열 복사 (t = 토큰 수)
전처리 공간O(t) — 토큰 배열
검색 시간O(n*t) 최악 — 각 위치에서 모든 토큰 시도
적합 조건프로토콜 파싱, 문자 클래스 매칭, 가변 길이 필드

알고리즘 선택 가이드

세 알고리즘은 각각 다른 시나리오에서 최적의 성능을 보입니다. 선택 기준은 패턴 길이, 데이터 특성, 재사용 빈도입니다.

Algorithm Selection Decision Tree 패턴에 문자 클래스/와일드카드? Yes FSM No 데이터가 skb (비선형/분산)? Yes KMP (권장) No 패턴 길이 > 10 바이트? Yes Boyer-Moore No KMP KMP: 범용, skb 최적, O(n+m) 보장 BM: 긴 패턴, 연속 메모리, O(n/m) 평균 FSM: 패턴 클래스, 프로토콜 파싱 netfilter xt_string에서는 KMP가 기본, BM은 --algo bm으로 명시
기준KMPBoyer-MooreFSM
패턴 유형고정 문자열고정 문자열토큰 패턴
최악 시간O(n+m)O(n*m)O(n*t)
평균 시간O(n)O(n/m)O(n)
전처리 공간O(m)O(m+256)O(t)
분산 데이터우수 (역추적 없음)보통 (역추적 가능)보통
짧은 패턴 (<5B)우수이점 적음오버헤드
긴 패턴 (>20B)좋음우수 (큰 시프트)해당 없음
커널 기본xt_string 기본--algo bmconntrack helper

선택 기준 요약

실전 권장:
  • iptables/nftables에서 기본 문자열 매칭: KMP (기본값, 변경 불필요)
  • 긴 바이너리 시그니처(20B+) 검색: Boyer-Moore (--algo bm)
  • 프로토콜 파싱(가변 필드): FSM (프로그래밍 API 전용)
  • 확신이 없으면: KMP — 모든 시나리오에서 안정적이고 예측 가능한 성능

대부분의 네트워크 보안 규칙에서 패턴은 4~30바이트 범위이며, 이 범위에서 KMP와 Boyer-Moore의 성능 차이는 미미합니다. 알고리즘 선택보다 --from/--to를 통한 검색 범위 최적화가 전체 성능에 훨씬 큰 영향을 미칩니다.

전처리 비용

각 알고리즘의 init() 콜백에서 수행하는 전처리는 패턴이 변경될 때만 한 번 실행되며, 이후 검색은 전처리된 테이블을 재사용합니다. iptables 규칙이 로드될 때 textsearch_prepare()가 호출되고, 이후 모든 패킷 검사는 textsearch_find()만 호출합니다.

알고리즘전처리 내용시간메모리사례 (패턴 32B)
KMPprefix_tbl[] 실패 함수O(m)m * sizeof(unsigned int)128 bytes
Boyer-Moorebad_shift[256] + good_shift[m]O(m + 256)256 * 4 + m * 41,152 bytes
FSM토큰 배열 복사O(t)t * sizeof(ts_fsm_token)가변
재사용 패턴: netfilter 규칙은 일반적으로 시스템 부팅 시 한 번 설정되고 오랜 시간 유지됩니다. 전처리 비용은 규칙 로드 시 1회만 발생하므로, Boyer-Moore의 큰 전처리 테이블(1KB+)도 실질적 부담이 아닙니다. 핵심은 패킷당 반복되는 검색 성능입니다.

메모리 할당은 kmalloc()으로 수행되며, ts_config 구조체 뒤에 알고리즘별 private 데이터가 연속 할당됩니다. 단일 할당으로 구조체와 private 데이터를 함께 배치하면 캐시(Cache) 지역성이 향상되고, 해제 시에도 kfree() 한 번으로 충분합니다.

/* lib/textsearch.c — textsearch_prepare() 할당 구조 */
/* +------+----------+------------------+
 * | hdr  | ts_config | algo private data |
 * +------+----------+------------------+
 * ^                   ^
 * conf               ts_config_priv(conf)
 */
struct ts_config *conf;
conf = kmalloc(sizeof(struct ts_config) + priv_size, gfp_mask);
/* priv_size는 알고리즘이 요구하는 추가 크기 */

API 레퍼런스

텍스트 검색 프레임워크의 공개 API는 6개의 핵심 함수로 구성됩니다.

textsearch_prepare()

struct ts_config *textsearch_prepare(
    const char *algo,           /* "kmp", "bm", "fsm" */
    const void *pattern,        /* 검색 패턴 */
    unsigned int len,           /* 패턴 길이 */
    gfp_t gfp_mask,             /* GFP_KERNEL 등 */
    int flags                   /* TS_AUTOLOAD | TS_IGNORECASE */
);

알고리즘 이름으로 ts_ops를 찾고, ops->init()을 호출하여 패턴을 전처리합니다. 반환된 ts_config *를 이후 검색에 사용합니다.

textsearch_find() / textsearch_next()

static inline unsigned int textsearch_find(
    struct ts_config *conf,
    struct ts_state *state)
{
    state->offset = 0;
    return textsearch_next(conf, state);
}

static inline unsigned int textsearch_next(
    struct ts_config *conf,
    struct ts_state *state)
{
    unsigned int ret = conf->ops->find(conf, state);
    if (ret != UINT_MAX)
        state->offset = ret + 1;
    return ret;
}

textsearch_find()는 오프셋을 0으로 리셋하고 첫 매칭을 반환합니다. textsearch_next()는 이전 매칭 이후부터 다음 매칭을 찾습니다. 반환값이 UINT_MAX이면 매칭이 없습니다.

반환값 규약: 매칭 성공 시 반환되는 오프셋은 패턴이 시작되는 위치입니다. 예를 들어 텍스트 "Hello World"에서 패턴 "World"를 검색하면 6이 반환됩니다. xt_string에서는 이 값이 UINT_MAX가 아닌지만 확인하고 (매칭 존재 여부), 정확한 위치는 사용하지 않습니다. conntrack 헬퍼에서는 이 위치를 기반으로 후속 파싱을 수행합니다.

textsearch_destroy()

static inline void textsearch_destroy(struct ts_config *conf)
{
    if (conf->ops->destroy)
        conf->ops->destroy(conf);
    module_put(conf->ops->owner);
    kfree(conf);
}

textsearch_register() / textsearch_unregister()

int textsearch_register(struct ts_ops *ops);
int textsearch_unregister(struct ts_ops *ops);

알고리즘 모듈이 로드/언로드될 때 호출됩니다. ts_mod_lock spinlock으로 보호되는 전역 리스트에 추가/제거합니다. 같은 이름의 알고리즘이 이미 등록되어 있으면 -EEXIST를 반환합니다.

커스텀 알고리즘 등록: 커널 모듈에서 자체 검색 알고리즘을 구현하고 textsearch_register()로 등록하면, textsearch_prepare("myalgo", ...)로 사용할 수 있습니다. 최소 구현 요구사항은 name, init, find 세 필드입니다.
함수컨텍스트반환용도
textsearch_prepare()프로세스 (슬립 가능)ts_config * 또는 ERR_PTR패턴 전처리
textsearch_find()모든 컨텍스트오프셋 또는 UINT_MAX첫 매칭 검색
textsearch_next()모든 컨텍스트오프셋 또는 UINT_MAX다음 매칭 검색
textsearch_destroy()프로세스void자원 해제
textsearch_register()모듈 init0 또는 -errno알고리즘 등록
textsearch_unregister()모듈 exit0 또는 -errno알고리즘 해제

skb와 텍스트 검색

네트워크 패킷은 sk_buff(skb) 구조체에 담기며, 페이로드가 선형 영역과 비선형 fragments로 분산될 수 있습니다. skb_find_text()는 이 분산 데이터를 선형화하지 않고 순회하며 패턴을 검색하는 래퍼 함수입니다.

/* net/core/utils.c */
unsigned int skb_find_text(struct sk_buff *skb,
                           unsigned int from,
                           unsigned int to,
                           struct ts_config *config)
{
    struct ts_state state;
    unsigned int ret;

    BUILD_BUG_ON(sizeof(struct skb_seq_state) >
                 sizeof(state.cb));

    config->get_next_block = skb_ts_get_next_block;
    config->finish = skb_ts_finish;

    skb_prepare_seq_read(skb, from, to, TS_SKB_CB(&state));
    ret = textsearch_find(config, &state);

    return ret;
}

skb_seq_state 반복자

skb_seq_state는 skb의 선형 영역, frags[], frag_list를 순차적으로 순회하는 반복자입니다. get_next_block() 콜백에 바인딩되어 텍스트 검색 알고리즘에 블록 단위로 데이터를 제공합니다.

/* include/linux/skbuff.h */
struct skb_seq_state {
    __u32           lower_offset;  /* 검색 시작 오프셋 */
    __u32           upper_offset;  /* 검색 종료 오프셋 */
    __u32           frag_idx;      /* 현재 fragment 인덱스 */
    __u32           stepped_offset;/* 현재까지 진행한 바이트 */
    struct sk_buff  *root_skb;     /* 원본 skb */
    struct sk_buff  *cur_skb;      /* 현재 순회 중인 skb */
    __u8            *frag_data;    /* kmap된 fragment 데이터 */
    __u32           frag_off;      /* fragment 내 오프셋 */
};
선형화 회피의 장점: skb_linearize()는 모든 fragments를 연속 메모리로 복사하므로 O(n) 추가 비용과 메모리 할당이 필요합니다. skb_find_text()는 fragments를 제자리에서 순회하므로 복사 비용이 없고, 고속 패킷 처리 경로에서 성능 이점이 큽니다.

skb 데이터 레이아웃과 검색 범위

skb의 데이터 영역은 선형 영역(head~tail), 페이지(Page) fragments(skb_shinfo->frags[]), frag_list(GSO/GRO 결합 패킷)로 구성됩니다. skb_find_text()from/to 매개변수는 이 전체 영역에 대한 논리적 오프셋입니다.

/* skb 데이터 영역 구조 */
/*
 * offset 0                    skb->len
 * |                           |
 * +--------+----------+-------+--------+
 * | linear | frag[0]  |frag[1]|frag_lst|
 * | (head~ | (page    | (page | (chained
 * |  tail) |  frag)   |  frag)|  skbs)
 * +--------+----------+-------+--------+
 *          ^                           ^
 *      skb->data_len 시작        skb->len 끝
 *
 * skb_find_text(skb, from, to, config)
 *   from/to는 이 전체 범위 내의 오프셋
 */
검색 범위 최적화: netfilter에서 xt_string--from/--to 옵션은 이 오프셋에 직접 매핑(Mapping)됩니다. IP+TCP 헤더가 보통 40-60바이트이므로, HTTP 페이로드 검색 시 --from 40으로 시작하면 헤더 영역의 불필요한 검색을 생략할 수 있습니다.

skb_seq_read 상태 전이

skb_seq_read()는 호출될 때마다 다음 연속 데이터 블록을 반환합니다. 내부적으로 3단계 상태 전이를 거칩니다.

단계데이터 소스반환
1단계선형 영역 (skb->data ~ skb->tail)선형 데이터 포인터
2단계frags[0..nr_frags-1]kmap된 페이지 포인터
3단계frag_list의 각 skb재귀적 순회

netfilter xt_string

xt_string은 netfilter의 문자열 매칭 확장 모듈로, iptables/ip6tables의 -m string 옵션을 구현합니다. 패킷 페이로드에서 특정 문자열을 검색하여 매칭 여부를 판단합니다.

xt_string Packet Processing Path Packet (skb) Netfilter Hook xt_string match() from/to offset (L3/L4 header skip) skb_find_text(skb, from, to) textsearch_find(conf, state) KMP / BM / FSM MATCH NO MATCH found UINT_MAX DROP/ACCEPT/LOG continue chain

xt_string 구현

/* net/netfilter/xt_string.c */
static bool string_mt(const struct sk_buff *skb,
                      struct xt_action_param *par)
{
    const struct xt_string_info *conf = par->matchinfo;
    bool invert = conf->u.v1.flags & XT_STRING_FLAG_INVERT;

    return (skb_find_text((struct sk_buff *)skb,
                          conf->from_offset,
                          conf->to_offset,
                          conf->config) != UINT_MAX) ^ invert;
}

규칙 초기화 (checkentry)

static int string_mt_check(const struct xt_mtchk_param *par)
{
    struct xt_string_info *conf = par->matchinfo;

    /* 알고리즘 선택: "kmp"(기본) 또는 "bm" */
    conf->config = textsearch_prepare(conf->algo,
                        conf->pattern,
                        conf->patlen,
                        GFP_KERNEL,
                        TS_AUTOLOAD);
    if (IS_ERR(conf->config))
        return PTR_ERR(conf->config);
    return 0;
}

nf_conntrack 활용

netfilter 연결 추적(conntrack) 헬퍼는 텍스트 검색을 사용하여 Application Layer Gateway(ALG) 프로토콜을 파싱합니다. FTP, SIP, IRC 등의 프로토콜에서 데이터 연결에 필요한 주소/포트 정보를 페이로드에서 추출합니다.

FTP 헬퍼 사례

FTP의 PORT/PASV 명령은 데이터 연결을 위한 IP 주소와 포트를 ASCII 텍스트로 전송합니다. conntrack FTP 헬퍼는 이 텍스트를 검색하여 관련(related) 연결을 기대(expect)합니다.

/* nf_conntrack_ftp.c — 텍스트 검색 활용 (개념) */
static struct ts_config *ts_port;
static struct ts_config *ts_pasv;

/* 모듈 초기화 */
ts_port = textsearch_prepare("kmp", "PORT ", 5,
                             GFP_KERNEL, TS_AUTOLOAD);
ts_pasv = textsearch_prepare("kmp", "227 ", 4,
                             GFP_KERNEL, TS_AUTOLOAD);

/* 패킷 처리 시 */
unsigned int pos = skb_find_text(skb, dataoff, skb->len, ts_port);
if (pos != UINT_MAX) {
    /* PORT 명령 발견 → 주소/포트 파싱 후 expect 생성 */
    parse_port_command(skb, pos);
}
보안 고려: conntrack 헬퍼의 텍스트 검색은 NAT 환경에서 Application Layer Gateway 기능에 필수적이지만, 보안 표면을 넓히는 요인이기도 합니다. nf_conntrack_helper_auto sysctl을 비활성화하고 필요한 헬퍼만 명시적으로 로드하는 것이 권장됩니다.
헬퍼검색 패턴용도
FTP"PORT ", "227 ", "EPRT", "229 "데이터 연결 주소/포트 추출
SIP"SIP/2.0", "Via:", "Contact:"SDP 미디어 주소 추적
IRC"DCC "DCC 파일 전송 주소 추출
TFTP바이너리 opcode 매칭TFTP 데이터 연결 추적

SIP 헬퍼 사례

SIP(Session Initiation Protocol) 헬퍼는 INVITE/BYE/ACK 메시지와 SDP(Session Description Protocol) 본문에서 미디어 스트림의 IP 주소와 포트를 추출합니다. NAT 환경에서 SIP 통화가 정상 작동하려면 이 텍스트 검색이 필수적입니다.

/* nf_conntrack_sip.c — SIP 헤더 검색 (개념) */
static struct ts_config *ts_sip_via;
static struct ts_config *ts_sip_contact;
static struct ts_config *ts_sip_content;

/* 모듈 초기화 */
ts_sip_via = textsearch_prepare("kmp", "\r\nVia:", 6,
                                GFP_KERNEL, TS_AUTOLOAD);
ts_sip_contact = textsearch_prepare("kmp", "\r\nContact:", 10,
                                    GFP_KERNEL, TS_AUTOLOAD);
ts_sip_content = textsearch_prepare("kmp", "\r\nContent-Type:", 15,
                                    GFP_KERNEL, TS_AUTOLOAD);

/* 패킷 처리 시 — Via 헤더에서 주소 추출 */
unsigned int pos = skb_find_text(skb, dataoff, datalen,
                                 ts_sip_via);
if (pos != UINT_MAX) {
    /* Via: SIP/2.0/UDP 192.168.1.100:5060 파싱 */
    parse_via_header(skb, pos + 6, &addr, &port);
    /* NAT 변환을 위한 expect 등록 */
    nf_ct_expect_init(exp, NF_CT_EXPECT_CLASS_DEFAULT,
                       nf_ct_l3num(ct), &addr, ...);
}

conntrack 헬퍼 보안 고려

ALG 보안 위험: conntrack 헬퍼는 패킷 페이로드를 파싱하여 방화벽(Firewall)에 동적 핀홀(pinhole)을 엽니다. 이는 공격자가 조작된 프로토콜 메시지로 방화벽을 우회할 수 있는 경로를 제공합니다. 다음 보안 설정을 권장합니다.
# 자동 헬퍼 할당 비활성화 (수동 설정으로 전환)
echo 0 > /proc/sys/net/netfilter/nf_conntrack_helper

# 필요한 헬퍼만 명시적으로 할당 (nftables)
nft add ct helper inet filter ftp-helper \
  { type "ftp" protocol tcp \; }
nft add rule inet filter input \
  tcp dport 21 ct helper set "ftp-helper"

# 기대(expect) 테이블 모니터링
conntrack -E expect   # 실시간 expect 이벤트 추적
conntrack -L expect   # 현재 expect 목록 조회

iptables/nftables 설정

실제 운영 환경에서 텍스트 검색을 활용하는 방법을 iptables와 nftables 예제로 설명합니다.

iptables -m string 기본 문법

# KMP 알고리즘(기본)으로 HTTP GET 요청에서 "/admin" 검색
iptables -A INPUT -p tcp --dport 80 \
  -m string --string "/admin" --algo kmp \
  --from 40 --to 200 -j DROP

# Boyer-Moore로 대소문자 무시 검색
iptables -A INPUT -p tcp --dport 80 \
  -m string --string "password" --algo bm \
  --icase -j LOG --log-prefix "[PASSWD_LEAK] "

# 16진수 패턴 매칭 (바이너리 시그니처)
iptables -A FORWARD -m string \
  --hex-string "|7f 45 4c 46|" --algo kmp \
  -j DROP   # ELF 바이너리 전송 차단

# --from/--to로 검색 범위 제한 (성능 최적화)
iptables -A INPUT -p tcp --dport 443 \
  -m string --string "TLS_RSA" --algo bm \
  --from 0 --to 100 -j LOG

nftables 설정

# nftables에서 텍스트 검색 (nft expression)
nft add rule inet filter input \
  tcp dport 80 \
  meta l4proto tcp \
  @th,160,1280 "GET /admin" \
  drop

# payload match를 활용한 HTTP 메서드 필터링
nft add rule inet filter input \
  tcp dport 80 \
  tcp payload offset 0 length 3 "GET" \
  counter accept
성능 영향: 패킷 페이로드 검색은 CPU 집약적입니다. 고속 네트워크(10Gbps+)에서 모든 패킷에 문자열 검색을 적용하면 심각한 성능 저하가 발생합니다. --from/--to로 검색 범위를 제한하고, 먼저 포트/프로토콜로 대상 패킷을 좁힌 후 문자열 검색을 적용하세요.

실전 DPI 규칙 예시

# SQL Injection 기본 방어 (여러 패턴 체인)
iptables -A INPUT -p tcp --dport 80 \
  -m string --string "UNION SELECT" --algo kmp --icase \
  -j DROP

iptables -A INPUT -p tcp --dport 80 \
  -m string --string "OR 1=1" --algo kmp --icase \
  -j DROP

# DNS 터널링 탐지 (긴 하위 도메인)
iptables -A OUTPUT -p udp --dport 53 \
  -m string --hex-string "|3f|" --algo bm \
  -m length --length 200: -j LOG --log-prefix "[DNS_TUNNEL] "

xt_string 옵션 상세

옵션설명기본값
--string "패턴"검색할 문자열 (ASCII)필수
--hex-string "|xx xx|"16진수 바이너리 패턴-
--algo kmp|bm검색 알고리즘 선택kmp
--from N검색 시작 오프셋 (바이트)0
--to N검색 종료 오프셋 (바이트)패킷 끝
--icase대소문자 무시비활성
! --string매칭 반전 (NOT)비활성

iptables 규칙 최적화 전략

규칙 순서 최적화: iptables 체인은 순서대로 평가됩니다. string 매칭은 CPU 집약적이므로, 저비용 매칭(포트, 프로토콜, conntrack 상태)을 먼저 배치하여 대부분의 패킷을 빠르게 통과시키고, string 매칭은 마지막에 적용하세요.
# 나쁜 예: 모든 패킷에 string 검색 적용
iptables -A INPUT -m string --string "malware" --algo kmp -j DROP
iptables -A INPUT -p tcp --dport 80 -j ACCEPT

# 좋은 예: 먼저 포트로 좁히고, 해당 트래픽에만 string 검색
iptables -A INPUT -p tcp --dport 80 -j WEB_INSPECT
iptables -A WEB_INSPECT -m conntrack --ctstate ESTABLISHED -j ACCEPT
iptables -A WEB_INSPECT -m string --string "malware" --algo kmp \
  --from 40 --to 500 -j DROP
iptables -A WEB_INSPECT -j ACCEPT

nftables와 payload 표현식

nftables에서는 @th(transport header) 또는 @ih(inner header) 표현식을 사용하여 더 정밀한 페이로드 검색이 가능합니다. nftables의 집합(set)과 결합하면 다수의 패턴을 효율적으로 매칭할 수 있습니다.

# nftables 연결 추적 헬퍼 명시적 할당
nft add ct helper inet filter ftp-helper \
  { type "ftp" protocol tcp \; }
nft add rule inet filter input \
  tcp dport 21 ct helper set "ftp-helper"

# nftables 문자열 매칭 (rawpayload 사용)
nft add rule inet filter input \
  tcp dport 80 \
  tcp flags syn / syn,ack \
  counter accept

nft add rule inet filter input \
  tcp dport 80 \
  meta l4proto tcp \
  @th,160,800 "GET /admin" \
  counter drop

성능 벤치마크

세 알고리즘의 성능은 패턴 길이, 텍스트 특성, 매칭 빈도에 따라 크게 달라집니다. 아래는 일반적인 네트워크 페이로드(HTTP 트래픽)에서의 벤치마크 경향입니다.

Performance Comparison (cycles per byte) cycles/byte 5.0 4.0 3.0 2.0 1.0 0.0 Pattern Length (bytes) 4 8 16 32 64 128 KMP Boyer-Moore FSM BM은 긴 패턴에서 시프트 이점이 커져 cycles/byte가 감소 (lower = faster)
시나리오최선 알고리즘이유
짧은 패턴 (1-8B), skbKMP전처리 비용 최소, 역추적 없음
긴 패턴 (20B+), 연속 메모리Boyer-MooreO(n/m) 평균, 큰 시프트 이점
문자 클래스 패턴FSM유일한 선택지
높은 트래픽, 다수 규칙KMP예측 가능한 O(n) 성능
바이너리 데이터 (높은 엔트로피)Boyer-Moore불일치가 빈번하여 시프트 극대화
반복 패턴 (낮은 엔트로피)KMPBM 시프트가 작아져 이점 감소
캐시 효과: 실제 커널에서는 L1/L2 캐시 효과가 이론적 복잡도보다 중요합니다. KMP의 prefix_tbl은 보통 수십 바이트로 L1에 완전히 적재되지만, Boyer-Moore의 bad_shift[256]는 1KB로 L1 압박을 줄 수 있습니다. 짧은 패턴에서 KMP가 BM보다 빠른 이유 중 하나입니다.

벤치마크 분석 상세

패턴 길이별 성능 특성을 더 자세히 분석합니다.

패턴 길이KMP (ns/1KB)BM (ns/1KB)FSM (ns/1KB)비고
4B ("GET ")~800~900~1500KMP 우위, BM 시프트 이점 미미
8B ("HTTP/1.1")~750~700~1400BM이 KMP와 비슷해지기 시작
16B~700~500~1300BM 시프트 이점 발현
32B~680~350~1200BM이 KMP의 절반 수준
64B~670~220~1100BM 최적 영역
128B~660~150~1050BM sub-linear 검색
실측 방법: 커널 내 벤치마크는 ktime_get_ns()로 측정할 수 있습니다. 테스트 모듈에서 textsearch_prepare()textsearch_find()를 수천 회 반복하고 평균을 계산합니다. NOHZ_FULL CPU에서 측정하면 인터럽트(Interrupt) 간섭을 최소화할 수 있습니다.

softirq 경로에서의 영향

netfilter에서 텍스트 검색은 주로 softirq 컨텍스트에서 실행됩니다. 패킷당 검색 시간이 길어지면 softirq 처리 시간이 증가하여 네트워크 지연(latency)에 직접적 영향을 줍니다. 다수의 string 규칙이 체인에 있으면 각 규칙마다 전체 페이로드를 스캔해야 하므로, 규칙 수에 비례하여 CPU 사용량이 증가합니다.

# softirq CPU 사용량 모니터링
mpstat -P ALL 1 | grep -E "CPU|soft"

# NET_RX softirq에서 textsearch 오버헤드 확인
perf top -e cpu-clock -g --symbol-filter="kmp_find\|bm_find\|string_mt"

주요 커널 호출자

텍스트 검색 프레임워크를 사용하는 커널 서브시스템은 주로 네트워킹 관련 코드에 집중되어 있습니다.

호출자소스 파일용도알고리즘
xt_stringnet/netfilter/xt_string.ciptables 문자열 매칭KMP/BM (사용자 선택)
nf_conntrack_ftpnet/netfilter/nf_conntrack_ftp.cFTP PORT/PASV 파싱KMP
nf_conntrack_sipnet/netfilter/nf_conntrack_sip.cSIP 헤더 파싱KMP
nf_conntrack_ircnet/netfilter/nf_conntrack_irc.cIRC DCC 파싱KMP
nf_conntrack_amandanet/netfilter/nf_conntrack_amanda.cAmanda 백업 프로토콜KMP
ematch (TC)net/sched/em_text.cTC 트래픽 분류KMP/BM

TC ematch 텍스트 분류

Traffic Control(TC)의 ematch 모듈도 텍스트 검색을 활용하여 패킷을 분류합니다. 이를 통해 특정 페이로드 패턴을 가진 트래픽에 QoS 정책을 적용할 수 있습니다.

# TC ematch로 텍스트 기반 트래픽 분류
tc filter add dev eth0 parent 1:0 protocol ip \
  u32 match ip dport 80 0xffff \
  action ematch text("video", kmp) \
  flowid 1:10   # video 트래픽을 1:10 큐로 분류

em_text 내부 구현

net/sched/em_text.c는 TC ematch 프레임워크의 텍스트 매칭 모듈입니다. xt_string과 유사하게 textsearch_prepare()로 초기화하고 skb_find_text()로 검색합니다.

/* net/sched/em_text.c — 핵심 매칭 함수 */
static int em_text_match(struct sk_buff *skb,
                        struct tcf_ematch *m,
                        struct tcf_pkt_info *info)
{
    struct text_match *tm = EM_TEXT_PRIV(m);
    int from, to;

    from = tcf_get_base_ptr(skb, tm->from_layer)
           + tm->from_offset;
    to = tcf_get_base_ptr(skb, tm->to_layer)
         + tm->to_offset;

    return (skb_find_text(skb, from, to,
                          tm->config) != UINT_MAX);
}

텍스트 검색 활용 가능 영역

현재 커널에서는 네트워킹 서브시스템이 주요 사용처이지만, 텍스트 검색 프레임워크는 범용적으로 설계되어 있어 다음 영역에서도 활용 가능합니다.

구현 상세

텍스트 검색 프레임워크의 내부 구현에서 주요한 설계 결정과 구현 세부사항을 분석합니다.

모듈 등록과 자동 로드

/* lib/ts_kmp.c — 모듈 초기화 */
static struct ts_ops kmp_ops = {
    .name       = "kmp",
    .find       = kmp_find,
    .init       = kmp_init,
    .get_pattern     = kmp_get_pattern,
    .get_pattern_len = kmp_get_pattern_len,
    .owner      = THIS_MODULE,
};

static int __init init_kmp(void)
{
    return textsearch_register(&kmp_ops);
}

static void __exit exit_kmp(void)
{
    textsearch_unregister(&kmp_ops);
}

module_init(init_kmp);
module_exit(exit_kmp);
MODULE_ALIAS("ts_kmp");

MODULE_ALIAS("ts_kmp")request_module("ts_kmp")가 호출될 때 이 모듈을 자동으로 찾을 수 있게 합니다.

slab 할당과 메모리 레이아웃

/* textsearch_prepare() 내부 할당 흐름 */
struct ts_config *textsearch_prepare(const char *algo, ...)
{
    struct ts_ops *ops;
    struct ts_config *conf;

    /* 1. 알고리즘 이름으로 ts_ops 찾기 */
    ops = lookup_ts_algo(algo);
    if (!ops && flags & TS_AUTOLOAD) {
        request_module("ts_%s", algo);
        ops = lookup_ts_algo(algo);
    }

    /* 2. ops->init()이 ts_config + private data 할당 */
    conf = ops->init(pattern, len, gfp_mask, flags);
    conf->ops = ops;

    return conf;
}

RCU 보호

전역 ts_ops_listlist_add_tail_rcu()/list_del_rcu()로 관리됩니다. 검색 중인(hot path) textsearch_find()는 RCU 읽기 측이므로 락 없이 ts_ops에 접근할 수 있습니다. 알고리즘 등록/해제는 ts_mod_lock spinlock으로 직렬화(Serialization)됩니다.

경로보호 메커니즘컨텍스트
알고리즘 등록/해제ts_mod_lock (spinlock)모듈 init/exit
알고리즘 조회RCU readtextsearch_prepare()
패턴 검색없음 (ts_config 불변)softirq/인터럽트 가능
ts_state 접근없음 (호출자 로컬)스택 할당

flags 비트 필드

#define TS_AUTOLOAD    1  /* 알고리즘 모듈 자동 로드 */
#define TS_IGNORECASE  2  /* 대소문자 무시 (icase) */

TS_IGNORECASE가 설정되면 패턴과 텍스트를 비교할 때 tolower()를 적용합니다. iptables --icase 옵션이 이 플래그를 활성화합니다.

get_next_block 콜백 메커니즘

get_next_block()은 텍스트 검색 프레임워크의 핵심 추상화입니다. 이 콜백을 통해 알고리즘은 데이터 소스가 연속 메모리인지, skb fragment 체인인지 알 필요 없이 동일한 방식으로 데이터를 순회합니다.

/* 연속 메모리 버퍼용 get_next_block */
static unsigned int simple_get_next_block(
    unsigned int consumed,
    const u8 **dst,
    struct ts_config *conf,
    struct ts_state *state)
{
    struct ts_linear_state *st = (struct ts_linear_state *)state->cb;

    if (consumed >= st->len)
        return 0;  /* 데이터 소진 */

    *dst = st->data + consumed;
    return st->len - consumed;
}

/* skb용 get_next_block — skb_seq_read() 활용 */
static unsigned int skb_ts_get_next_block(
    unsigned int consumed,
    const u8 **dst,
    struct ts_config *conf,
    struct ts_state *state)
{
    return skb_seq_read(consumed, dst,
                         TS_SKB_CB(state));
}
커스텀 데이터 소스: get_next_block 콜백을 직접 구현하면 파일 시스템 버퍼, DMA 영역, 사용자 공간(User Space) 메모리 등 임의 데이터 소스에서 텍스트 검색을 수행할 수 있습니다. ts_state.cb[]의 40바이트 공간에 반복자 상태를 저장합니다.

모듈 참조 카운트(Reference Count)

textsearch_prepare()는 알고리즘 모듈의 참조 카운트를 증가시키고(try_module_get()), textsearch_destroy()에서 감소시킵니다(module_put()). 이를 통해 검색 설정이 활성 상태인 동안 알고리즘 모듈이 언로드되는 것을 방지합니다.

/* textsearch_prepare() 내부 */
if (!try_module_get(ops->owner))
    return ERR_PTR(-EBUSY);

/* textsearch_destroy() 내부 */
module_put(conf->ops->owner);

Kconfig 옵션

/* lib/Kconfig (관련 설정) */
config TEXTSEARCH
    bool

config TEXTSEARCH_KMP
    tristate "Knuth-Morris-Pratt"
    select TEXTSEARCH

config TEXTSEARCH_BM
    tristate "Boyer-Moore"
    select TEXTSEARCH

config TEXTSEARCH_FSM
    tristate "Finite state machine"
    select TEXTSEARCH

TEXTSEARCH는 bool 타입으로 직접 선택할 수 없고, 알고리즘 모듈이 선택될 때 자동으로 활성화됩니다. xt_stringCONFIG_NETFILTER_XT_MATCH_STRING으로 활성화하며, 이것이 TEXTSEARCH_KMP를 자동 선택합니다.

ts_config_priv 매크로(Macro)

알고리즘별 private 데이터에 접근하는 표준 방법은 ts_config_priv() 인라인 함수(Inline Function)입니다.

/* include/linux/textsearch.h */
static inline void *ts_config_priv(struct ts_config *conf)
{
    return ((u8 *)conf + TS_PRIV_ALIGN(sizeof(*conf)));
}

/* 사용 예: KMP의 private 데이터 접근 */
struct ts_kmp *kmp = ts_config_priv(conf);
/* kmp->pattern, kmp->prefix_tbl 등에 접근 */

TS_PRIV_ALIGN은 private 데이터가 올바르게 정렬되도록 ts_config 크기를 ALIGNOF(unsigned long) 경계에 맞춥니다. 이를 통해 알고리즘의 전처리 테이블이 정렬되지 않은 메모리 접근(unaligned access)으로 인한 성능 저하를 방지합니다.

에러 처리 패턴

/* textsearch_prepare() 에러 처리 모범 사례 */
struct ts_config *conf;
conf = textsearch_prepare("kmp", pattern, len,
                          GFP_KERNEL, TS_AUTOLOAD);
if (IS_ERR(conf)) {
    switch (PTR_ERR(conf)) {
    case -ENOENT:
        pr_err("algorithm not found\n");
        break;
    case -ENOMEM:
        pr_err("memory allocation failed\n");
        break;
    case -EINVAL:
        pr_err("invalid pattern\n");
        break;
    }
    return PTR_ERR(conf);
}

디버깅(Debugging) 가이드

텍스트 검색 관련 문제를 진단하는 방법과 도구를 설명합니다.

iptables 규칙 확인

# 현재 string 매칭 규칙 확인
iptables -L -v -n | grep "STRING match"

# 상세 규칙 정보 (알고리즘, 패턴, 범위)
iptables -L INPUT -v -n --line-numbers

# 규칙별 패킷/바이트 카운터로 매칭 빈도 확인
iptables -L -v -n -x

# 실시간 매칭 모니터링
watch -n 1 'iptables -L INPUT -v -n -x | grep "STRING"'

커널 모듈 상태 확인

# textsearch 모듈 로드 상태
lsmod | grep ts_

# 예상 출력:
# ts_kmp                  16384  1 xt_string
# ts_bm                   16384  0

# xt_string 모듈 의존성
modinfo xt_string
modinfo ts_kmp

# 모듈 수동 로드 (자동 로드 실패 시)
modprobe ts_kmp
modprobe ts_bm
modprobe xt_string

성능 프로파일링(Profiling)

# perf로 textsearch 함수 프로파일링
perf record -g -a -- sleep 10
perf report --symbol-filter="textsearch\|kmp_find\|bm_find"

# ftrace로 함수 추적
echo 1 > /sys/kernel/debug/tracing/events/nf_tables/enable
echo "kmp_find bm_find" > /sys/kernel/debug/tracing/set_ftrace_filter
echo function > /sys/kernel/debug/tracing/current_tracer
cat /sys/kernel/debug/tracing/trace_pipe

# BPF를 활용한 상세 지연 분석
# bpftrace -e 'kprobe:kmp_find { @start = nsecs; }
#              kretprobe:kmp_find { @latency = hist(nsecs - @start); }'
흔한 실수:
  • --from/--to 오프셋 미설정 — 전체 패킷을 검색하여 성능 저하
  • --algo 미지정 — 기본 KMP 사용 (의도적이면 괜찮지만, 긴 패턴에서는 BM 검토)
  • 모듈 미로드 — TS_AUTOLOAD 없이 사용하면 알고리즘을 찾지 못함
  • 검색 범위 초과 — --to가 패킷 길이를 넘으면 불필요한 fragment 순회 발생

textsearch 설정 디버깅 코드

/* 커널 모듈에서 textsearch 동작 확인 */
static void test_textsearch(void)
{
    struct ts_config *conf;
    struct ts_state state;
    const char *text = "Hello World Hello";
    unsigned int pos;

    conf = textsearch_prepare("kmp", "Hello", 5,
                              GFP_KERNEL, TS_AUTOLOAD);
    if (IS_ERR(conf)) {
        pr_err("textsearch_prepare failed: %ld\n",
               PTR_ERR(conf));
        return;
    }

    /* get_next_block을 간단한 버퍼 반복자로 설정 */
    textsearch_find_continuous(conf, &state,
                               text, strlen(text));

    pos = textsearch_find(conf, &state);
    pr_info("First match at: %u\n", pos);  /* 0 */

    pos = textsearch_next(conf, &state);
    pr_info("Next match at: %u\n", pos);   /* 12 */

    pos = textsearch_next(conf, &state);
    pr_info("No more: %u\n", pos);        /* UINT_MAX */

    textsearch_destroy(conf);
}

자주 발생하는 문제와 해결

증상원인해결
iptables -m string 실패: "No such file or directory"ts_kmp 또는 xt_string 모듈 미로드modprobe xt_string (자동으로 ts_kmp도 로드)
규칙은 있지만 패킷이 매칭되지 않음from/to 범위가 페이로드를 벗어남--from/--to 값 조정, tcpdump로 실제 오프셋 확인
대소문자 매칭 실패--icase 누락-m string --string "pattern" --algo kmp --icase
성능 저하 (높은 softirq CPU)string 규칙이 너무 많거나 검색 범위가 넓음규칙 통합, --from/--to 최적화, 포트 필터 우선 적용
conntrack 헬퍼가 동작하지 않음nf_conntrack_helper=0 (자동 할당 비활성)nftables에서 명시적 헬퍼 할당 설정
알고리즘 변경이 적용되지 않음기존 규칙은 이미 prepare된 ts_config 사용규칙 삭제 후 새 알고리즘으로 재추가

textsearch 관련 커널 로그 확인

# textsearch 관련 커널 메시지 확인
dmesg | grep -i "textsearch\|ts_kmp\|ts_bm\|xt_string"

# netfilter LOG 타겟으로 매칭 상세 정보 수집
iptables -A INPUT -p tcp --dport 80 \
  -m string --string "/admin" --algo kmp \
  -j LOG --log-prefix "[TS_MATCH] " --log-level 4

# LOG 출력 확인
journalctl -k | grep "TS_MATCH"

# NFLOG로 유저 공간에서 패킷 상세 분석
iptables -A INPUT -p tcp --dport 80 \
  -m string --string "/admin" --algo kmp \
  -j NFLOG --nflog-group 1

# ulogd 또는 tcpdump로 NFLOG 패킷 수집
tcpdump -i nflog:1 -w /tmp/matched_packets.pcap
프로덕션 디버깅 팁: 운영 환경에서는 LOG 타겟 대신 -j NFLOG를 사용하세요. LOG는 printk를 통해 커널 링 버퍼(Ring Buffer)에 기록하여 높은 트래픽에서 성능 영향이 큽니다. NFLOG는 netlink를 통해 사용자 공간으로 전달하므로 커널 부하가 적습니다.

KMP 알고리즘 심층 분석

KMP(Knuth-Morris-Pratt) 알고리즘의 정확성과 시간 복잡도를 수학적으로 증명하고, 실패 함수의 오토마톤 관점 해석, 잠재 함수(potential function) 기반의 상각 분석, 그리고 최악 사례 패턴에서도 O(n+m)을 보장하는 원리를 심층 분석합니다.

실패 함수의 수학적 정의와 정확성 증명

패턴 P[0..m-1]에 대해 실패 함수 F[q]는 다음과 같이 정의됩니다:

F[q] = max { k : k < q ∧ P[0..k-1] = P[q-k+1..q] }

즉, P[0..q]의 진접두사(proper prefix)이면서 동시에 접미사인
최대 길이 문자열의 길이입니다.

예시: 패턴 "ABABAC" (m=6)
  q=0: F[0] = 0  (단일 문자, 진접두사 없음)
  q=1: F[1] = 0  ("AB" → 접두사=접미사인 것 없음)
  q=2: F[2] = 1  ("ABA" → "A" = "A")
  q=3: F[3] = 2  ("ABAB" → "AB" = "AB")
  q=4: F[4] = 3  ("ABABA" → "ABA" = "ABA")
  q=5: F[5] = 0  ("ABABAC" → 없음)
KMP 실패 함수 F[q] — 패턴 "ABABAC" 위치 q 문자 F[q] 0 A 0 1 B 0 2 A 1 3 B 2 4 A 3 5 C 0 접두사 = 접미사 오버랩 q=2: F[2]=1 "ABA" A B A "A" = "A" q=3: F[3]=2 "ABAB" A B A B "AB" = "AB" q=4: F[4]=3 "ABABA" A B A B A "ABA" = "ABA" q=5: F[5]=0 "ABABAC" A B A B A C C가 접두사 "A"와 불일치 → 오버랩 없음 실패 링크 체인 (불일치 시 이동) 0 1 2 3 4 5 F[2]=1 F[3]=2 F[4]=3 F[5]=0 (C가 모든 접두사와 불일치) F=0 F=0
실패 함수 체인: F[q]는 자기 자신에 대해 재귀적 구조를 가집니다. q → F[q] → F[F[q]] → ... → 0 체인은 패턴의 모든 유효한 접두사-접미사 매칭 길이를 감소 순서로 나열합니다. 이 체인이 KMP의 불일치 시 "점프"를 결정합니다.

잠재 함수 기반 상각 분석 (O(n+m) 증명)

KMP의 검색 단계에서 텍스트 위치 i와 패턴 위치 q가 동시에 사용됩니다. 잠재 함수 Φ = q로 정의하면:

잠재 함수: Φ = q (현재 패턴에서 매칭된 위치)

Case 1: P[q] == T[i] (일치)
  - 실제 비용: 1 (비교 1회)
  - Φ 변화: +1 (q가 1 증가)
  - 상각 비용: 1 + 1 = 2

Case 2: P[q] != T[i], q > 0 (불일치, 실패 링크 따라감)
  - 실제 비용: 0 (비교 없이 q만 조정)
  - Φ 변화: -(q - F[q-1]) ≤ -1 (q가 감소)
  - 상각 비용: 0 + (-(q - F[q-1])) ≤ -1

Case 3: P[q] != T[i], q == 0 (불일치, 패턴 시작)
  - 실제 비용: 1 (비교 1회)
  - Φ 변화: 0
  - 상각 비용: 1

총 비용:
  - Φ는 0에서 시작하고, 0 이상을 유지하며, 최대 n만큼 증가 가능
  - Case 2의 Φ 감소 총합 ≤ Case 1의 Φ 증가 총합 ≤ n
  - 따라서 Case 2 실행 횟수 ≤ n
  - Case 1 + Case 3 실행 횟수 = n (매 실행마다 i 증가)
  - 총 비교 횟수 ≤ 2n → O(n)
KMP 잠재 함수 Φ = q 변화 추적 Text: "ABABABABAC", Pattern: "ABABAC" (m=6) Φ = q (패턴 위치) 0 1 2 3 4 5 6 텍스트 위치 i A 0 B 1 A 2 B 3 A 4 B 5 A 6 B 7 A 8 C 9 불일치! Φ: 5→3 매칭! 상각 분석 핵심 상승 (일치, Case 1) 하강 (실패 링크, Case 2) 총 상승 ≤ n → 총 하강 ≤ n → 총 비교 ≤ 2n = O(n)
KMP 변형실패 함수최악 비교 횟수특징
원본 MP (Morris-Pratt)F[q] = max proper prefix = suffix length2n - 1실패 시 항상 F[q]로 이동
KMP (Knuth 개선)F'[q] = F[q] if P[F[q]] != P[q], else F'[F[q]]2n - 1연속 불일치 건너뛰기
리눅스 커널 KMP표준 F[q], icase 옵션 지원2n대소문자 무시 변환 추가
Mismatch-counting KMPF[q] + 불일치 카운터2nk-mismatch 근사 매칭
KMP Automaton: Pattern "ABABAC" — DFA-like State Transitions S0 F[0]=0 S1 F[1]=0 S2 F[2]=1 S3 F[3]=2 S4 F[4]=3 S5 F[5]=0 S6 MATCH A B A B A C !=B → S0 !=A → S1 !=B → S2 !=A → S3 !=C → S0 (F[5]=0, 접두사-접미사 없음) 매칭 전이 실패 링크 (F[q]로 전이) 수락 상태

icase 처리와 커널 최적화

/* lib/ts_kmp.c — 대소문자 무시 KMP 검색 */
static unsigned int kmp_find(struct ts_config *conf,
                             struct ts_state *state)
{
    struct ts_kmp *kmp = ts_config_priv(conf);
    unsigned int q = 0, text_len, consumed = state->offset;
    const u8 *text;
    const int icase = conf->flags & TS_IGNORECASE;

    while ((text_len = conf->get_next_block(consumed, &text,
                                            conf, state)) > 0) {
        for (unsigned int i = 0; i < text_len; i++) {
            u8 c = icase ? toupper(text[i]) : text[i];

            while (q > 0 && kmp->pattern[q] != c)
                q = kmp->prefix_tbl[q - 1];

            if (kmp->pattern[q] == c)
                q++;

            if (q == kmp->pattern_len) {
                state->offset = consumed + i + 1;
                return state->offset - kmp->pattern_len;
            }
        }
        consumed += text_len;
    }
    return UINT_MAX;
}
최악 사례 패턴: KMP에서 실패 링크를 가장 많이 타는 패턴은 "AAAA...AB" 형태입니다. 텍스트 "AAAA...A"에서 매 위치마다 끝 'B'에서 불일치가 발생하지만, F 체인을 따라가는 총 횟수가 텍스트 길이를 초과할 수 없으므로 여전히 O(n+m)입니다. 이는 잠재 함수 Φ=q의 증가 총량이 n을 넘지 못하기 때문입니다.

Boyer-Moore 변형과 최적화

Boyer-Moore 알고리즘의 핵심 강점은 텍스트를 건너뛸 수 있다는 점이지만, 기본 구현에는 O(nm) 최악 사례가 존재합니다. Galil Rule, Horspool 단순화, Turbo-BM 등 다양한 변형이 이를 해결하며, 리눅스 커널이 왜 특정 변형을 선택했는지 분석합니다.

Galil Rule: O(nm) 최악 사례 방지

Galil Rule은 매칭이 발견된 후 패턴의 주기(period)만큼만 비교하면 되도록 하여, 이미 비교한 접미사 부분을 다시 비교하지 않는 최적화입니다.

Galil Rule 원리:
  패턴 P의 주기(period) = m - F[m-1] (F는 KMP 실패 함수)

  매칭 성공 후:
    다음 정렬 위치에서 처음 period 문자만 비교하면 됨
    나머지 (m - period) 문자는 이전 매칭에서 이미 확인됨

  예: 패턴 "ABAB" (period = 2)
    Text:  ... A B A B A B A B ...
    1st:       A B A B  ← 매칭!
    2nd:           A B [A B]  ← "AB"만 새로 비교, "AB"는 보장됨

  → 이렇게 하면 최악 O(n) 비교로 모든 매칭 위치를 찾을 수 있음
Galil Rule: 주기 기반 재비교 생략 패턴: "ABAB" (m=4), F[3]=2, period = m − F[m−1] = 4 − 2 = 2 Text: A B A B A B A B A B 1차 정렬: 전체 4문자 비교 A B A B ✓ 매칭! (4회 비교) period=2 이동 2차 정렬: Galil Rule 적용 A B A B ✓ 매칭! (2회 비교만) 보장됨 (비교 생략) period=2 이동 3차 정렬: 동일 적용 A B A B ✓ 매칭! (2회 비교만) 범례: 실제 비교한 문자 이전 매칭에서 보장된 문자 (비교 생략) Galil Rule: 매칭 후 period 문자만 비교 → 전체 매칭 찾기 O(n) 보장 비교 횟수 비교 Galil Rule 없이: 4 + 4 + 4 = 12회 Galil Rule 적용: 4 + 2 + 2 = 8회

Horspool 단순화와 커널 적용

Boyer-Moore-Horspool은 good suffix 테이블을 제거하고 bad character shift만 사용합니다. 구현이 단순하고 실전 성능이 우수합니다.

Boyer-Moore-Horspool: Bad Character Shift Only Text: T H I S _ I S _ A _ P A T T E R N _ H E R E Step 1: P A T T E R N 0 1 2 3 4 5 6 compare rightmost: text[6]='S' vs P[6]='N' MISMATCH bad_shift['S'] = 7 (S not in "PATTERN") shift right by 7 Step 2: T H I S _ I S _ A _ P A T T E R N _ H E R E P A T T E R N text[13]='T' vs P[6]='N' MISMATCH bad_shift['T'] = 3 (T at positions 2,3; last=3, shift=7-1-3=3) shift right by 3 Step 3: T H I S _ I S _ A _ P A T T E R N _ H E R E P A T T E R N RIGHT-TO-LEFT 전체 비교: N=N, R=R, E=E, T=T, T=T, A=A, P=P MATCH at position 10! Horspool: 텍스트 22바이트를 단 3단계(10+7=17바이트 건너뜀)로 검색 — bad_shift만으로 효율적
알고리즘시프트 테이블최선최악공간구현 복잡도
Boyer-Moore (Original)bad_char + good_suffixO(n/m)O(nm)O(m + Σ)높음
BM + Galil Rulebad_char + good_suffix + periodO(n/m)O(n)O(m + Σ)매우 높음
Boyer-Moore-Horspoolbad_char onlyO(n/m)O(nm)O(Σ)낮음
Turbo-BMbad_char + good_suffix + turbo_shiftO(n/m)O(n)O(m + Σ)높음
리눅스 커널 (ts_bm)bad_char + good_suffixO(n/m)O(nm)O(m + 256)중간
커널이 Galil Rule을 사용하지 않는 이유: Galil Rule은 "모든 매칭 위치 찾기"에서 O(n)을 보장하지만, 커널의 xt_string은 첫 매칭만 필요합니다. 또한 Galil Rule은 구현 복잡도가 높고, 실전 네트워크 패턴(짧은 패턴, 큰 알파벳)에서 O(nm) 최악 사례가 거의 발생하지 않습니다. 보안이 중요한 경우 KMP를 사용하면 O(n+m)이 보장됩니다.

Turbo-BM 개념

Turbo-BM: 이전 매칭 시도에서 얼마나 매칭됐는지를 기억

  turbo_shift = u - v  (u: 이전 매칭 길이, v: 현재 good_suffix)
  최종 시프트 = max(good_suffix[j], bad_char[text[i]], turbo_shift)

  이렇게 하면 이미 비교한 문자를 건너뛰어 O(n) 보장
  하지만 추가 변수(u) 관리 필요 → 분기 예측 실패 증가
Turbo-BM: 세 가지 시프트 후보 비교 Step k: 오른쪽→왼쪽 비교 Text: D X C B A B Pattern: A B A B A B ← 불일치 (j=2, text[i]='C') ← u=3 문자 매칭됨 u = 3 (이전 매칭 길이) Step k+1: 시프트 후보 3가지 ① bad_char[T[i]] bad_char['C'] = shift 3 ② good_suffix[j] good_suffix[2] = shift 2 ③ turbo_shift = u − v 3 − 1 = shift 2 (이미 비교한 영역 활용) max() max(3, 2, 2) 최종 시프트 = 3 turbo_shift가 이미 매칭된 u 문자를 재비교하지 않게 하여 O(n) 보장 Turbo-BM = BM + 이전 매칭 길이 기억 → 최악 O(n) (원본 BM은 O(nm))

FSM 내부 구현 심층

FSM(유한 상태 기계) 검색 알고리즘 ts_fsm은 단순 문자열이 아닌 토큰 패턴을 매칭합니다. 각 토큰은 문자 클래스(digit, alpha, space 등) 또는 와일드카드(TS_FSM_ANY)를 나타내며, greedy/non-greedy 매칭과 백트래킹 동작을 지원합니다.

토큰 타입과 ctype 매핑

토큰 타입enum 값ctype 함수매칭 문자용도 예시
TS_FSM_SPECIFIC0직접 비교지정된 단일 문자구분자, 프로토콜 마커
TS_FSM_WILDCARD1isalnum()[A-Za-z0-9]일반 텍스트 매칭
TS_FSM_DIGIT2isdigit()[0-9]포트 번호, IP 주소
TS_FSM_ALPHA3isalpha()[A-Za-z]프로토콜 키워드
TS_FSM_XDIGIT4isxdigit()[0-9A-Fa-f]16진수 데이터
TS_FSM_UPPER5isupper()[A-Z]HTTP 메서드
TS_FSM_SPACE6isspace()공백, 탭, 개행구분자
TS_FSM_ANY특수항상 참모든 바이트와일드카드 (. 유사)

FSM 매칭 핵심 루프

/* lib/ts_fsm.c — fsm_find() 핵심 매칭 로직 (simplified) */
static unsigned int fsm_find(struct ts_config *conf,
                             struct ts_state *state)
{
    struct ts_fsm *fsm = ts_config_priv(conf);
    unsigned int consumed = state->offset;
    const u8 *text;
    unsigned int text_len;

    while ((text_len = conf->get_next_block(consumed, &text,
                                            conf, state)) > 0) {
        for (unsigned int i = 0; i < text_len; i++) {
            unsigned int match_start = consumed + i;
            int result = fsm_match_tokens(fsm, text + i,
                                          text_len - i, &i);
            if (result == FSM_MATCH) {
                state->offset = match_start;
                return match_start;
            }
            /* FSM_NOMATCH: try next start position */
        }
        consumed += text_len;
    }
    return UINT_MAX;
}

/* 토큰 하나를 매칭하는 내부 함수 */
static int match_token(struct ts_fsm_token *tok, u8 c)
{
    switch (tok->type) {
    case TS_FSM_SPECIFIC:
        return (c == tok->value);
    case TS_FSM_DIGIT:
        return isdigit(c);
    case TS_FSM_ALPHA:
        return isalpha(c);
    case TS_FSM_XDIGIT:
        return isxdigit(c);
    case TS_FSM_SPACE:
        return isspace(c);
    case TS_FSM_ANY:
        return 1;  /* 항상 매칭 */
    }
    return 0;
}
FSM Matching: Greedy TS_FSM_ANY Backtracking Pattern: [DIGIT] [DIGIT] [ANY*] [DIGIT] — "두 자리 숫자 + 임의 문자들 + 숫자" Text: 1 2 3 A 4 5 B 7 T0 DIGIT T1 DIGIT T2 ANY* T3 DIGIT OK '1' '2' greedy: 3,A,4,5,B try backtrack: ANY* 하나 줄임 Try 1: ANY*="3A45B" → 남은것 없음 → FAIL Try 2: ANY*="3A45" → T3='B' → NOT DIGIT → FAIL Try 3: ANY*="3A4" → T3='5' → DIGIT → MATCH! '5' Greedy ANY*: 최대한 소비 후 실패하면 하나씩 반환 백트래킹 깊이 = ANY*가 소비한 문자 수 최악 O(n*k): n=텍스트 길이, k=ANY* 토큰 수
FSM 백트래킹 DoS 위험: FSM의 greedy TS_FSM_ANY 토큰은 백트래킹을 유발합니다. 패턴에 여러 개의 TS_FSM_ANY가 있으면 지수적 백트래킹이 발생할 수 있습니다. 이는 정규표현식의 catastrophic backtracking과 동일한 문제입니다. 커널에서 FSM은 주로 nf_conntrack 헬퍼의 단순 프로토콜 파싱에만 사용되며, 사용자가 임의 패턴을 지정할 수 있는 xt_string에서는 지원되지 않습니다.

skb Fragment 횡단 검색 심층

네트워크 패킷은 sk_buff 내에서 여러 조각(fragment)으로 분산 저장됩니다. 텍스트 검색 프레임워크의 핵심 과제 중 하나는 패턴이 두 fragment의 경계를 걸쳐 있을 때도 정확히 매칭하는 것입니다. 이 섹션에서는 skb_seq_read 기반의 블록 순회와 알고리즘별 경계 처리 방식을 심층 분석합니다.

skb 데이터 구조와 fragment 종류

Fragment 유형위치접근 방식메모리 특성생성 원인
선형 데이터 (head)skb->data ~ skb_tail_pointer()직접 포인터연속 kmalloc 버퍼드라이버 alloc_skb
paged fragmentskb_shinfo(skb)->frags[]kmap_local_page()struct page, 오프셋 + 길이sendfile, splice, GRO
frag_listskb_shinfo(skb)->frag_list연결 리스트(Linked List) 순회각각 독립된 sk_buffIP 재조립, UDP GRO
Pattern "HELLO" Spanning Fragment Boundary 선형 데이터 (head) — 14 bytes G E T _ / p a t h _ H E L 0 1 2 3 4 5 6 7 8 9 10 11 12 frag[0] — 8 bytes L O _ W O R L D 13 14 15 16 17 18 19 20 frag[1] — 10 bytes \\r \\n H E L L O \\r \\n ! 21 22 23 24 25 26 27 28 29 30 H E L L O pos 9~13 (경계 횡단!) get_next_block 호출 순서: Block 0: offset=0, len=13 head: "GET /path HEL" Block 1: offset=13, len=8 frag[0]: "LO WORLD" Block 2: offset=21, len=10 frag[1]: "\\r\\nHELLO\\r\\n!" KMP 경계 횡단 처리: Block 0 끝에서: q=3 (패턴 "HEL"까지 매칭, q는 다음 블록으로 유지됨) Block 1 시작: q=3 상태에서 계속, text[0]='L' vs P[3]='L' → 매칭! q=4 text[1]='O' vs P[4]='O' → 매칭! q=5=m → MATCH at offset 9 BM 경계 횡단 문제: BM은 오른쪽에서 왼쪽으로 비교 → 포인터가 뒤로 이동할 수 있음 Block 1에서 비교 시작: text[1]='O' vs P[4]='O' → OK, text[0]='L' vs P[3]='L' → OK → 이전 Block 0으로 돌아가야 함! skb_seq_read는 이전 블록 재방문 불가 → 별도 버퍼 필요

skb_seq_read와 get_next_block 상호작용

/* net/core/skbuff.c — skb_seq_read (simplified) */
unsigned int skb_seq_read(unsigned int consumed,
                          const u8 **data,
                          struct ts_state *st)
{
    struct sk_buff *skb = st->skb;
    unsigned int head_len = skb_headlen(skb);

    /* Phase 1: 선형 데이터 영역 */
    if (consumed < head_len) {
        *data = skb->data + consumed;
        return min(head_len - consumed, st->block_size);
    }

    /* Phase 2: paged fragments */
    consumed -= head_len;
    for (int i = 0; i < skb_shinfo(skb)->nr_frags; i++) {
        const skb_frag_t *frag = &skb_shinfo(skb)->frags[i];
        unsigned int frag_size = skb_frag_size(frag);

        if (consumed < frag_size) {
            *data = kmap_local_page(skb_frag_page(frag))
                    + skb_frag_off(frag) + consumed;
            return frag_size - consumed;
        }
        consumed -= frag_size;
    }

    /* Phase 3: frag_list (재귀적) */
    /* ... frag_list 순회 생략 ... */

    return 0;  /* 데이터 끝 */
}
GRO/GSO와 검색: GRO(Generic Receive Offload)가 활성화되면 여러 패킷이 하나의 super-packet으로 합쳐져 frag_list로 연결됩니다. 이때 검색 대상 데이터 크기가 64KB 이상이 될 수 있습니다. skb_find_text()는 이 전체를 순회하므로, 큰 GRO 패킷에서 복잡한 패턴을 검색하면 CPU 시간이 증가합니다. --from/--to 범위를 지정하여 검색 영역을 제한하는 것이 중요합니다.

다중 패턴 검색과 Aho-Corasick

현재 리눅스 커널 텍스트 검색 프레임워크는 단일 패턴 매칭만 지원합니다. 그러나 netfilter에서 여러 -m string 규칙을 사용하면 동일 패킷에 대해 다중 패턴을 순차 검색해야 하며, 이는 O(n*k) 오버헤드를 유발합니다. Aho-Corasick 알고리즘은 이 문제를 O(n+m+z)로 해결할 수 있습니다.

현재 다중 패턴 처리의 비효율성

# 3개 패턴으로 HTTP 악성 요청 탐지 — 패킷당 3번 전체 스캔
iptables -A INPUT -p tcp --dport 80 \
  -m string --string "/etc/passwd" --algo kmp -j DROP
iptables -A INPUT -p tcp --dport 80 \
  -m string --string "../../../" --algo kmp -j DROP
iptables -A INPUT -p tcp --dport 80 \
  -m string --string "<script>" --algo kmp -j DROP

# 각 규칙이 독립적으로 패킷을 전체 스캔
# 패킷 1500 bytes × 3 규칙 = 4500 bytes 분량의 비교 연산

Aho-Corasick 알고리즘 개요

Aho-Corasick은 KMP를 다중 패턴으로 확장한 알고리즘입니다. 모든 패턴으로 트라이(trie)를 구축하고, KMP의 실패 함수에 해당하는 실패 링크를 트라이 노드 간에 설정합니다.

Aho-Corasick Trie: Patterns {"he", "she", "his", "hers"} R h h s s e out: {he} e i i h h r r s out: {his} s e out: {she, he} e s out: {hers} s fail: sh→h fail: she→he (output link!) 범례 goto 전이 fail 링크 출력 노드
접근 방식전처리검색 시간패턴 k개, 총 문자 m커널 지원
순차 textsearch (현재)O(m) per patternO(n * k)각 패턴 독립 스캔지원
Aho-CorasickO(m * Σ)O(n + z)트라이 1회 순회, z=매칭 수미지원
nftables set 매칭해시(Hash)/rbtree 기반O(1) per lookup정확 매칭만 (부분 문자열 불가)지원
Wu-ManberO(m)O(n * B/m_min)BM의 다중 패턴 확장미지원
Aho-Corasick 커널 패치(Patch) 역사: 2005년과 2010년에 Aho-Corasick을 ts_ac로 커널에 추가하는 패치가 제안되었으나, 모두 머지되지 않았습니다. 주요 반대 이유는: (1) 트라이 구축에 필요한 메모리가 GFP_KERNEL 컨텍스트에서만 할당 가능하여 유연성 부족, (2) 대부분의 실전 사용에서 패턴이 1~3개로 순차 스캔의 오버헤드가 미미, (3) nftables의 set 기반 매칭이 대안으로 발전. 대규모 패턴 세트가 필요하면 Snort/Suricata 같은 사용자 공간 IDS를 사용하는 것이 권장됩니다.

커스텀 검색 알고리즘 구현

텍스트 검색 프레임워크의 플러그인 구조를 활용하여 새로운 검색 알고리즘을 커널 모듈로 구현할 수 있습니다. 이 섹션에서는 Rabin-Karp 롤링 해시 알고리즘을 예제로 사용하여 전체 구현 과정을 단계별로 안내합니다.

Rabin-Karp 롤링 해시 개념

Rabin-Karp Rolling Hash: Pattern "ABC" (m=3) Text: X A B D A B C E Pattern hash: H("ABC") = 294 H(XAB)=297 297 != 294 → skip H(ABD)=267 rolling: H = (H-X*d^2)*d + D 267 != 294 → skip H(BDA)=264 264 != 294 → skip H(DAB)=263 263 != 294 → skip H(ABC)=294 294 == 294 → verify chars → MATCH! Rolling Hash: H(s[i+1..i+m]) = (H(s[i..i+m-1]) - s[i] * d^(m-1)) * d + s[i+m] d = base (예: 256), mod = prime (예: 1000000007) — O(1) 업데이트, 해시 충돌 시에만 O(m) 문자 비교

완전한 ts_rk 커널 모듈

/* ts_rk.c — Rabin-Karp 텍스트 검색 알고리즘 커널 모듈 */
#include <linux/module.h>
#include <linux/textsearch.h>
#include <linux/slab.h>

#define RK_BASE      256ULL
#define RK_MOD       1000000007ULL

struct ts_rk {
    u8           *pattern;
    unsigned int patlen;
    u64          pat_hash;     /* 패턴 해시 */
    u64          high_pow;     /* RK_BASE^(patlen-1) mod RK_MOD */
};

static u64 rk_hash(const u8 *data, unsigned int len)
{
    u64 h = 0;
    for (unsigned int i = 0; i < len; i++)
        h = (h * RK_BASE + data[i]) % RK_MOD;
    return h;
}

static unsigned int rk_find(struct ts_config *conf,
                            struct ts_state *state)
{
    struct ts_rk *rk = ts_config_priv(conf);
    unsigned int consumed = state->offset;
    const u8 *text;
    unsigned int text_len;
    u64 h;
    unsigned int i;

    /* 단순화: 연속 블록 가정 (실제에는 블록 간 처리 필요) */
    text_len = conf->get_next_block(consumed, &text, conf, state);
    if (text_len < rk->patlen)
        return UINT_MAX;

    /* 초기 윈도우 해시 */
    h = rk_hash(text, rk->patlen);

    for (i = 0; i <= text_len - rk->patlen; i++) {
        if (h == rk->pat_hash) {
            /* 해시 일치 → 실제 문자 비교 (충돌 확인) */
            if (memcmp(text + i, rk->pattern, rk->patlen) == 0) {
                state->offset = consumed + i;
                return state->offset;
            }
        }
        /* 롤링 해시 업데이트 */
        if (i + rk->patlen < text_len) {
            h = (h + RK_MOD - text[i] * rk->high_pow % RK_MOD)
                % RK_MOD;
            h = (h * RK_BASE + text[i + rk->patlen]) % RK_MOD;
        }
    }
    return UINT_MAX;
}

static struct ts_config *rk_init(const void *pattern,
                                 unsigned int len,
                                 gfp_t gfp_mask,
                                 int flags)
{
    struct ts_config *conf;
    struct ts_rk *rk;

    conf = alloc_ts_config(sizeof(*rk) + len, gfp_mask);
    if (IS_ERR(conf))
        return conf;

    conf->flags = flags;
    rk = ts_config_priv(conf);
    rk->patlen  = len;
    rk->pattern = (u8 *)(rk + 1);
    memcpy(rk->pattern, pattern, len);

    /* 패턴 해시 및 high_pow 전처리 */
    rk->pat_hash = rk_hash(rk->pattern, len);
    rk->high_pow = 1;
    for (unsigned int i = 0; i < len - 1; i++)
        rk->high_pow = (rk->high_pow * RK_BASE) % RK_MOD;

    return conf;
}

static void rk_destroy(struct ts_config *conf) { }

static struct ts_ops rk_ops = {
    .name    = "rk",
    .find    = rk_find,
    .init    = rk_init,
    .destroy = rk_destroy,
    .owner   = THIS_MODULE,
};

static int __init ts_rk_init(void)
{
    return textsearch_register(&rk_ops);
}

static void __exit ts_rk_exit(void)
{
    textsearch_unregister(&rk_ops);
}

module_init(ts_rk_init);
module_exit(ts_rk_exit);
MODULE_LICENSE("GPL");
MODULE_ALIAS_TS("rk");
구현 요구 사항설명필수 여부
ts_ops.name알고리즘 고유 이름 (iptables --algo에서 사용)필수
ts_ops.find검색 콜백 (get_next_block으로 데이터 순회)필수
ts_ops.initts_config 초기화, 전처리 테이블 구축필수
ts_ops.destroy추가 할당 리소스 해제선택 (비어있을 수 있음)
ts_ops.get_pattern패턴 포인터/길이 반환 (디버깅용)선택
ts_ops.ownerTHIS_MODULE — 모듈 참조 카운트필수
MODULE_ALIAS_TS("name")자동 로드용 별칭권장
커스텀 알고리즘 테스트: 모듈을 빌드한 후 insmod ts_rk.ko로 로드하고, iptables -A INPUT -m string --string "test" --algo rk -j LOG으로 테스트할 수 있습니다. MODULE_ALIAS_TS("rk")를 설정하면 iptables 규칙 추가 시 자동 로드됩니다.

메모리 레이아웃과 캐시 최적화

텍스트 검색 프레임워크의 각 알고리즘은 ts_config 구조체 뒤에 private 데이터를 연속 할당합니다. 이 레이아웃이 캐시 성능에 미치는 영향을 분석합니다.

alloc_ts_config 메모리 할당

/* lib/textsearch.c — ts_config 할당 */
struct ts_config *alloc_ts_config(size_t payload,
                                  gfp_t gfp_mask)
{
    struct ts_config *conf;

    /* ts_config + payload를 하나의 kmalloc으로 할당 */
    conf = kmalloc(TS_PRIV_ALIGN(sizeof(*conf)) + payload,
                   gfp_mask);
    if (!conf)
        return ERR_PTR(-ENOMEM);

    memset(conf, 0, sizeof(*conf));
    return conf;
}

/* private 데이터 접근 */
static inline void *ts_config_priv(struct ts_config *conf)
{
    return (u8 *)conf + TS_PRIV_ALIGN(sizeof(*conf));
}
Memory Layout: ts_config + Private Data (pattern "HELLO", m=5) Cache Line ts_kmp (패턴 5바이트) ts_config (48 bytes) pad pattern 5 B prefix_tbl 5*4=20 B L1 (64B) L2 (64B) 총: ~103 bytes (2 cache lines) ts_bm (패턴 5바이트) ts_config (48 bytes) pad pattern 5 B bad_shift[256] = 256 * 4 = 1024 bytes (16 cache lines!) good_shift[5] 5*4=20 B 총: ~1,127 bytes (18 cache lines) ts_fsm (5 토큰) ts_config (48 bytes) pad tokens[5] 5 * sizeof(ts_fsm_token) = ~40 B 총: ~118 bytes (2 cache lines) 캐시 성능 분석: KMP: 캐시 친화적 • prefix_tbl: 패턴 길이만큼만 사용 (짧은 패턴 = 1 cache line) • 순방향 스캔: 텍스트 포인터가 항상 앞으로 → prefetcher 효과적 BM: 캐시 비친화적 • bad_shift[256]: 1KB → 16 cache lines, 랜덤 인덱스 접근 (cache miss) • 역방향 비교: 텍스트 포인터가 뒤로 → prefetcher 무효화 FSM: 토큰 배열 크기에 의존
NUMA와 per-CPU 사용: ts_config는 읽기 전용(Read-Only)이므로 여러 CPU에서 동시에 사용할 수 있습니다. 그러나 NUMA 시스템에서 원격 노드의 메모리를 참조하면 레이턴시가 2~3배 증가합니다. netfilter 규칙이 고빈도로 호출되는 경우, 각 NUMA 노드에 ts_config를 복제하여 로컬 메모리에서 접근하는 것이 이상적입니다. 현재 커널은 이를 자동으로 수행하지 않습니다.

DPI 회피 기법과 대응

DPI(Deep Packet Inspection)에서 텍스트 검색을 사용할 때, 공격자는 다양한 회피 기법을 통해 패턴 매칭을 우회할 수 있습니다. IP/TCP 조각화, 인코딩 변환, 대소문자 혼합 등의 기법과 커널에서의 대응 메커니즘을 분석합니다.

회피 기법 분류와 대응

회피 기법카테고리예시커널 대응효과
IP 조각화네트워크 계층패턴을 2개 IP fragment에 분할nf_defrag_ipv4 재조립완전 차단
TCP 세그먼트 분할전송 계층패턴을 여러 TCP 세그먼트로 전송nf_conntrack 스트림 재조립부분 대응
대소문자 혼합인코딩"GET" → "gEt"TS_IGNORECASE / --icase완전 차단
URL 인코딩인코딩"/admin" → "/%61dmin"디코딩 없음 (미대응)회피 가능
NULL 바이트 삽입삽입"scr\x00ipt"바이너리 매칭 (부분 대응)패턴 의존
HTTP 청크 인코딩프로토콜Transfer-Encoding: chunkedconntrack 헬퍼 (제한적)부분 대응
TLS 암호화(Encryption)암호화HTTPS 페이로드불가 (SNI만 평문)회피 가능
IPv6 확장 헤더네트워크Routing/Fragment 헤더 체인nf_defrag_ipv6완전 차단
DPI Evasion Attack Tree & Kernel Countermeasures DPI 회피 공격 1. 조각화 회피 IP Fragment TCP Segment nf_defrag + conntrack 재조립 2. 인코딩 회피 URL %xx iCase UTF-8 --icase (부분), URL 디코딩 없음 3. 프로토콜 회피 Chunked Gzip HTTP/2 커널에서 디코딩 불가 → 미대응 4. 암호화 회피 TLS VPN/IPSec 근본적 불가 커널 방어 메커니즘 상세 nf_defrag_ipv4/ipv6 • PREROUTING 훅에서 재조립 • conntrack 의존성 • 완전한 패킷으로 string 매칭 nf_conntrack 헬퍼 • FTP/SIP/H.323 프로토콜 파싱 • 스트림 상태 추적 • 제한적: HTTP chunked 미지원 xt_string 옵션 • --icase: 대소문자 무시 • --hex-string: 바이너리 매칭 • --from/--to: 검색 범위 제한 커널 DPI 한계: • 평문 프로토콜(HTTP, FTP, SMTP)에서만 효과적 — TLS/QUIC 트래픽에는 무력 • URL/HTML 인코딩 디코딩 없음 — 정규화된 패턴만 매칭 가능 • 상태 기반 분석 불가 — Suricata/Snort 같은 사용자 공간 IDS가 필요한 이유

IP 조각화 회피 방어 코드

# nf_defrag_ipv4 활성화 확인 (conntrack이 자동으로 로드)
lsmod | grep nf_defrag

# conntrack 기반 재조립 보장 — string 매칭 전에 conntrack 활성화
iptables -A INPUT -m conntrack --ctstate ESTABLISHED,RELATED -j ACCEPT
iptables -A INPUT -p tcp --dport 80 \
  -m string --string "/etc/passwd" --algo kmp -j DROP

# TCP MSS 클램핑으로 과도한 분할 방지
iptables -A FORWARD -p tcp --tcp-flags SYN,RST SYN \
  -j TCPMSS --clamp-mss-to-pmtu

# 비정상 조각 차단
iptables -A INPUT -f -j DROP  # 모든 IP fragment 차단 (주의: 정상 트래픽도 차단)
conntrack 메모리 비용: nf_conntrack은 연결 추적 테이블을 유지하며, 기본 nf_conntrack_max는 시스템 메모리에 비례합니다. 각 연결 엔트리는 약 300~600 바이트를 소비합니다. DDoS 공격 시 연결 테이블이 가득 차면 정상 트래픽도 차단될 수 있으므로, /proc/sys/net/nf_conntrack_max 튜닝과 -m conntrack --ctstate INVALID -j DROP 규칙이 필수입니다.

TC ematch 텍스트 분류 심층

텍스트 검색 프레임워크는 netfilter뿐 아니라 TC(Traffic Control) 서브시스템의 em_text ematch에서도 활용됩니다. 이를 통해 패킷 페이로드의 텍스트 내용을 기반으로 QoS 정책을 적용할 수 있습니다.

em_text 아키텍처

em_text는 TC의 확장 매칭(extended match) 프레임워크 위에 구현됩니다. 네트워크 계층(link/network/transport)별 오프셋을 지정하여 검색 범위를 제한할 수 있습니다.

TC ematch Pipeline: Packet → Classifier → Text Match → Action Packet (skb) qdisc (HTB/HFSC) tc filter (cls_basic) ematch tree em_text("pattern") textsearch_find() classid 1:10 police/drop match match class 1:10 from_layer / to_layer 오프셋 개념 L2 Header 14B (Eth) L3 Header 20B (IPv4) L4 Header 20B (TCP) Payload (검색 대상 영역) link(0) network(14) transport(34) payload(54)

TC 텍스트 매칭 설정 예제

# HTB qdisc 설정
tc qdisc add dev eth0 root handle 1: htb default 30
tc class add dev eth0 parent 1: classid 1:10 htb rate 1mbit ceil 2mbit
tc class add dev eth0 parent 1: classid 1:20 htb rate 10mbit ceil 20mbit
tc class add dev eth0 parent 1: classid 1:30 htb rate 5mbit ceil 10mbit

# HTTP 동영상 트래픽을 저대역폭 클래스로 분류
tc filter add dev eth0 parent 1: protocol ip prio 1 \
  basic match 'text(pattern "video/mp4" from transport layer) or
               text(pattern "Content-Type: video" from transport layer)' \
  classid 1:10

# API 트래픽을 고대역폭 클래스로 분류
tc filter add dev eth0 parent 1: protocol ip prio 2 \
  basic match 'text(pattern "/api/" from transport layer)' \
  classid 1:20

# 정적 파일 요청에 rate limit 적용
tc filter add dev eth0 parent 1: protocol ip prio 3 \
  basic match 'text(pattern ".jpg" from transport layer) or
               text(pattern ".png" from transport layer)' \
  action police rate 500kbit burst 100k conform-exceed drop/continue
ematch 유형모듈매칭 대상textsearch 사용용도
textem_text패킷 페이로드 문자열사용L7 콘텐츠 기반 QoS
cmpem_cmp숫자 비교 (헤더 필드)미사용TTL, TOS 필드 비교
nbyteem_nbyte고정 오프셋 바이트 비교미사용특정 위치 바이트 패턴
metaem_meta패킷/시스템 메타데이터미사용인터페이스, 마크 값
canidem_canidCAN bus 프레임 ID미사용차량/산업 네트워크
TC vs iptables 선택 기준: 텍스트 매칭은 두 곳에서 사용할 수 있습니다. iptables -m string은 패킷 필터링(허용/차단)에, tc em_text는 QoS 분류(대역폭(Bandwidth) 제한/우선순위(Priority))에 적합합니다. 동일 패턴을 두 곳에서 중복 검색하지 않도록 설계해야 합니다. tc는 egress에서도 동작하므로 송신 트래픽 제어(Traffic Control)에 특히 유용합니다.

실전 패턴 설계

효과적인 DPI 패턴을 설계하려면 프로토콜 구조를 이해하고, 오탐(false positive)을 최소화하면서 정확한 탐지를 달성해야 합니다. --from/--to 옵션을 활용한 검색 범위 최적화와 프로토콜별 시그니처 설계 기법을 다룹니다.

프로토콜별 검색 범위 최적화

Protocol Offset Map: --from / --to Optimization IP Header 20 bytes (0~19) TCP Header 20~60 bytes (20~79) Payload (Application Data) ~1460 bytes (40+) 0 20 40 80 200 1500 HTTP Request Line from=40 to=120 (GET/POST/PUT...) HTTP Headers from=40 to=500 (Host, User-Agent, Content-Type...) DNS Query from=40 to=120 TLS ClientHello (SNI) from=40 to=300 (Server Name Extension) SMTP Command from=40 to=100 최적화: --from/--to를 좁게 설정하면 검색 대상 데이터가 줄어들어 CPU 사용률이 감소합니다. 예: 1500B 패킷에서 HTTP 메서드만 검색 시 --from 40 --to 80 → 검색량 97% 감소 (1500B → 40B)

프로토콜 시그니처 테이블

프로토콜패턴--algo--from--to용도
HTTP GET"GET "bm4044HTTP GET 요청 식별
HTTP POST"POST "bm4045HTTP POST 요청 식별
HTTP Host"Host: example.com"kmp40500특정 도메인 필터링
TLS SNI"|16 03|" (hex)bm4042TLS ClientHello 탐지
DNS Query"example.com"kmp40120DNS 쿼리 차단
SMTP EHLO"EHLO "bm4060SMTP 세션 탐지
SSH Banner"SSH-2.0"bm4060SSH 연결 탐지
BitTorrent"|13|BitTorrent"bm4080P2P 트래픽 차단

오탐(False Positive) 감소 기법

# Bad: 짧은 패턴 → 오탐 다수
iptables -A INPUT -m string --string "GET" --algo bm -j DROP
# "GET"은 일반 바이너리 데이터에도 빈번하게 출현

# Good: 충분히 긴 패턴 + 범위 제한
iptables -A INPUT -p tcp --dport 80 \
  -m string --string "GET /" --algo bm \
  --from 40 --to 50 -j ACCEPT

# Better: 복합 조건 (포트 + 프로토콜 + 문자열)
iptables -A INPUT -p tcp --dport 80 \
  -m conntrack --ctstate ESTABLISHED \
  -m string --string "/wp-login.php" --algo kmp \
  --from 40 --to 200 \
  -m hashlimit --hashlimit-above 5/min --hashlimit-mode srcip \
    --hashlimit-name wp-login \
  -j DROP

# hex-string: 바이너리 프로토콜 매칭
iptables -A INPUT -p tcp \
  -m string --hex-string "|0d 0a 0d 0a|" --algo bm \
  --from 40 --to 500 \
  -j LOG --log-prefix "HTTP-HDR-END "
# \r\n\r\n — HTTP 헤더 끝 감지
패턴 길이 권장: 오탐률은 패턴 길이에 반비례합니다. 알파벳 크기 Σ=256일 때, 랜덤 데이터에서 길이 m 패턴의 기대 매칭 확률은 약 1/256^m입니다. 4바이트 패턴은 약 1/4.3G로 충분히 낮지만, 2바이트 패턴은 1/65536으로 1500바이트 패킷당 약 2.3% 확률로 오탐합니다. 최소 4바이트 이상의 패턴을 사용하세요.

커널 테스트 모듈 작성

3가지 텍스트 검색 알고리즘(KMP, BM, FSM)의 성능을 정량적으로 비교하는 커널 모듈을 작성합니다. ktime_get_ns()를 사용한 고정밀 벤치마킹과 통계 분석 방법론을 포함합니다.

Benchmark Methodology: textsearch Performance Test 1. Prepare textsearch_prepare() alloc test data 2. Warm 5회 dummy 검색 캐시 프라이밍 3. Measure ktime_get_ns() N=10000 iterations preempt_disable() local_irq_disable() 4. Statistics min/max/avg ns per search 5. Report textsearch_destroy() printk results repeat for each algo (kmp, bm) 벤치마크 정확도 확보 요소: 1. preempt_disable() + local_irq_disable() → 인터럽트/선점에 의한 측정 왜곡 방지 2. 캐시 워밍 5회 → 콜드 캐시 효과 제거 (hot path 성능 측정) 3. 10000회 반복 → 개별 편차 상쇄, 평균 안정화 4. min/max 함께 보고 → 이상치(outlier) 감지 5. 다양한 시나리오: 짧은/긴 패턴, 매칭/비매칭, 연속/분산 메모리

완전한 벤치마크 모듈

/* ts_bench.c — textsearch 벤치마크 커널 모듈 */
#include <linux/module.h>
#include <linux/textsearch.h>
#include <linux/slab.h>
#include <linux/ktime.h>
#include <linux/random.h>

#define BENCH_ITERATIONS  10000
#define WARMUP_RUNS       5
#define TEXT_SIZE         4096

struct bench_result {
    u64 total_ns;
    u64 min_ns;
    u64 max_ns;
    int match_count;
};

static void run_bench(const char *algo,
                      const char *pattern, int patlen,
                      const u8 *text, int textlen,
                      struct bench_result *res)
{
    struct ts_config *conf;
    struct ts_state state;
    u64 start, end, elapsed;
    int i, pos;

    conf = textsearch_prepare(algo, pattern, patlen,
                              GFP_KERNEL, TS_AUTOLOAD);
    if (IS_ERR(conf)) {
        pr_err("ts_bench: prepare failed for %s\n", algo);
        return;
    }

    /* 캐시 워밍 */
    for (i = 0; i < WARMUP_RUNS; i++) {
        memset(&state, 0, sizeof(state));
        textsearch_find_continuous(conf, &state, text, textlen);
    }

    /* 본 측정 */
    res->total_ns = 0;
    res->min_ns = U64_MAX;
    res->max_ns = 0;
    res->match_count = 0;

    preempt_disable();
    local_irq_disable();

    for (i = 0; i < BENCH_ITERATIONS; i++) {
        memset(&state, 0, sizeof(state));
        start = ktime_get_ns();
        pos = textsearch_find_continuous(conf, &state,
                                         text, textlen);
        end = ktime_get_ns();

        elapsed = end - start;
        res->total_ns += elapsed;
        if (elapsed < res->min_ns) res->min_ns = elapsed;
        if (elapsed > res->max_ns) res->max_ns = elapsed;
        if (pos != UINT_MAX) res->match_count++;
    }

    local_irq_enable();
    preempt_enable();

    pr_info("ts_bench [%s] pattern=%d text=%d: "
            "avg=%llu ns, min=%llu ns, max=%llu ns, "
            "matches=%d/%d\n",
            algo, patlen, textlen,
            res->total_ns / BENCH_ITERATIONS,
            res->min_ns, res->max_ns,
            res->match_count, BENCH_ITERATIONS);

    textsearch_destroy(conf);
}

static int __init ts_bench_init(void)
{
    u8 *text;
    struct bench_result res;
    const char *algos[] = { "kmp", "bm" };
    int a;

    /* 테스트 텍스트 생성 (끝 부분에 패턴 삽입) */
    text = kmalloc(TEXT_SIZE, GFP_KERNEL);
    if (!text) return -ENOMEM;
    get_random_bytes(text, TEXT_SIZE);
    memcpy(text + TEXT_SIZE - 16,
           "HELLO WORLD TEST", 16);

    pr_info("ts_bench: === Starting benchmarks ===\n");

    /* 시나리오 1: 짧은 패턴 (4바이트) */
    for (a = 0; a < 2; a++)
        run_bench(algos[a], "TEST", 4,
                  text, TEXT_SIZE, &res);

    /* 시나리오 2: 긴 패턴 (16바이트) */
    for (a = 0; a < 2; a++)
        run_bench(algos[a], "HELLO WORLD TEST", 16,
                  text, TEXT_SIZE, &res);

    /* 시나리오 3: 비매칭 패턴 */
    for (a = 0; a < 2; a++)
        run_bench(algos[a], "ZZZZZZZZ", 8,
                  text, TEXT_SIZE, &res);

    pr_info("ts_bench: === Benchmarks complete ===\n");

    kfree(text);
    return -EAGAIN;  /* 모듈 로드 실패시켜 자동 언로드 */
}

static void __exit ts_bench_exit(void) { }

module_init(ts_bench_init);
module_exit(ts_bench_exit);
MODULE_LICENSE("GPL");

예상 벤치마크 결과

시나리오패턴 길이텍스트 크기KMP avg (ns)BM avg (ns)비고
짧은 패턴 (매칭)4 B4 KB~2,500~2,800BM의 건너뛰기 효과 미미
긴 패턴 (매칭)16 B4 KB~2,400~1,200BM 건너뛰기 효과 발현
비매칭8 B4 KB~2,800~1,500BM이 더 많이 건너뜀
짧은 패턴, 큰 텍스트4 B64 KB~38,000~42,000선형 스케일링
긴 패턴, 큰 텍스트16 B64 KB~36,000~15,000BM 우위 극대화
반복 패턴 (최악)"AAAB" (4 B)"AAA...A" (4 KB)~3,000~12,000BM 최악 사례
벤치마크 주의사항: local_irq_disable()으로 인터럽트를 비활성화하면 시스템이 일시적으로 응답 불가가 됩니다. 10000회 반복은 수 밀리초 내에 완료되므로 안전하지만, 반복 횟수를 과도하게 늘리면 NMI 워치독이 트리거될 수 있습니다. -EAGAIN 반환으로 모듈이 자동 언로드되므로 수동 rmmod가 필요 없습니다.

사용자 공간 IDS/IPS와 비교

커널 textsearch 기반 패턴 매칭과 Snort, Suricata, nDPI 같은 사용자 공간 IDS/IPS를 아키텍처, 성능, 기능, 안전성 측면에서 비교합니다. 각 접근 방식의 장단점과 적절한 사용 시나리오를 분석합니다.

아키텍처 비교

Kernel textsearch vs Userspace IDS: Architecture Comparison 커널 경로 (xt_string) 사용자 공간 경로 (Suricata) NIC (RX Ring) Network Driver Netfilter Hook xt_string textsearch_find() ACCEPT/DROP ~1-5 us total 장점: zero-copy, no context switch 저지연, 단순 규칙에 효과적 단점: crash→system down 제한된 패턴 언어 NIC (RX Ring) Network Driver NFQUEUE / AF_PACKET syscall boundary (copy_to_user) Suricata Engine Aho-Corasick PCRE regex Stateful inspection Rule DB 30,000+ rules Alert/Block ~50-500 us total 장점: 풍부한 규칙, stateful crash→재시작, 안전 단점: 메모리 복사 오버헤드 context switch 비용

기능 비교 테이블

기능커널 xt_stringSnort 3SuricatanDPI
패턴 매칭단일 문자열다중 + PCRE다중 + PCRE프로토콜 시그니처
알고리즘KMP, BMAho-Corasick + HyperscanAho-Corasick + MPMDFA + 휴리스틱
상태 기반 분석없음 (패킷 단위)플로우 기반플로우 기반플로우 기반
프로토콜 파싱없음HTTP/TLS/DNS 등HTTP/TLS/DNS 등280+ 프로토콜
규칙 수~10-10030,000+30,000+~300 프로토콜
레이턴시1-5 us50-500 us50-500 us20-100 us
처리량 (10G)선형 스케일~3-5 Gbps~5-10 Gbps~10+ Gbps
TLS 검사SNI만 (평문)SSL/TLS 복호화(Decryption)SSL/TLS 복호화JA3/JA4 핑거프린트
크래시 영향커널 패닉(Kernel Panic)프로세스 재시작(Reboot)프로세스 재시작라이브러리 호출자 재시작
메모리 사용~100 B/규칙~1-4 GB~1-4 GB~50-200 MB
라이선스GPL (커널)GPL v2GPL v2LGPL v3

XDP/eBPF: 커널-사용자 공간 사이의 대안

/* XDP 기반 텍스트 매칭 개념 코드 (eBPF) */
SEC("xdp")
int xdp_text_match(struct xdp_md *ctx)
{
    void *data = (void *)(long)ctx->data;
    void *data_end = (void *)(long)ctx->data_end;
    struct ethhdr *eth = data;
    struct iphdr *ip;
    struct tcphdr *tcp;
    unsigned char *payload;
    int payload_len;

    /* 헤더 경계 검증 (verifier 통과 필수) */
    if (data + sizeof(*eth) > data_end)
        return XDP_PASS;

    ip = data + sizeof(*eth);
    if ((void *)ip + sizeof(*ip) > data_end)
        return XDP_PASS;

    tcp = (void *)ip + ip->ihl * 4;
    if ((void *)tcp + sizeof(*tcp) > data_end)
        return XDP_PASS;

    payload = (void *)tcp + tcp->doff * 4;
    payload_len = data_end - (void *)payload;

    /* 간단한 패턴 매칭 (verifier 친화적 루프) */
    if (payload_len >= 4 && payload + 4 <= data_end) {
        if (payload[0] == 'G' && payload[1] == 'E' &&
            payload[2] == 'T' && payload[3] == ' ')
            return XDP_DROP;  /* HTTP GET 차단 */
    }

    return XDP_PASS;
}
적절한 도구 선택 가이드:
  • 커널 xt_string: 규칙 10개 이하, 단순 문자열 매칭, 최저 레이턴시 필요 (방화벽 차단)
  • XDP/eBPF: 초고속 패킷 필터링, 드라이버 수준 처리, 규칙이 단순하지만 성능이 중요한 경우
  • Suricata/Snort: 수천 개 규칙, 상태 기반 분석, TLS 검사, 규정 준수(compliance) 필요
  • nDPI: 프로토콜/애플리케이션 식별, QoS/트래픽 분류 목적
성능 계층: 패킷당 처리 레이턴시 기준으로 XDP (~100ns) < xt_string (~1-5us) < nDPI (~20-100us) < Suricata (~50-500us) 순서입니다. 하지만 기능 풍부도는 정반대입니다. 실전에서는 XDP로 명확한 악성 트래픽을 1차 필터링하고, 통과한 트래픽을 Suricata로 정밀 분석하는 2단계 구조가 최적입니다.

텍스트 검색 프레임워크와 관련된 주제를 더 깊이 이해하려면 다음 문서를 참고하세요.

참고 자료