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 헬퍼에서의 실전 활용, 알고리즘 선택 기준과 성능 벤치마크까지 포괄합니다.
핵심 요약
- 플러그인 아키텍처 —
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 공격에 대한 안전성을 확보합니다.
단계별 이해
- 패턴 준비 —
textsearch_prepare()로 알고리즘을 선택하고 패턴 문자열을 전처리합니다. KMP는 실패 함수 테이블을, Boyer-Moore는 시프트 테이블을 미리 구축합니다. - 검색 실행 —
textsearch_find()에 데이터 소스(skb 또는 일반 버퍼(Buffer))를 넘기면, 등록된 알고리즘이 패턴 위치를 찾아 반환합니다. - 연속 검색 —
textsearch_next()로 동일 데이터에서 다음 매칭 위치를 계속 찾을 수 있습니다.ts_state가 검색 진행 상태를 유지합니다. - 정리 —
textsearch_destroy()로 전처리 테이블과 설정 메모리를 해제합니다.
텍스트 검색 프레임워크 개요
리눅스 커널의 텍스트 검색 프레임워크는 lib/textsearch.c에 구현된 범용 패턴 매칭 인프라입니다. 2004년 Thomas Graf가 netfilter의 문자열 매칭 요구를 일반화하여 도입했으며, 핵심 설계 목표는 다음 세 가지입니다.
- 알고리즘 독립성 — 검색 알고리즘을 플러그인으로 교체할 수 있어, 사용 패턴에 맞는 최적 알고리즘을 선택할 수 있습니다.
- 데이터 소스 추상화 — 연속 메모리 버퍼뿐 아니라 skb의 비선형 fragment 체인에서도 동일한 API로 검색할 수 있습니다.
- 동시성 안전 — 설정(config)과 상태(state)를 분리하여 같은 패턴 설정을 여러 컨텍스트에서 동시에 사용할 수 있습니다.
- 프레임워크 코어:
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
프레임워크는 3계층 구조로 동작합니다. 최상위의 호출자(xt_string, conntrack helper 등)가 textsearch API를 통해 검색을 요청하면, API 계층은 ts_config에 바인딩된 ts_ops의 콜백(Callback)을 호출하여 실제 알고리즘을 실행합니다. 이 구조 덕분에 호출자는 어떤 알고리즘이 사용되는지 알 필요가 없습니다.
프레임워크 도입 배경
텍스트 검색 프레임워크 도입 전, netfilter의 문자열 매칭은 xt_string 모듈 내부에 직접 구현된 단순 brute-force 알고리즘을 사용했습니다. 이 방식에는 몇 가지 문제가 있었습니다.
- O(n*m) 최악 성능 — 악의적으로 조작된 패킷이 최악 경우를 유발하여 CPU DoS 공격에 취약
- 알고리즘 교체 불가 — 사용 시나리오에 맞는 최적 알고리즘 선택 불가능
- 코드 중복 — conntrack 헬퍼들이 각각 자체 문자열 검색 로직을 구현
- skb 처리 비효율 — 비선형 skb를 먼저 선형화해야 검색 가능
프레임워크 도입 후, 이 모든 문제가 해결되었습니다. KMP/Boyer-Moore의 O(n+m) 보장으로 DoS 위험이 감소하고, 플러그인 아키텍처로 알고리즘을 자유롭게 선택할 수 있으며, get_next_block 콜백으로 skb의 비선형 데이터를 선형화 없이 검색할 수 있게 되었습니다.
커널 버전별 변화
| 버전 | 변경 사항 |
|---|---|
| 2.6.14 | 텍스트 검색 프레임워크 최초 도입 (Thomas Graf) |
| 2.6.14 | ts_kmp, ts_bm, ts_fsm 3개 알고리즘 동시 추가 |
| 2.6.14 | xt_string이 프레임워크 기반으로 전환 |
| 3.x | TS_IGNORECASE 플래그 추가 (대소문자 무시) |
| 4.x | skb_seq_read 최적화로 fragment 순회 성능 개선 |
| 5.x | nftables payload 매칭과의 통합 강화 |
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_config는 textsearch_prepare()에서 한 번 생성된 후 변경되지 않습니다. 패턴 문자열과 전처리 테이블이 이 구조체의 확장 영역(알고리즘별)에 저장됩니다. 불변이므로 여러 CPU에서 동시에 참조해도 안전합니다.
ts_state — per-call 가변 상태
/* include/linux/textsearch.h */
struct ts_state {
unsigned int offset; /* 현재 검색 오프셋 */
char cb[40]; /* 알고리즘별 콜백 데이터 */
};
ts_state는 각 검색 호출마다 스택에 할당됩니다. offset 필드는 현재까지 검색을 진행한 위치를 추적하고, cb[] 배열은 알고리즘이 자유롭게 사용할 수 있는 40바이트 스크래치 공간입니다. 스택 할당이므로 별도의 메모리 할당/해제가 필요 없습니다.
struct skb_seq_state보다 커야 합니다. 이는 BUILD_BUG_ON(sizeof(struct skb_seq_state) > sizeof(state.cb))로 컴파일 시 검증됩니다. 커스텀 알고리즘 구현 시 40바이트를 초과하는 상태가 필요하면, 별도로 힙 할당해야 합니다.
| 속성 | ts_config | ts_state |
|---|---|---|
| 수명 | prepare ~ destroy | find/next 호출 동안 |
| 할당 | kmalloc (힙) | 스택 또는 호출자 지역 변수 |
| 변경 가능성 | 불변 (read-only) | 매 호출마다 갱신 |
| 공유 가능성 | 여러 CPU 공유 가능 | 호출자 로컬 전용 |
| 내용 | 패턴, ops, 전처리 테이블 | 현재 오프셋(Offset), 알고리즘 임시 데이터 |
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; /* 전역 알고리즘 리스트 */
};
알고리즘 등록/해제
/* 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 검색 구현
/* 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), 짧은 패턴, 스트리밍 입력 |
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);
}
textsearch_next() 호출 시 이전 매칭의 다음 위치부터 검색을 재개합니다. 패턴 "ABA"로 텍스트 "ABABA"를 검색하면 위치 0과 2에서 두 번 매칭됩니다. 이는 실패 함수가 중첩된 접두사를 자동으로 처리하기 때문입니다.
Boyer-Moore 알고리즘 (ts_bm)
Boyer-Moore 알고리즘은 패턴을 오른쪽에서 왼쪽으로 비교하면서, 불일치 시 두 가지 시프트 규칙을 적용하여 큰 폭으로 건너뛰는 알고리즘입니다. 긴 패턴과 큰 알파벳에서 KMP보다 빠른 평균 성능을 보입니다.
나쁜 문자 시프트 (Bad Character Shift)
텍스트에서 불일치가 발생한 문자가 패턴에 없으면 패턴 전체를 건너뛰고, 있으면 해당 문자의 패턴 내 마지막 위치에 맞춰 이동합니다.
좋은 접미사 시프트 (Good Suffix Shift)
이미 매칭된 접미사가 패턴의 다른 위치에도 나타나면, 그 위치에 맞춰 이동합니다.
커널 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 변형 없는 경우 |
| 적합 조건 | 긴 패턴, 연속 메모리 버퍼, 바이너리 데이터 |
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;
}
FSM 알고리즘 (ts_fsm)
FSM(Finite State Machine) 알고리즘은 단순 문자열이 아닌 토큰 기반 패턴을 매칭할 수 있는 유한 상태 기계입니다. 정규식의 간소화된 버전으로, 문자 클래스·반복·와일드카드를 지원합니다.
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 매칭 사용 예시
/* 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);
TS_FSM_HEAD_IGNORE recur 타입을 첫 번째 토큰에 사용하면 텍스트 시작 부분의 임의 데이터를 건너뛸 수 있습니다. 이는 패킷 페이로드에서 특정 프로토콜 명령이 임의 위치에 나타날 수 있는 경우에 유용합니다.
| 특성 | 값 |
|---|---|
| 전처리 시간 | O(t) — 토큰 배열 복사 (t = 토큰 수) |
| 전처리 공간 | O(t) — 토큰 배열 |
| 검색 시간 | O(n*t) 최악 — 각 위치에서 모든 토큰 시도 |
| 적합 조건 | 프로토콜 파싱, 문자 클래스 매칭, 가변 길이 필드 |
알고리즘 선택 가이드
세 알고리즘은 각각 다른 시나리오에서 최적의 성능을 보입니다. 선택 기준은 패턴 길이, 데이터 특성, 재사용 빈도입니다.
| 기준 | KMP | Boyer-Moore | FSM |
|---|---|---|---|
| 패턴 유형 | 고정 문자열 | 고정 문자열 | 토큰 패턴 |
| 최악 시간 | 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 bm | conntrack 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) |
|---|---|---|---|---|
| KMP | prefix_tbl[] 실패 함수 | O(m) | m * sizeof(unsigned int) | 128 bytes |
| Boyer-Moore | bad_shift[256] + good_shift[m] | O(m + 256) | 256 * 4 + m * 4 | 1,152 bytes |
| FSM | 토큰 배열 복사 | O(t) | t * sizeof(ts_fsm_token) | 가변 |
메모리 할당은 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이면 매칭이 없습니다.
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() | 모듈 init | 0 또는 -errno | 알고리즘 등록 |
textsearch_unregister() | 모듈 exit | 0 또는 -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는 이 전체 범위 내의 오프셋
*/
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 구현
/* 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);
}
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 헬퍼 보안 고려
# 자동 헬퍼 할당 비활성화 (수동 설정으로 전환)
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
--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 규칙 최적화 전략
# 나쁜 예: 모든 패킷에 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 트래픽)에서의 벤치마크 경향입니다.
| 시나리오 | 최선 알고리즘 | 이유 |
|---|---|---|
| 짧은 패턴 (1-8B), skb | KMP | 전처리 비용 최소, 역추적 없음 |
| 긴 패턴 (20B+), 연속 메모리 | Boyer-Moore | O(n/m) 평균, 큰 시프트 이점 |
| 문자 클래스 패턴 | FSM | 유일한 선택지 |
| 높은 트래픽, 다수 규칙 | KMP | 예측 가능한 O(n) 성능 |
| 바이너리 데이터 (높은 엔트로피) | Boyer-Moore | 불일치가 빈번하여 시프트 극대화 |
| 반복 패턴 (낮은 엔트로피) | KMP | BM 시프트가 작아져 이점 감소 |
벤치마크 분석 상세
패턴 길이별 성능 특성을 더 자세히 분석합니다.
| 패턴 길이 | KMP (ns/1KB) | BM (ns/1KB) | FSM (ns/1KB) | 비고 |
|---|---|---|---|---|
| 4B ("GET ") | ~800 | ~900 | ~1500 | KMP 우위, BM 시프트 이점 미미 |
| 8B ("HTTP/1.1") | ~750 | ~700 | ~1400 | BM이 KMP와 비슷해지기 시작 |
| 16B | ~700 | ~500 | ~1300 | BM 시프트 이점 발현 |
| 32B | ~680 | ~350 | ~1200 | BM이 KMP의 절반 수준 |
| 64B | ~670 | ~220 | ~1100 | BM 최적 영역 |
| 128B | ~660 | ~150 | ~1050 | BM 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_string | net/netfilter/xt_string.c | iptables 문자열 매칭 | KMP/BM (사용자 선택) |
| nf_conntrack_ftp | net/netfilter/nf_conntrack_ftp.c | FTP PORT/PASV 파싱 | KMP |
| nf_conntrack_sip | net/netfilter/nf_conntrack_sip.c | SIP 헤더 파싱 | KMP |
| nf_conntrack_irc | net/netfilter/nf_conntrack_irc.c | IRC DCC 파싱 | KMP |
| nf_conntrack_amanda | net/netfilter/nf_conntrack_amanda.c | Amanda 백업 프로토콜 | KMP |
| ematch (TC) | net/sched/em_text.c | TC 트래픽 분류 | 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);
}
텍스트 검색 활용 가능 영역
현재 커널에서는 네트워킹 서브시스템이 주요 사용처이지만, 텍스트 검색 프레임워크는 범용적으로 설계되어 있어 다음 영역에서도 활용 가능합니다.
- 보안 모듈 — LSM에서 파일 내용 기반 접근 제어(Access Control)
- 감사(audit) — 시스템 콜(System Call) 인자 문자열 패턴 매칭
- 파일 시스템 — 파일 내용 검색 (커널 공간에서의 grep)
- 디바이스 드라이버 — 펌웨어(Firmware) 응답 파싱, AT 명령 처리
구현 상세
텍스트 검색 프레임워크의 내부 구현에서 주요한 설계 결정과 구현 세부사항을 분석합니다.
모듈 등록과 자동 로드
/* 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_list는 list_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 read | textsearch_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_string은 CONFIG_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
-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" → 없음)
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 변형 | 실패 함수 | 최악 비교 횟수 | 특징 |
|---|---|---|---|
| 원본 MP (Morris-Pratt) | F[q] = max proper prefix = suffix length | 2n - 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 KMP | F[q] + 불일치 카운터 | 2n | k-mismatch 근사 매칭 |
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;
}
"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) 비교로 모든 매칭 위치를 찾을 수 있음
Horspool 단순화와 커널 적용
Boyer-Moore-Horspool은 good suffix 테이블을 제거하고 bad character shift만 사용합니다. 구현이 단순하고 실전 성능이 우수합니다.
| 알고리즘 | 시프트 테이블 | 최선 | 최악 | 공간 | 구현 복잡도 |
|---|---|---|---|---|---|
| Boyer-Moore (Original) | bad_char + good_suffix | O(n/m) | O(nm) | O(m + Σ) | 높음 |
| BM + Galil Rule | bad_char + good_suffix + period | O(n/m) | O(n) | O(m + Σ) | 매우 높음 |
| Boyer-Moore-Horspool | bad_char only | O(n/m) | O(nm) | O(Σ) | 낮음 |
| Turbo-BM | bad_char + good_suffix + turbo_shift | O(n/m) | O(n) | O(m + Σ) | 높음 |
| 리눅스 커널 (ts_bm) | bad_char + good_suffix | O(n/m) | O(nm) | O(m + 256) | 중간 |
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) 관리 필요 → 분기 예측 실패 증가
FSM 내부 구현 심층
FSM(유한 상태 기계) 검색 알고리즘 ts_fsm은 단순 문자열이 아닌 토큰 패턴을 매칭합니다. 각 토큰은 문자 클래스(digit, alpha, space 등) 또는 와일드카드(TS_FSM_ANY)를 나타내며, greedy/non-greedy 매칭과 백트래킹 동작을 지원합니다.
토큰 타입과 ctype 매핑
| 토큰 타입 | enum 값 | ctype 함수 | 매칭 문자 | 용도 예시 |
|---|---|---|---|---|
TS_FSM_SPECIFIC | 0 | 직접 비교 | 지정된 단일 문자 | 구분자, 프로토콜 마커 |
TS_FSM_WILDCARD | 1 | isalnum() | [A-Za-z0-9] | 일반 텍스트 매칭 |
TS_FSM_DIGIT | 2 | isdigit() | [0-9] | 포트 번호, IP 주소 |
TS_FSM_ALPHA | 3 | isalpha() | [A-Za-z] | 프로토콜 키워드 |
TS_FSM_XDIGIT | 4 | isxdigit() | [0-9A-Fa-f] | 16진수 데이터 |
TS_FSM_UPPER | 5 | isupper() | [A-Z] | HTTP 메서드 |
TS_FSM_SPACE | 6 | isspace() | 공백, 탭, 개행 | 구분자 |
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;
}
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 fragment | skb_shinfo(skb)->frags[] | kmap_local_page() | struct page, 오프셋 + 길이 | sendfile, splice, GRO |
| frag_list | skb_shinfo(skb)->frag_list | 연결 리스트(Linked List) 순회 | 각각 독립된 sk_buff | IP 재조립, UDP GRO |
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; /* 데이터 끝 */
}
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의 실패 함수에 해당하는 실패 링크를 트라이 노드 간에 설정합니다.
| 접근 방식 | 전처리 | 검색 시간 | 패턴 k개, 총 문자 m | 커널 지원 |
|---|---|---|---|---|
| 순차 textsearch (현재) | O(m) per pattern | O(n * k) | 각 패턴 독립 스캔 | 지원 |
| Aho-Corasick | O(m * Σ) | O(n + z) | 트라이 1회 순회, z=매칭 수 | 미지원 |
| nftables set 매칭 | 해시(Hash)/rbtree 기반 | O(1) per lookup | 정확 매칭만 (부분 문자열 불가) | 지원 |
| Wu-Manber | O(m) | O(n * B/m_min) | BM의 다중 패턴 확장 | 미지원 |
ts_ac로 커널에 추가하는 패치가 제안되었으나, 모두 머지되지 않았습니다. 주요 반대 이유는: (1) 트라이 구축에 필요한 메모리가 GFP_KERNEL 컨텍스트에서만 할당 가능하여 유연성 부족, (2) 대부분의 실전 사용에서 패턴이 1~3개로 순차 스캔의 오버헤드가 미미, (3) nftables의 set 기반 매칭이 대안으로 발전. 대규모 패턴 세트가 필요하면 Snort/Suricata 같은 사용자 공간 IDS를 사용하는 것이 권장됩니다.
커스텀 검색 알고리즘 구현
텍스트 검색 프레임워크의 플러그인 구조를 활용하여 새로운 검색 알고리즘을 커널 모듈로 구현할 수 있습니다. 이 섹션에서는 Rabin-Karp 롤링 해시 알고리즘을 예제로 사용하여 전체 구현 과정을 단계별로 안내합니다.
Rabin-Karp 롤링 해시 개념
완전한 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.init | ts_config 초기화, 전처리 테이블 구축 | 필수 |
ts_ops.destroy | 추가 할당 리소스 해제 | 선택 (비어있을 수 있음) |
ts_ops.get_pattern | 패턴 포인터/길이 반환 (디버깅용) | 선택 |
ts_ops.owner | THIS_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));
}
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: chunked | conntrack 헬퍼 (제한적) | 부분 대응 |
| TLS 암호화(Encryption) | 암호화 | HTTPS 페이로드 | 불가 (SNI만 평문) | 회피 가능 |
| IPv6 확장 헤더 | 네트워크 | Routing/Fragment 헤더 체인 | nf_defrag_ipv6 | 완전 차단 |
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 차단 (주의: 정상 트래픽도 차단)
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 텍스트 매칭 설정 예제
# 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 사용 | 용도 |
|---|---|---|---|---|
text | em_text | 패킷 페이로드 문자열 | 사용 | L7 콘텐츠 기반 QoS |
cmp | em_cmp | 숫자 비교 (헤더 필드) | 미사용 | TTL, TOS 필드 비교 |
nbyte | em_nbyte | 고정 오프셋 바이트 비교 | 미사용 | 특정 위치 바이트 패턴 |
meta | em_meta | 패킷/시스템 메타데이터 | 미사용 | 인터페이스, 마크 값 |
canid | em_canid | CAN bus 프레임 ID | 미사용 | 차량/산업 네트워크 |
-m string은 패킷 필터링(허용/차단)에, tc em_text는 QoS 분류(대역폭(Bandwidth) 제한/우선순위(Priority))에 적합합니다. 동일 패턴을 두 곳에서 중복 검색하지 않도록 설계해야 합니다. tc는 egress에서도 동작하므로 송신 트래픽 제어(Traffic Control)에 특히 유용합니다.
실전 패턴 설계
효과적인 DPI 패턴을 설계하려면 프로토콜 구조를 이해하고, 오탐(false positive)을 최소화하면서 정확한 탐지를 달성해야 합니다. --from/--to 옵션을 활용한 검색 범위 최적화와 프로토콜별 시그니처 설계 기법을 다룹니다.
프로토콜별 검색 범위 최적화
프로토콜 시그니처 테이블
| 프로토콜 | 패턴 | --algo | --from | --to | 용도 |
|---|---|---|---|---|---|
| HTTP GET | "GET " | bm | 40 | 44 | HTTP GET 요청 식별 |
| HTTP POST | "POST " | bm | 40 | 45 | HTTP POST 요청 식별 |
| HTTP Host | "Host: example.com" | kmp | 40 | 500 | 특정 도메인 필터링 |
| TLS SNI | "|16 03|" (hex) | bm | 40 | 42 | TLS ClientHello 탐지 |
| DNS Query | "example.com" | kmp | 40 | 120 | DNS 쿼리 차단 |
| SMTP EHLO | "EHLO " | bm | 40 | 60 | SMTP 세션 탐지 |
| SSH Banner | "SSH-2.0" | bm | 40 | 60 | SSH 연결 탐지 |
| BitTorrent | "|13|BitTorrent" | bm | 40 | 80 | P2P 트래픽 차단 |
오탐(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 헤더 끝 감지
커널 테스트 모듈 작성
3가지 텍스트 검색 알고리즘(KMP, BM, FSM)의 성능을 정량적으로 비교하는 커널 모듈을 작성합니다. ktime_get_ns()를 사용한 고정밀 벤치마킹과 통계 분석 방법론을 포함합니다.
완전한 벤치마크 모듈
/* 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 B | 4 KB | ~2,500 | ~2,800 | BM의 건너뛰기 효과 미미 |
| 긴 패턴 (매칭) | 16 B | 4 KB | ~2,400 | ~1,200 | BM 건너뛰기 효과 발현 |
| 비매칭 | 8 B | 4 KB | ~2,800 | ~1,500 | BM이 더 많이 건너뜀 |
| 짧은 패턴, 큰 텍스트 | 4 B | 64 KB | ~38,000 | ~42,000 | 선형 스케일링 |
| 긴 패턴, 큰 텍스트 | 16 B | 64 KB | ~36,000 | ~15,000 | BM 우위 극대화 |
| 반복 패턴 (최악) | "AAAB" (4 B) | "AAA...A" (4 KB) | ~3,000 | ~12,000 | BM 최악 사례 |
local_irq_disable()으로 인터럽트를 비활성화하면 시스템이 일시적으로 응답 불가가 됩니다. 10000회 반복은 수 밀리초 내에 완료되므로 안전하지만, 반복 횟수를 과도하게 늘리면 NMI 워치독이 트리거될 수 있습니다. -EAGAIN 반환으로 모듈이 자동 언로드되므로 수동 rmmod가 필요 없습니다.
사용자 공간 IDS/IPS와 비교
커널 textsearch 기반 패턴 매칭과 Snort, Suricata, nDPI 같은 사용자 공간 IDS/IPS를 아키텍처, 성능, 기능, 안전성 측면에서 비교합니다. 각 접근 방식의 장단점과 적절한 사용 시나리오를 분석합니다.
아키텍처 비교
기능 비교 테이블
| 기능 | 커널 xt_string | Snort 3 | Suricata | nDPI |
|---|---|---|---|---|
| 패턴 매칭 | 단일 문자열 | 다중 + PCRE | 다중 + PCRE | 프로토콜 시그니처 |
| 알고리즘 | KMP, BM | Aho-Corasick + Hyperscan | Aho-Corasick + MPM | DFA + 휴리스틱 |
| 상태 기반 분석 | 없음 (패킷 단위) | 플로우 기반 | 플로우 기반 | 플로우 기반 |
| 프로토콜 파싱 | 없음 | HTTP/TLS/DNS 등 | HTTP/TLS/DNS 등 | 280+ 프로토콜 |
| 규칙 수 | ~10-100 | 30,000+ | 30,000+ | ~300 프로토콜 |
| 레이턴시 | 1-5 us | 50-500 us | 50-500 us | 20-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 v2 | GPL v2 | LGPL 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/트래픽 분류 목적
관련 문서
텍스트 검색 프레임워크와 관련된 주제를 더 깊이 이해하려면 다음 문서를 참고하세요.
참고 자료
- include/linux/textsearch.h — 텍스트 검색 프레임워크의 핵심 헤더로, ts_config·ts_state·ts_ops 구조체와 API 선언을 포함합니다
- lib/textsearch.c — 텍스트 검색 프레임워크의 코어 구현으로, textsearch_find·textsearch_prepare·textsearch_register 등 공통 API를 제공합니다
- lib/ts_kmp.c — Knuth-Morris-Pratt 알고리즘 기반 텍스트 검색 모듈(ts_kmp) 구현입니다
- lib/ts_bm.c — Boyer-Moore 알고리즘 기반 텍스트 검색 모듈(ts_bm) 구현으로, 긴 패턴에서 높은 성능을 발휘합니다
- lib/ts_fsm.c — 유한 상태 기계(FSM) 기반 텍스트 검색 모듈(ts_fsm) 구현으로, 와일드카드 패턴을 지원합니다
- net/netfilter/xt_string.c — netfilter 문자열 매칭 모듈로, textsearch 프레임워크를 활용하여 패킷 페이로드에서 문자열을 검색합니다
- net/netfilter/nf_conntrack_ftp.c — FTP 연결 추적 헬퍼로, textsearch를 사용하여 FTP 명령어 패턴을 매칭합니다
- 커널 문서: Text Search Infrastructure — 리눅스 커널 공식 문서의 텍스트 검색 인프라 설명입니다
- LWN.net: Generic text search infrastructure — Thomas Graf가 제안한 커널 텍스트 검색 프레임워크의 설계 배경과 구현 방향을 다룬 LWN 기사입니다
- LWN.net: The kernel string search API — 커널 내 문자열 검색 API의 사용법과 알고리즘 선택 기준을 설명하는 기사입니다
- Wikipedia: Knuth-Morris-Pratt 알고리즘 — ts_kmp 모듈이 구현하는 KMP 알고리즘의 이론적 배경으로, O(n+m) 시간 복잡도를 보장합니다
- Wikipedia: Boyer-Moore 문자열 검색 알고리즘 — ts_bm 모듈이 구현하는 Boyer-Moore 알고리즘으로, 나쁜 문자·좋은 접미사 규칙을 활용하여 긴 패턴에서 최적 성능을 달성합니다
- Wikipedia: 유한 상태 기계(FSM) — ts_fsm 모듈의 기반 이론으로, 정규 표현식과 유사한 패턴 매칭을 가능하게 합니다
- iptables-extensions: string match — iptables의 -m string 확장 모듈 사용법으로, --algo 옵션을 통해 kmp 또는 bm 알고리즘을 선택할 수 있습니다
- net/sched/em_text.c — TC(트래픽 제어)의 ematch 텍스트 분류기로, textsearch 프레임워크를 사용하여 패킷 분류를 수행합니다