패킷 분류 알고리즘 (Packet Classification)

네트워크 패킷의 헤더 필드(5-tuple 등)를 규칙 집합(ruleset)과 대조하여 해당 패킷의 처리 방식을 결정하는 패킷 분류(Packet Classification) 알고리즘의 이론과 구현을 다룹니다. 선형 탐색, 트라이, 결정 트리(HiCuts/HyperCuts), 튜플 공간 탐색(Tuple Space Search), RFC(Recursive Flow Classification), TCAM 등 주요 접근법의 원리·복잡도·트레이드오프를 비교하고, 리눅스 커널의 cls_u32, cls_flower, cls_bpf 및 하드웨어 오프로드 구현까지 살펴봅니다.

전제 조건: 이 문서를 읽기 전에 TC (Traffic Control), 라우팅, BPF/XDP 문서를 선독하면 이해에 도움이 됩니다.
일상 비유: 공항 보안 검색대를 떠올려 보세요. 승객(패킷)이 도착하면 여권(출발지 IP), 목적지(목적지 IP), 탑승편(포트), 수하물 유형(프로토콜), 좌석 등급(DSCP) 같은 여러 기준을 동시에 확인하여 통과·추가 검사·거부를 결정합니다. 규칙(Rule)이 수백~수만 개일 때, 모든 승객을 한 명씩 모든 규칙과 대조하면(선형 탐색) 대기줄이 끝없이 늘어납니다. 패킷 분류 알고리즘은 이 검색을 체계적으로 빠르게 수행하는 다양한 전략입니다.

핵심 요약

  • 패킷 분류는 다차원(보통 5-tuple) 매칭 문제로, 1차원 LPM(라우팅)보다 본질적으로 어렵습니다
  • RFC는 헤더 필드를 독립적으로 검색한 뒤 단계적으로 교차곱을 축소하여 O(D) 메모리 접근으로 분류합니다 (D = 단계 수, 보통 3~4)
  • HiCuts/HyperCuts는 규칙 공간을 기하학적으로 분할하는 결정 트리 방식입니다
  • Tuple Space Search는 접두사 길이 조합별 해시 테이블을 사용하며, Open vSwitch가 활용합니다
  • TCAM은 하드웨어 병렬 매칭으로 O(1)이지만 용량·전력 제한이 큽니다
  • 리눅스 커널은 cls_u32(해시 기반), cls_flower(마스크 해시), cls_bpf(프로그래밍 가능)를 제공하며, NIC 하드웨어 오프로드도 지원합니다

단계별 이해

  1. 문제 정의를 읽고 패킷 분류가 왜 어려운지 이해합니다
  2. 선형 탐색의 단순한 접근과 한계를 봅니다
  3. 트라이결정 트리로 발전하는 과정을 따라갑니다
  4. RFC 알고리즘의 핵심 아이디어를 집중적으로 학습합니다
  5. 리눅스 커널 구현에서 실제 적용을 확인합니다
  6. 알고리즘 비교 표로 전체를 정리합니다

기초 개념 — 네트워크 패킷의 구조

패킷 분류를 이해하려면 먼저 네트워크 패킷이 무엇인지, 그리고 패킷의 어떤 부분을 보고 분류하는지를 알아야 합니다. 이 섹션에서는 네트워킹 초보자를 위해 기초 개념을 설명합니다.

패킷이란?

인터넷을 통해 데이터를 보낼 때, 데이터는 한꺼번에 전송되지 않고 작은 조각(패킷)으로 나뉘어 전송됩니다. 각 패킷에는 헤더(Header)페이로드(Payload)가 있습니다.

편지 비유: 패킷은 편지 봉투와 같습니다. 봉투 겉면(헤더)에는 보내는 사람 주소, 받는 사람 주소, 우편 종류 등의 정보가 적혀 있고, 봉투 안(페이로드)에는 실제 내용물이 들어 있습니다. 우체국(라우터/방화벽)은 봉투 겉면만 보고 편지를 분류하지, 봉투를 열어서 내용을 읽지는 않습니다.
/* 네트워크 패킷의 기본 구조 */

┌─────────────────────────────────────────────────────┐
│                    이더넷 헤더 (14바이트)               │
│  ┌──────────┬──────────┬──────────┐                 │
│  │ 목적지 MAC │ 출발지 MAC │ EtherType│                 │
│  │  (6바이트) │  (6바이트) │ (2바이트) │                 │
│  └──────────┴──────────┴──────────┘                 │
├─────────────────────────────────────────────────────┤
│                     IP 헤더 (20바이트~)                │
│  ┌────┬────┬──────┬──────┬────────┬────────┐       │
│  │버전 │길이 │ TTL  │프로토콜│ 출발지 IP │ 목적지 IP │       │
│  │(4b)│(4b)│(8b)  │ (8b) │ (32비트) │ (32비트) │       │
│  └────┴────┴──────┴──────┴────────┴────────┘       │
├─────────────────────────────────────────────────────┤
│                  TCP/UDP 헤더 (8~20바이트)              │
│  ┌──────────┬──────────┬───────────────┐            │
│  │ 출발지 포트 │ 목적지 포트 │ 시퀀스/플래그... │            │
│  │  (16비트)  │  (16비트)  │               │            │
│  └──────────┴──────────┴───────────────┘            │
├─────────────────────────────────────────────────────┤
│                   페이로드 (실제 데이터)                 │
│  (웹 페이지, 파일, 이메일 내용 등)                        │
└─────────────────────────────────────────────────────┘

5-tuple — 패킷 분류의 핵심 필드

패킷 분류에서 가장 많이 사용하는 5개 필드를 5-tuple이라고 합니다. 이 5개 필드만으로도 "어떤 컴퓨터가 어떤 서비스에 어떻게 접속하는지"를 식별할 수 있습니다.

필드의미비유예시
출발지 IP패킷을 보내는 컴퓨터의 주소편지의 발신인 주소192.168.1.100
목적지 IP패킷이 도착할 컴퓨터의 주소편지의 수신인 주소10.1.2.3
출발지 포트보내는 쪽의 프로그램 번호발신인의 전화 내선번호49152 (임시 포트)
목적지 포트받는 쪽의 서비스 번호수신인의 전화 내선번호80 (HTTP), 443 (HTTPS)
프로토콜통신 방식 (TCP, UDP, ICMP 등)편지 종류 (등기, 일반, 속달)TCP(6), UDP(17)
/* C 구조체로 표현한 5-tuple */
struct five_tuple {
    uint32_t src_ip;      /* 출발지 IP: 예) 192.168.1.100 = 0xC0A80164 */
    uint32_t dst_ip;      /* 목적지 IP: 예) 10.1.2.3     = 0x0A010203 */
    uint16_t src_port;    /* 출발지 포트: 예) 49152 (클라이언트 임시 포트) */
    uint16_t dst_port;    /* 목적지 포트: 예) 80 (HTTP 서버) */
    uint8_t  protocol;    /* 프로토콜: TCP=6, UDP=17, ICMP=1 */
};

/* 실제 패킷에서 5-tuple을 추출하는 함수 */
void extract_five_tuple(const uint8_t *pkt, struct five_tuple *ft)
{
    const struct iphdr *ip = (struct iphdr *)(pkt + 14); /* 이더넷 14바이트 */
    ft->src_ip   = ntohl(ip->saddr);        /* 네트워크 → 호스트 바이트 순서 */
    ft->dst_ip   = ntohl(ip->daddr);
    ft->protocol = ip->protocol;

    if (ip->protocol == IPPROTO_TCP || ip->protocol == IPPROTO_UDP) {
        const uint16_t *ports = (uint16_t *)(pkt + 14 + ip->ihl * 4);
        ft->src_port = ntohs(ports[0]);     /* TCP/UDP 헤더 첫 2바이트 */
        ft->dst_port = ntohs(ports[1]);     /* TCP/UDP 헤더 다음 2바이트 */
    } else {
        ft->src_port = ft->dst_port = 0;
    }
}

왜 패킷을 분류해야 하는가?

네트워크 장비(라우터, 방화벽, 스위치)는 초당 수백만~수십억 개의 패킷을 처리합니다. 각 패킷에 대해 "이 패킷을 어떻게 처리할 것인가?"를 빠르게 결정해야 합니다.

분류 목적사용 예시사용되는 곳
방화벽허용/차단 결정iptables, nftables, 보안 그룹
QoS (서비스 품질)중요 트래픽 우선 처리TC qdisc, 대역폭 보장
로드 밸런싱트래픽을 여러 서버로 분배IPVS, Cilium, Envoy
모니터링특정 트래픽 통계 수집NetFlow, sFlow, BPF
라우팅 정책출발지에 따라 다른 경로 선택policy routing, VRF
규모의 문제: 방화벽 규칙이 10개일 때는 모든 패킷을 10개 규칙과 하나씩 비교해도 충분히 빠릅니다. 하지만 클라우드 데이터센터에서 보안 그룹 규칙이 10,000개이고, 100Gbps 트래픽(초당 약 1.5억 패킷)을 처리해야 한다면, 패킷당 6.7나노초 안에 분류를 완료해야 합니다. 이때 "영리한 알고리즘"이 필수적입니다.

규칙(Rule)의 구조

패킷 분류 규칙은 "조건 + 액션"으로 구성됩니다. 조건은 5-tuple의 각 필드에 대한 매칭 조건이고, 액션은 매칭된 패킷에 적용할 처리입니다.

/* 분류 규칙의 C 구조체 */
struct classify_rule {
    /* === 매칭 조건 === */
    uint32_t src_ip;          /* 출발지 IP 주소 */
    uint32_t src_mask;        /* 출발지 서브넷 마스크 (예: /24 → 0xFFFFFF00) */
    uint32_t dst_ip;          /* 목적지 IP 주소 */
    uint32_t dst_mask;        /* 목적지 서브넷 마스크 */
    uint16_t src_port_min;    /* 출발지 포트 범위 시작 (0=와일드카드) */
    uint16_t src_port_max;    /* 출발지 포트 범위 끝   (65535=와일드카드) */
    uint16_t dst_port_min;    /* 목적지 포트 범위 시작 */
    uint16_t dst_port_max;    /* 목적지 포트 범위 끝 */
    uint8_t  protocol;        /* 프로토콜 (0=와일드카드) */

    /* === 메타데이터 === */
    int      priority;        /* 우선순위 (숫자가 작을수록 높음) */
    int      action;          /* 액션: ALLOW=1, DROP=0 */
    char     name[16];        /* 규칙 이름 (예: "R1") */
};

/* 패킷이 규칙에 매칭되는지 검사하는 함수 */
int rule_matches(const struct classify_rule *rule,
                 const struct five_tuple *pkt)
{
    /* 1. 출발지 IP: 서브넷 마스크를 적용하여 비교 */
    if ((pkt->src_ip & rule->src_mask) != (rule->src_ip & rule->src_mask))
        return 0;  /* 불매칭 */

    /* 2. 목적지 IP: 동일 방식 */
    if ((pkt->dst_ip & rule->dst_mask) != (rule->dst_ip & rule->dst_mask))
        return 0;

    /* 3. 프로토콜: 0이면 와일드카드 (모든 프로토콜 매칭) */
    if (rule->protocol != 0 && rule->protocol != pkt->protocol)
        return 0;

    /* 4. 출발지 포트: 범위 검사 */
    if (pkt->src_port < rule->src_port_min || pkt->src_port > rule->src_port_max)
        return 0;

    /* 5. 목적지 포트: 범위 검사 */
    if (pkt->dst_port < rule->dst_port_min || pkt->dst_port > rule->dst_port_max)
        return 0;

    return 1;  /* 모든 필드 매칭! */
}

/* 매칭 예시 추적:
 * 규칙: R1 = {src=192.168.1.0/24, dst=10.0.0.0/8, dport=80, proto=TCP}
 * 패킷: {src=192.168.1.100, dst=10.1.2.3, sport=49152, dport=80, proto=TCP}
 *
 * 단계 1: src_ip & mask = 192.168.1.100 & 0xFFFFFF00 = 192.168.1.0
 *         rule.src_ip & mask = 192.168.1.0 & 0xFFFFFF00 = 192.168.1.0 → 일치 ✓
 * 단계 2: dst_ip & mask = 10.1.2.3 & 0xFF000000 = 10.0.0.0
 *         rule.dst_ip & mask = 10.0.0.0 & 0xFF000000 = 10.0.0.0 → 일치 ✓
 * 단계 3: protocol = TCP(6) == TCP(6) → 일치 ✓
 * 단계 4: src_port 49152 ∈ [0, 65535] → 일치 ✓ (와일드카드)
 * 단계 5: dst_port 80 ∈ [80, 80] → 일치 ✓
 * 결과: R1 매칭!
 */
서브넷 마스크 이해하기: 192.168.1.0/24에서 /24는 "IP 주소의 앞 24비트만 비교하라"는 뜻입니다. 즉, 192.168.1.0 ~ 192.168.1.255 범위의 모든 IP 주소가 매칭됩니다. /8이면 앞 8비트만 비교하므로 10.0.0.0 ~ 10.255.255.255 전체가 매칭됩니다. *(와일드카드)는 /0과 같으며, 모든 IP가 매칭됩니다.

패킷 분류 문제 정의

다차원 매칭 문제

패킷 분류는 패킷 헤더의 d개 필드N개 규칙으로 구성된 규칙 집합(ruleset, classifier)과 대조하여, 매칭되는 가장 높은 우선순위의 규칙을 찾는 문제입니다. 전통적인 5-tuple 분류에서 각 규칙은 다음 필드를 지정합니다.

필드크기매칭 유형예시
출발지 IP (Source IP)32비트접두사 (Prefix)192.168.1.0/24
목적지 IP (Destination IP)32비트접두사 (Prefix)10.0.0.0/8
출발지 포트 (Source Port)16비트범위 (Range)1024-65535
목적지 포트 (Destination Port)16비트범위 (Range)80, 443
프로토콜 (Protocol)8비트정확 (Exact)TCP (6)

성능 지표

패킷 분류 알고리즘을 평가하는 세 가지 핵심 지표가 있습니다.

근본적 어려움: 1차원 검색(LPM)은 트라이로 O(W) (W = 비트 수)에 풀 수 있지만, d차원으로 확장하면 최악의 경우 O(Nd)의 공간이 필요할 수 있습니다. 이 차원의 저주(curse of dimensionality)가 패킷 분류를 본질적으로 어렵게 만듭니다.
패킷 분류 개념도 패킷 헤더에서 5-tuple 필드를 추출하여 규칙 테이블과 매칭하는 전체 흐름을 보여줍니다. 패킷 IP + TCP/UDP Header 5-tuple 추출 Src IP Dst IP Src Port Dst Port Protocol (...기타 필드) 분류 엔진 Linear / Trie / HiCuts / RFC / Tuple Space / TCAM / BPF (알고리즘 선택) 규칙 테이블 R1: 192.168.0.0/16 → allow R2: 10.0.0.0/8 → deny ... (N개 규칙) 매칭 규칙 발견 → 액션 적용 (allow/deny/...) 매칭 없음 → 기본 정책 적용 패킷 분류 (Packet Classification) N개 규칙 중 최고 우선순위 매칭 규칙을 빠르게 찾는 문제

기하학적 해석 (Geometric Interpretation)

패킷 분류를 기하학적으로 해석하면 본질적인 어려움이 더 명확해집니다. d차원 공간에서 각 규칙은 초직사각형(hyper-rectangle)을 정의하고, 패킷은 이 공간의 한 점입니다. 패킷 분류 문제는 곧 그 점을 포함하는 가장 높은 우선순위의 초직사각형을 찾는 것입니다.

패킷 분류의 기하학적 해석: 2D 규칙 공간 SrcIP(x축) × DstIP(y축) 2차원 공간에서 규칙이 겹치는 직사각형으로 표현되고, 테스트 패킷이 점으로 표현되는 기하학적 해석을 보여줍니다. 기하학적 해석: 패킷 분류 = 가장 높은 우선순위 초직사각형 찾기 2D 규칙 공간 (SrcIP × DstIP) DstIP SrcIP 0 255.x 0 255.x R8: * × * (DROP) R7: * × * (ICMP, ALLOW) R4: 172.16/12×172.16/12 R3: 192.168/16 × * (UDP) R1,R2 192.168.1 /24 × 10/8 R6: 10/8× 192.168/16 테스트 패킷 (192.168.1.100, 10.1.2.3) 기하학적 해석 핵심 • 각 규칙 = 다차원 초직사각형 • 패킷 = 공간의 한 점 • 분류 = 점을 포함하는 최고 우선순위 직사각형 왜 어려운가? 1. 직사각형이 겹침 (overlap) 2. d=5차원 → 시각화 불가 3. 와일드카드 = 전체 차원 4. 점이 여러 직사각형 내부 → 우선순위 판단 필요 테스트 패킷 ● 포함 직사각형: R1 (우선순위 1) ← 선택! R3 (우선순위 3 — SrcIP만) R8 (우선순위 8 — 와일드카드) 이 페이지의 모든 알고리즘은 이 "점-직사각형 검색" 문제를 해결하는 서로 다른 전략입니다

복잡도 하한 (Complexity Lower Bounds)

패킷 분류의 본질적인 어려움은 이론적으로 증명된 하한(lower bound)에서 드러납니다. 어떤 알고리즘을 설계해도 피할 수 없는 근본적인 제약이 존재합니다.

시간-공간 트레이드오프 정리: d ≥ 4인 경우, 모든 패킷 분류 알고리즘에 대해 다음 트레이드오프가 성립합니다. 공간이 O(Nc)이면 (c는 상수), 분류 시간은 Ω(log(d-1) N)이 필요합니다. 이 결과는 단순히 더 영리한 알고리즘을 설계해도 동시에 빠른 시간, 작은 메모리, 빠른 업데이트를 모두 달성할 수 없음을 의미합니다.
알고리즘 속성이론적 하한현실적 의미
O(N·d) 메모리로 분류O(N) 시간 필요 (선형 탐색 한계)규칙 100만 개 = 100만 비교
O(log N) 분류 시간O(Nd/2) 메모리 필요d=5이면 O(N2.5) 공간 → 실용 불가
O(1) 분류 시간 (TCAM)O(N) 하드웨어 병렬 회로TCAM 칩 비용/전력이 O(N)에 비례

이 하한의 실용적 함의는: 어떤 소프트웨어 알고리즘도 "빠른 분류 + 작은 메모리 + 빠른 업데이트"를 동시에 달성할 수 없습니다. RFC는 메모리를 희생하여 분류 속도를 얻고, 튜플 공간은 다소 느린 분류 대신 업데이트 속도를 얻습니다. TCAM은 이 제약을 하드웨어 병렬성으로 우회하는 유일한 방법이지만, 전력과 비용 제약이 있습니다.

규칙 집합 예시

이후 모든 알고리즘 설명에서 공통으로 사용할 예시 규칙 집합을 정의합니다. 이 8개 규칙을 기준으로 각 알고리즘의 동작을 추적합니다.

규칙우선순위Src IPDst IPSrc PortDst PortProto액션
R11 (최고)192.168.1.0/2410.0.0.0/8*80TCPALLOW
R22192.168.1.0/2410.0.0.0/8*443TCPALLOW
R33192.168.0.0/16**53UDPALLOW
R44172.16.0.0/12172.16.0.0/12***ALLOW
R55***22TCPDROP
R6610.0.0.0/8192.168.0.0/161024-6553580TCPALLOW
R77****ICMPALLOW
R88 (최저)*****DROP

테스트 패킷: Src=192.168.1.100, Dst=10.1.2.3, SrcPort=49152, DstPort=80, Proto=TCP

이 패킷은 R1(192.168.1.0/24 → 10.0.0.0/8, dport=80, TCP)과 R6(10.0.0.0/8은 Src 불일치), R8(와일드카드)에 매칭됩니다. 최고 우선순위 R1(ALLOW)이 선택되어야 합니다.

가장 단순한 방식은 규칙을 우선순위 순서대로 순차 비교하는 것입니다. 첫 번째 매칭 규칙을 반환하거나, 모든 규칙을 비교하여 최고 우선순위 매칭을 선택합니다.

동작 과정

/* 의사코드: 선형 탐색 분류 */
function linear_classify(packet, rules[N]):
    best_match = DEFAULT_RULE
    for i = 0 to N-1:
        if match(packet, rules[i]):
            if rules[i].priority > best_match.priority:
                best_match = rules[i]
    return best_match

예시 규칙 집합으로 추적

테스트 패킷 (192.168.1.100, 10.1.2.3, 49152, 80, TCP)으로 선형 탐색을 추적합니다.

선형 탐색 과정 테스트 패킷을 8개 규칙과 순차적으로 비교하여 최고 우선순위 매칭을 찾는 과정을 보여줍니다. 선형 탐색: 패킷 (192.168.1.100, 10.1.2.3, 49152, 80, TCP) 패킷 R1 매칭! R2 dport≠443 R3 proto≠UDP R4 src≠172.x R5 dport≠22 R6 src≠10.x R7 ≠ICMP 단계별 비교 상세 비교 1: R1 — SrcIP: 192.168.1.100 ∈ 192.168.1.0/24 DstIP: 10.1.2.3 ∈ 10.0.0.0/8 ✓ → 매칭! best=R1 비교 2: R2 — SrcIP DstIP DstPort: 80 ≠ 443 ✗ → 불매칭 비교 3: R3 — SrcIP Proto: TCP ≠ UDP ✗ → 불매칭 비교 4: R4 — SrcIP: 192.168.1.100 ∉ 172.16.0.0/12 ✗ → 불매칭 (첫 필드에서 조기 종료) 비교 5: R5 — DstPort: 80 ≠ 22 ✗ → 불매칭 비교 6: R6 — SrcIP: 192.168.1.100 ∉ 10.0.0.0/8 ✗ → 불매칭 비교 7: R7 — Proto: TCP ≠ ICMP ✗ → 불매칭 비교 8: R8 — 모든 필드 와일드카드 ✓ → 매칭, but priority 8 < R1(1) 결과: best_match = R1 (ALLOW) — 총 8회 비교, 40회 필드 접근 (N×d = 8×5) 문제: 규칙이 10,000개이면 최악 50,000회 필드 접근 — 고속 네트워크에서는 불가

복잡도

지표복잡도비고
분류 시간O(N·d)N = 규칙 수, d = 필드 수
메모리O(N·d)규칙 저장만 필요, 추가 자료구조 없음
업데이트O(1)규칙 추가/삭제가 즉시 가능

리눅스 커널에서의 선형 탐색

리눅스 커널의 TC filter chain 평가 방식이 본질적으로 이 구조입니다. tc filterprio 순서로 순차 평가하며, 첫 매칭에서 종료하거나 continue 액션으로 다음 필터로 넘어갑니다.

/* net/sched/cls_api.c — tcf_classify() 핵심 루프 */

int tcf_classify(struct sk_buff *skb,
                const struct tcf_proto *tp,
                struct tcf_result *res, bool compat_mode)
{
    for (; tp; tp = rcu_dereference_bh(tp->next)) {
        /* 각 filter를 prio 순서로 순차 평가 */
        int err = tp->classify(skb, tp, res);
        if (err >= 0)
            return err;            /* 매칭! → 액션 적용 */
        if (err == TC_ACT_UNSPEC)
            continue;              /* continue → 다음 filter */
    }
    return TC_ACT_UNSPEC;               /* 매칭 없음 → 기본 정책 */
}
캐시 효율: 규칙 수가 적을 때(수십 개 이하) 선형 탐색은 캐시 효율이 높아 더 복잡한 알고리즘보다 빠를 수 있습니다. 규칙 배열이 연속 메모리에 있으면 CPU L1/L2 캐시에 들어가므로, 해시 테이블의 랜덤 접근보다 순차 스캔이 오히려 빠릅니다. iptables가 수천 개 규칙에서도 "충분히 빠른" 이유 중 하나입니다.

최적화 변형: 조기 종료

선형 탐색의 핵심 최적화는 조기 종료(early termination)입니다. 규칙을 우선순위 내림차순으로 정렬하면, 첫 매칭 시점에서 즉시 반환할 수 있습니다(이후 규칙은 모두 낮은 우선순위). 리눅스 TC의 prio 기반 체인이 이 방식입니다. 또한 필드 비교 시에도 첫 번째 불매칭 필드에서 해당 규칙을 건너뛰는 필드 수준 조기 종료가 가능합니다.

선형 탐색 최적화 기법 상세

단순한 조기 종료 외에도 선형 탐색의 성능을 실용적인 수준으로 끌어올리는 다양한 기법이 존재합니다.

블룸 필터 사전 검사 (Bloom Filter Pre-screening)

각 규칙 그룹에 대해 블룸 필터(Bloom Filter)를 구축하면, 불가능한 규칙 그룹 전체를 단 한 번의 해시 연산으로 건너뛸 수 있습니다. 블룸 필터는 공간 효율적인 확률적 자료구조로, 거짓 양성(false positive)은 가능하지만 거짓 음성(false negative)은 불가능합니다. 즉, "이 패킷이 이 그룹에 매칭 가능성 없음"은 100% 확실하지만, "매칭 가능성 있음"은 실제로 없을 수도 있습니다.

/* 블룸 필터 기반 선형 탐색 최적화 */
struct rule_group {
    bloom_filter_t  bf;          /* 그룹의 가능한 (srcip, dstip) 쌍 */
    rule_t          rules[];     /* 실제 규칙 배열 */
    int             num_rules;
};

int bloom_filter_classify(packet_t *pkt, rule_group_t groups[], int ng) {
    for (int g = 0; g < ng; g++) {
        /* O(k) 해시 연산으로 그룹 전체 skip 여부 결정 */
        if (!bloom_query(&groups[g].bf, pkt->srcip, pkt->dstip))
            continue;    /* 확실히 매칭 없음 → 그룹 전체 skip */

        /* 매칭 가능성 있는 그룹만 선형 탐색 */
        for (int r = 0; r < groups[g].num_rules; r++) {
            if (match(pkt, &groups[g].rules[r]))
                return groups[g].rules[r].action;
        }
    }
    return DEFAULT_ACTION;
}

블룸 필터의 오탐률(false positive rate)은 비트 배열 크기와 해시 함수 수로 조절합니다. 실용적인 설정에서 오탐률을 1% 이하로 유지하면서 99%의 그룹을 건너뛸 수 있습니다.

블룸 필터 사전 검사 파이프라인 패킷이 각 규칙 그룹의 블룸 필터를 먼저 통과하여 불가능한 그룹을 건너뛰고, 통과한 그룹만 선형 탐색하는 최적화 파이프라인을 보여줍니다. 블룸 필터 사전 검사: 선형 탐색 최적화 패킷 SrcIP, DstIP SrcP, DstP, Proto 그룹 1 블룸 필터 TCP/80 규칙 집합 ■□■□□■□■ (비트 배열) MISS → 그룹 skip! 건너뜀 그룹 2 블룸 필터 192.168.x/TCP 규칙 집합 ■■□■■□■■ (비트 배열) HIT → 선형 탐색 수행 선형 탐색 R1: 192.168.1.0/24 ✓ R2: 192.168.1.0/24 ✗ → R1 매칭! R1 (ALLOW) 조기 종료! 블룸 필터 사전 검사의 효과 • 블룸 필터 hit 여부: O(k) 해시 연산 (k = 해시 함수 수, 보통 3~7) • 오탐률 1%: 평균 99개 그룹 중 1개만 실제 선형 탐색 — 100배 가속 • 주의: 오탐 시 불필요한 선형 탐색 발생 — 오탐률 vs 메모리 트레이드오프 • 블룸 필터 크기 최적화: m = -n·ln(p) / (ln2)² (n=원소수, p=오탐률)

필드 순서 최적화 (Field Ordering)

선형 탐색에서 필드를 비교하는 순서는 평균 비교 횟수에 큰 영향을 미칩니다. 가장 많은 규칙을 제거하는(discriminating) 필드를 먼저 비교하면 조기 종료가 더 자주 발생합니다.

최적 필드 순서를 결정하는 기준은 해당 필드의 차별화 비율(discrimination ratio)입니다. 예를 들어, Protocol 필드가 와일드카드(*) 규칙 비율이 낮고 TCP/UDP/ICMP 등이 명시된 규칙 비율이 높다면, Protocol을 먼저 검사하는 것이 유리합니다.

/* 필드 순서 최적화: 통계 기반 */
/* 실제 트래픽 데이터로 각 필드의 discriminating power를 측정 */

/* 예: 규칙셋 통계 */
/* Protocol: 80% 규칙이 명시 → 높은 차별화 → 먼저 검사 */
/* DstPort:  60% 규칙이 명시 → 중간 차별화 */
/* SrcIP:    50% 규칙이 명시 → 중간 차별화 */
/* DstIP:    40% 규칙이 명시 → 낮은 차별화 → 나중에 검사 */
/* SrcPort:  20% 규칙이 명시 → 매우 낮음 (대부분 에페머럴 와일드카드) */

/* 최적 순서: Protocol → DstPort → SrcIP → DstIP → SrcPort */

int optimized_linear(packet_t *pkt, rule_t rules[], int N) {
    for (int i = 0; i < N; i++) {
        /* 차별화 순서대로 비교: 빠른 조기 종료 */
        if (rules[i].proto  != WILD && rules[i].proto  != pkt->proto)  continue;
        if (rules[i].dport  != WILD && rules[i].dport  != pkt->dport)  continue;
        if (!prefix_match(rules[i].srcip, pkt->srcip))                 continue;
        if (!prefix_match(rules[i].dstip, pkt->dstip))                 continue;
        if (rules[i].sport  != WILD && !range_match(rules[i].sport, pkt->sport)) continue;
        return rules[i].action;   /* 모든 필드 매칭 */
    }
    return DEFAULT_ACTION;
}

규칙 그룹화 (Rule Grouping)

공통 필드 패턴을 가진 규칙들을 그룹으로 묶으면, 첫 번째 공통 필드에서 불일치가 발생하면 그룹 전체를 건너뛸 수 있습니다. 이는 결정 트리의 단순화된 형태로 볼 수 있습니다.

/* 규칙 그룹화: Protocol별로 사전 분류 */
struct rule_group_by_proto {
    uint8_t   protocol;     /* 그룹 공통 필드 */
    rule_t    rules[];      /* 이 프로토콜의 모든 규칙 */
    int       count;
};

/* TCP 그룹: R1, R2, R5, R6 (모두 TCP 명시) */
/* UDP 그룹: R3 */
/* ICMP 그룹: R7 */
/* WILD 그룹: R4, R8 (프로토콜 와일드카드) */

int grouped_classify(packet_t *pkt, rule_groups_t groups[]) {
    int proto = pkt->proto;
    /* 해당 프로토콜 그룹만 선형 탐색 */
    search_group(&groups[proto]);      /* 예: TCP 그룹 4개 규칙 */
    search_group(&groups[WILD]);       /* 와일드카드 그룹 항상 */
}

완전한 C 구현 — 선형 탐색 분류기

아래는 실제로 컴파일하고 실행할 수 있는 수준의 선형 탐색 패킷 분류기 구현입니다. 앞서 정의한 8개 예시 규칙을 등록하고, 테스트 패킷을 분류하는 전체 과정을 보여줍니다.

/* linear_classifier.c — 완전한 선형 탐색 패킷 분류기
 *
 * 컴파일: gcc -O2 -o linear_classifier linear_classifier.c
 * 실행:   ./linear_classifier
 *
 * 이 코드는 패킷 분류의 가장 기본적인 구현을 보여줍니다.
 * 규칙을 우선순위 순서대로 순차 비교하여 첫 매칭을 반환합니다.
 */
#include <stdio.h>
#include <stdint.h>
#include <string.h>
#include <arpa/inet.h>

/* ── 자료구조 정의 ── */

enum action { ACT_DROP = 0, ACT_ALLOW = 1 };

struct rule {
    char     name[8];
    int      priority;       /* 숫자가 작을수록 높은 우선순위 */
    uint32_t src_ip, src_mask;
    uint32_t dst_ip, dst_mask;
    uint16_t sport_min, sport_max;
    uint16_t dport_min, dport_max;
    uint8_t  proto;          /* 0 = 와일드카드 */
    enum action act;
};

struct packet {
    uint32_t src_ip, dst_ip;
    uint16_t src_port, dst_port;
    uint8_t  proto;
};

/* ── IP 주소 문자열 → 정수 변환 헬퍼 ── */
static uint32_t ip2u32(const char *s) {
    struct in_addr a;
    inet_pton(AF_INET, s, &a);
    return ntohl(a.s_addr);
}

static uint32_t prefix2mask(int prefix_len) {
    return prefix_len == 0 ? 0 : ~((1U << (32 - prefix_len)) - 1);
}

/* ── 단일 규칙 매칭 검사 ── */
static int match_rule(const struct rule *r, const struct packet *p)
{
    /* 각 필드를 순서대로 검사 — 하나라도 불일치하면 즉시 반환 */
    if ((p->src_ip & r->src_mask) != (r->src_ip & r->src_mask))
        return 0;
    if ((p->dst_ip & r->dst_mask) != (r->dst_ip & r->dst_mask))
        return 0;
    if (r->proto != 0 && r->proto != p->proto)
        return 0;
    if (p->src_port < r->sport_min || p->src_port > r->sport_max)
        return 0;
    if (p->dst_port < r->dport_min || p->dst_port > r->dport_max)
        return 0;
    return 1;
}

/* ── 선형 탐색 분류 ── */
static const struct rule *linear_classify(
    const struct rule rules[], int n,
    const struct packet *pkt)
{
    const struct rule *best = NULL;
    int comparisons = 0;

    for (int i = 0; i < n; i++) {
        comparisons++;
        if (match_rule(&rules[i], pkt)) {
            if (!best || rules[i].priority < best->priority)
                best = &rules[i];
            /* 최적화: 규칙이 우선순위 순이면 첫 매칭에서 종료 가능 */
        }
    }
    printf("  [선형 탐색] %d회 비교 수행\n", comparisons);
    return best;
}

/* ── 예시 규칙 집합 등록 ── */
int main(void)
{
    struct rule rules[] = {
      /*  name pri  src_ip       src_mask   dst_ip       dst_mask
       *                sport_min/max  dport_min/max  proto  action */
      { "R1", 1, 0xC0A80100, 0xFFFFFF00, 0x0A000000, 0xFF000000,
                 0, 65535,  80, 80,   6, ACT_ALLOW },
      { "R2", 2, 0xC0A80100, 0xFFFFFF00, 0x0A000000, 0xFF000000,
                 0, 65535, 443, 443,  6, ACT_ALLOW },
      { "R3", 3, 0xC0A80000, 0xFFFF0000, 0, 0,
                 0, 65535,  53, 53,  17, ACT_ALLOW },
      { "R4", 4, 0xAC100000, 0xFFF00000, 0xAC100000, 0xFFF00000,
                 0, 65535,   0, 65535, 0, ACT_ALLOW },
      { "R5", 5, 0, 0, 0, 0,
                 0, 65535,  22, 22,   6, ACT_DROP  },
      { "R6", 6, 0x0A000000, 0xFF000000, 0xC0A80000, 0xFFFF0000,
              1024, 65535,  80, 80,   6, ACT_ALLOW },
      { "R7", 7, 0, 0, 0, 0,
                 0, 65535,   0, 65535, 1, ACT_ALLOW },
      { "R8", 8, 0, 0, 0, 0,
                 0, 65535,   0, 65535, 0, ACT_DROP  },
    };
    int n = sizeof(rules) / sizeof(rules[0]);

    /* 테스트 패킷: (192.168.1.100, 10.1.2.3, 49152, 80, TCP) */
    struct packet pkt = {
        .src_ip   = ip2u32("192.168.1.100"),
        .dst_ip   = ip2u32("10.1.2.3"),
        .src_port = 49152,
        .dst_port = 80,
        .proto    = 6   /* TCP */
    };

    printf("=== 선형 탐색 패킷 분류기 ===\n");
    printf("패킷: src=192.168.1.100 dst=10.1.2.3 sport=49152 dport=80 proto=TCP\n\n");

    const struct rule *result = linear_classify(rules, n, &pkt);
    if (result)
        printf("  결과: %s (우선순위 %d) → %s\n",
               result->name, result->priority,
               result->act == ACT_ALLOW ? "ALLOW" : "DROP");
    else
        printf("  결과: 매칭 없음 → 기본 정책\n");

    return 0;
}

/* 실행 결과:
 * === 선형 탐색 패킷 분류기 ===
 * 패킷: src=192.168.1.100 dst=10.1.2.3 sport=49152 dport=80 proto=TCP
 *
 *   [선형 탐색] 8회 비교 수행
 *   결과: R1 (우선순위 1) → ALLOW
 */
초급자 포인트: 이 코드의 핵심은 match_rule() 함수입니다. 패킷의 각 필드를 규칙의 조건과 하나씩 비교하고, 하나라도 불일치하면 즉시 다음 규칙으로 넘어갑니다(조기 종료). 이 조기 종료 덕분에 평균적으로 5개 필드를 모두 비교하지 않아도 되어 실제 성능이 이론 최악치보다 좋습니다.

트라이 기반 분류

단일 필드 LPM

IP 주소 하나에 대한 최장 접두사 매칭(LPM, Longest Prefix Match)은 이진 트라이(Binary Trie)나 LC-Trie로 효율적으로 해결됩니다. 리눅스 FIB(Forwarding Information Base)가 정확히 이 구조를 사용합니다.

/* 1차원 이진 트라이 LPM 의사코드 */
function trie_lpm(root, ip_address, W=32):
    node = root
    best_match = NULL
    for bit = W-1 downto 0:
        if node.has_prefix:        /* 이 노드에 접두사가 등록됨 */
            best_match = node.prefix /* 더 긴 매칭으로 갱신 */
        next_bit = (ip_address >> bit) & 1
        if node.child[next_bit] == NULL:
            break                  /* 더 이상 내려갈 수 없음 */
        node = node.child[next_bit]
    return best_match          /* O(W) 시간, W=32 for IPv4 */

다중 비트 트라이 (Multi-bit Trie)

1비트씩 처리하는 이진 트라이는 IPv4에서 최대 32단계의 메모리 접근이 필요합니다. 다중 비트 트라이(Multi-bit Trie)는 한 번에 k비트(스트라이드, stride)씩 처리하여 트리 깊이를 줄입니다.

/* 다중 비트 트라이 (stride=8): 4단계로 IPv4 LPM */
struct mbtrie_node {
    struct mbtrie_node *children[256];  /* 2^stride = 256 자식 */
    prefix_t            prefix;         /* 이 노드에 등록된 접두사 */
};

/* stride=8 → 4단계 (32/8=4) */
prefix_t mbtrie_lpm(mbtrie_t *root, uint32_t ip) {
    prefix_t best = NULL;
    mbtrie_node *node = root;
    for (int level = 0; level < 4 && node; level++) {
        uint8_t idx = (ip >> (24 - level*8)) & 0xFF;  /* 8비트씩 */
        if (node->prefix) best = node->prefix;
        node = node->children[idx];
    }
    return best;   /* O(4) 메모리 접근 vs 이진 트라이 O(32) */
}
스트라이드 (k)트리 깊이노드당 자식 수총 노드 수 (IPv4)트레이드오프
1비트322최대 232느림, 메모리 절약
8비트4256최대 2564빠름, 메모리 증가
16비트265,536최대 655362매우 빠름, 메모리 대
가변 (LC-Trie)평균 낮음가변공간 압축리눅스 FIB 실제 구현

리눅스 커널의 FIB 라우팅 테이블은 LC-Trie(Level Compressed Trie)를 사용하여 스트라이드를 동적으로 조절합니다. 자식이 없는 노드는 축소하고, 가득 찬 노드는 스트라이드를 늘려 메모리와 속도를 균형 있게 조절합니다.

다차원 확장의 한계

문제는 이를 d차원으로 확장할 때 발생합니다. 대표적인 접근법들입니다.

Grid-of-Tries

Srinivasan et al. (1998)이 제안한 Grid-of-Tries는 첫 번째 필드(SrcIP)의 트라이 리프에 두 번째 필드(DstIP)의 트라이를 연결합니다. switch pointer를 추가하여 불필요한 서브트라이 탐색을 생략합니다.

Grid-of-Tries 2차원 구조 Src IP 트라이의 리프에 Dst IP 서브트라이를 연결하고, switch pointer로 불필요한 탐색을 생략하는 구조를 보여줍니다. Grid-of-Tries: 2차원 (Src IP × Dst IP) Src IP Trie root 0 1 10.* 11.* 100* 101* Dst IP Sub-Tries Dst Trie (for SrcIP=100*) 0* 1* R4 Dst Trie (for SrcIP=101*) 0* R6 1* switch pointer Switch Pointer: 불필요한 서브트라이 탐색을 건너뛰어 O(2W) 시간 보장 한계: 3차원 이상에서 공간 폭발 → 패킷 분류의 5-tuple에는 부적합 교훈: 1차원 LPM은 쉽지만, 다차원은 본질적으로 다른 문제

Cross-producting

각 필드에서 독립적으로 LPM을 수행한 뒤, 결과의 교차곱(cross product) 테이블에서 최종 규칙을 조회합니다. RFC 알고리즘의 직접적인 원형입니다.

Set-pruning Trie

트라이의 각 노드에 해당 접두사와 매칭되는 규칙을 미리 복제하여 저장합니다. 탐색은 빠르지만(단일 트라이 탐색), 규칙 복제로 최악 O(Nd·W) 공간이 필요합니다.

핵심 통찰: 1차원 검색은 O(log N) 또는 O(W)로 쉽게 풀리지만, 다차원에서는 각 차원의 결과를 어떻게 결합(combine)하느냐가 알고리즘의 핵심 설계 결정입니다. 이후의 모든 알고리즘은 이 결합 전략의 변종으로 볼 수 있습니다.
접근법분류 시간메모리차원 확장성
Grid-of-TriesO(dW)O(NW)2차원만 실용적
Cross-productingO(dW + 1 lookup)O(Nd)공간 폭발
Set-pruning TrieO(dW)O(NdW)공간 폭발

결정 트리 방식 (HiCuts / HyperCuts)

결정 트리(Decision Tree) 방식은 규칙이 정의하는 다차원 공간을 재귀적으로 분할(cutting)하여, 리프 노드에 소수의 규칙만 남도록 합니다. 패킷이 도착하면 루트에서 리프까지 트리를 탐색하고, 리프의 소수 규칙만 선형 비교합니다.

HiCuts (Hierarchical Intelligent Cuttings)

Gupta & McKeown (2000)이 제안한 HiCuts는 각 트리 노드에서 단일 차원을 선택하여 균등하게 분할합니다.

알고리즘 상세

  1. 차원 선택: 현재 노드에 포함된 규칙들 중, 가장 많은 고유 프로젝션(projection)을 가진 차원을 선택합니다. 직관적으로, 규칙들이 가장 "구별되는" 차원으로 분할하면 효과적입니다
  2. 분할 수 결정: 분할 수 nc는 메모리 제약(spfac, space factor)에 의해 제한됩니다. 규칙 복제까지 고려한 총 자식 규칙 수가 spfac × N을 넘지 않도록 합니다
  3. 종료 조건: 리프 노드의 규칙 수가 임계값 binth(보통 8~16) 이하가 되면 분할을 멈추고, 나머지는 선형 탐색합니다
/* HiCuts 트리 구축 의사코드 */
function build_hicuts(rules[], binth, spfac):
    if |rules| <= binth:
        return LeafNode(rules)   /* 선형 탐색 구간 */

    dim = select_dimension(rules)       /* 가장 구별되는 차원 */
    nc = compute_num_cuts(rules, dim, spfac)

    node = InternalNode(dim, nc)
    for i = 0 to nc-1:
        child_rules = { r ∈ rules : r[dim] overlaps interval i }
        node.child[i] = build_hicuts(child_rules, binth, spfac)

    return node

/* HiCuts 분류 의사코드 */
function hicuts_classify(node, packet):
    if node is LeafNode:
        return linear_search(node.rules, packet)  /* O(binth) */
    interval = compute_interval(packet[node.dim], node.nc)
    return hicuts_classify(node.child[interval], packet)

HyperCuts

Singh et al. (2003)이 제안한 HyperCuts는 HiCuts를 확장하여 여러 차원을 동시에 분할합니다.

예시 규칙 집합으로 HiCuts 트리 구축 추적

앞서 정의한 8개 예시 규칙에 대해 HiCuts 트리를 단계별로 구축하면서, 실제 차원 선택·분할·복제 과정을 상세히 추적합니다. SrcIP와 DstIP 두 필드(2D)만 사용하는 단순화 버전으로 설명합니다.

Step 1: 루트 노드 — 차원 선택

루트에서 8개 규칙을 분석합니다. 각 차원의 고유 구간(distinct intervals) 수를 계산합니다.

HiCuts는 SrcIP 차원을 선택합니다 (더 많은 고유 구간). spfac=8로 설정하면 자식당 허용 규칙 수 = 8×8/5 ≈ 12. 이를 4구간으로 분할합니다.

Step 2: SrcIP 4-way 분할

/* SrcIP 공간 4구간 분할 */
구간 0: 0.0.0.0  - 9.255.255.255   → 규칙: R7(*, ICMP), R8(*,*) → 2개
구간 1: 10.0.0.0 - 99.255.255.255  → 규칙: R6(10/8), R7, R8     → 3개
구간 2: 100.0.0.0 - 172.31.255.255 → 규칙: R4(172.16/12), R7, R8 → 3개 (+R4 복제!)
구간 3: 172.32.0.0 - 255.255.255.255 → 포함: R1(192.168.1/24), R2, R3(192.168/16), R4★, R7, R8
                                     → 규칙: 6개 (R4 복제 발생!)

규칙 복제: R4(172.16.0.0/12)는 SrcIP 구간 2와 3 양쪽에 걸쳐 있어 두 자식 노드 모두에 복제됩니다. R7과 R8도 와일드카드이므로 모든 구간에 복제됩니다.

Step 3: 구간 3의 추가 분할 (DstIP 차원 선택)

구간 3에 6개 규칙이 있어 binth=4를 초과하므로 추가 분할합니다. 이번에는 DstIP 차원이 더 많은 고유 구간을 가지므로 선택됩니다.

/* 구간 3: DstIP 분할 */
구간 3.0: 0.0.0.0  - 9.255.255.255    → R1(Dst=10/8 포함), R2, R7, R8 → 4개
구간 3.1: 10.0.0.0 - 171.255.255.255  → R1★(복제), R2★, R7, R8        → 4개 (R1,R2 복제!)
구간 3.2: 172.0.0.0 - 191.255.255.255 → R3(Dst=192.168/16 경계), R7, R8 → 3개
구간 3.3: 192.168.0.0 - 255.255.255.255 → R3★, R4★, R7, R8            → 4개
HiCuts 트리 구축 결과 (예시 규칙 집합) 8개 예시 규칙에 대해 HiCuts 알고리즘이 SrcIP 차원과 DstIP 차원을 순차적으로 분할하여 구축한 결정 트리 구조를 보여줍니다. 규칙 복제가 어디서 발생하는지 강조합니다. HiCuts 트리: 8개 예시 규칙 (★ = 규칙 복제) 루트 노드 분할 차원: SrcIP | 4구간 규칙: R1~R8 (8개) 구간0: 0-9.x 구간1: 10.x 구간2: 172.x 구간3: 192.x+ 리프 0 R7, R8 2개 규칙 → 선형 탐색 리프 1 R6, R7, R8 3개 규칙 → 선형 탐색 리프 2 R4★, R7, R8 R4 복제! → 선형 탐색 내부 노드 3 분할: DstIP | 4구간 규칙: 6개 (R1,R2,R3,R4★,R7,R8) Dst0: Dst1: Dst2: Dst3: 리프 3.0 R1,R2,R7,R8 binth=4 → 리프! 리프 3.1 R1★,R2★,R7,R8 R1,R2 복제! 리프 3.2 R3,R7,R8 3개 규칙 리프 3.3 R3★,R4★,R7,R8 R3,R4 복제! 테스트 패킷 추적: (SrcIP=192.168.1.100, DstIP=10.1.2.3) 루트 → SrcIP=192.168.1.100 → 구간 3 선택 → 내부 노드 3 내부 노드 3 → DstIP=10.1.2.3 → 구간 1 선택 → 리프 3.1 (R1★, R2★, R7, R8) 리프 3.1 선형 탐색: R1★ 매칭 (우선순위 1) → R1 ALLOW [총 2회 비교 + 4개 리프 선형]

이 예시에서 총 규칙 복제 수는: R4(2회) + R7(4회) + R8(4회) + R1(1회) + R2(1회) + R3(1회) = 원래 8개 규칙이 트리 전체에서 21개 슬롯으로 복제됩니다. 복제 비율 = 21/8 ≈ 2.6배. 실제 대규모 규칙셋에서는 복제 비율이 수십 배까지 높아질 수 있습니다.

EffiCuts — 규칙 복제 최적화

Vamanan et al. (2010)의 EffiCuts는 HyperCuts의 규칙 복제 문제를 해결하기 위해 규칙 분리(separable rules) 전략을 도입했습니다. 큰 와일드카드를 가진 규칙과 작은 범위의 규칙을 별도의 서브트리로 분리하여, 와일드카드 규칙의 대량 복제를 방지합니다.

규칙 복제 문제

결정 트리 방식의 가장 큰 약점은 규칙 복제(rule replication)입니다. 하나의 규칙이 여러 분할 구간에 걸치면, 그 규칙은 해당되는 모든 자식 노드에 복사되어야 합니다. 최악의 경우 메모리 사용량이 O(Nd)로 폭발할 수 있습니다.

예시 규칙 집합에서 R8(모든 필드 와일드카드)은 어떤 분할을 하든 모든 자식에 복제됩니다. R4(172.16.0.0/12)처럼 넓은 범위의 규칙도 여러 구간에 걸쳐 복제됩니다. 이런 "큰" 규칙이 많을수록 복제 비율이 급증합니다.

HiCuts 2차원 공간 분할 2차원 규칙 공간에서 HiCuts가 단일 차원을 순차적으로 분할(cutting)하여 규칙 영역을 분리하는 과정을 보여줍니다. HiCuts: 2차원 공간 분할 예시 규칙 공간 (Src IP × Dst IP) Dst IP Src IP R1 R2 R3 R4 cut 1단계 분할 (Src IP 축, 3-cut) 구간 0 구간 1 구간 2 R1 R2 R1★ R2★ R3 R4 ★ = 규칙 복제 (replication): R1, R2가 여러 구간에 걸쳐 복사됨 메모리 증가 원인 — 최악의 경우 O(N^d) 복제 가능
지표HiCutsHyperCuts
분류 시간O(트리 깊이)O(트리 깊이), 일반적으로 HiCuts보다 얕음
메모리O(N·2W/nc) 최악유사하지만 깊이 감소로 총량 줄 수 있음
업데이트O(트리 깊이)O(트리 깊이)
주요 약점규칙 복제로 인한 메모리 폭발

튜플 공간 탐색 (Tuple Space Search)

Srinivasan et al. (1999)이 제안한 튜플 공간 탐색(Tuple Space Search)은 규칙을 접두사 길이 조합(tuple)별로 분류하여, 각 튜플에 대해 해시 테이블을 구축하는 방법입니다.

핵심 아이디어

5-tuple 규칙의 각 필드에는 접두사 길이가 있습니다. 예를 들어 규칙 192.168.0.0/16, 10.0.0.0/8, *, *, TCP의 접두사 길이 튜플은 (16, 8, 0, 0, exact)입니다. 같은 튜플을 가진 규칙들은 하나의 해시 테이블에 모아두고, 패킷의 해당 필드를 마스킹한 값으로 해시 룩업합니다.

예시 규칙 집합의 튜플 분류

예시 규칙 8개를 접두사 길이 튜플별로 분류하면:

튜플 (SrcIP/DstIP/SrcP/DstP/Proto)포함 규칙해시 테이블 크기
(24, 8, 0, exact, exact)R1, R22 엔트리
(16, 0, 0, exact, exact)R31 엔트리
(12, 12, 0, 0, 0)R41 엔트리
(0, 0, 0, exact, exact)R51 엔트리
(8, 16, range, exact, exact)R61 엔트리
(0, 0, 0, 0, exact)R71 엔트리
(0, 0, 0, 0, 0)R81 엔트리

7개 튜플이 생성됩니다. 패킷 분류 시 최악 7번의 해시 룩업이 필요하지만, 8개 규칙을 모두 선형 탐색하는 것과 대비하면 각 룩업이 O(1)이므로 큰 차이가 없어 보입니다. 그러나 규칙이 10,000개로 늘어나도 튜플 수는 보통 수십~수백 개에 불과하므로, 규칙 수가 늘수록 이점이 커집니다.

튜플 공간 탐색 구조 규칙을 접두사 길이 튜플별 해시 테이블로 분류하고, 패킷을 각 튜플에서 순차적으로 탐색하는 과정을 보여줍니다. 튜플 공간 탐색: 패킷 (192.168.1.100, 10.1.2.3, 49152, 80, TCP) 패킷 헤더 5-tuple 추출 Tuple 1 (24, 8, 0, exact, exact) → R1, R2 mask(pkt) = (192.168.1.0, 10.0.0.0, *, 80, TCP) → hash lookup → R1 매칭! (best=R1) Tuple 2 (16, 0, 0, exact, exact) → R3 mask(pkt) = (192.168.0.0, *, *, 80, TCP) → 미스 (R3 dport=53) Tuple 3 (12, 12, 0, 0, 0) → R4 mask(pkt) = (192.160.0.0, 10.0.0.0, *, *, *) → 미스 Tuple 4 (0, 0, 0, exact, exact) → R5 mask(pkt) = (*, *, *, 80, TCP) → 미스 (R5 dport=22) Tuple 5 (8, 16, range, exact, exact) → R6 mask(pkt) = (192.0.0.0, 10.1.0.0, ..., 80, TCP) → 미스 Tuple 6 (0, 0, 0, 0, exact) → R7 미스 (R7 proto=ICMP) Tuple 7 (0, 0, 0, 0, 0) → R8 → R8 매칭, but priority < R1 결과: R1 (ALLOW) 7회 해시 룩업으로 분류 완료 vs 선형 탐색 선형: 8규칙 × 5필드 = 40회 비교 TSS: 7튜플 × 1해시 = 7회 룩업 규칙 10K개 → TSS 약 50배 빠름 각 튜플에서 패킷을 마스킹한 뒤 해시 테이블에서 O(1) 룩업 수행 핵심: 규칙 수(N)가 아닌 튜플 수(T)에 비례 — 실제 규칙셋에서 T ≪ N 최적화: Tuple Priority Sorting — 최근 히트 튜플을 먼저 탐색

완전한 C 구현 — 튜플 공간 탐색 분류기

아래는 튜플 공간 탐색(Tuple Space Search)의 핵심 아이디어를 보여주는 완전한 구현입니다. 규칙을 접두사 길이 조합(마스크)별로 그룹화하고, 각 그룹의 해시 테이블에서 O(1) 룩업을 수행합니다.

/* tuple_space_classifier.c — 튜플 공간 탐색 분류기
 *
 * 핵심 아이디어:
 *   1. 같은 마스크 패턴의 규칙끼리 그룹(튜플)으로 묶음
 *   2. 각 튜플에 해시 테이블 구축
 *   3. 패킷 도착 시: 각 튜플에서 마스킹 → 해시 룩업 (O(1))
 *   4. 모든 튜플 순회 → 최고 우선순위 매칭 선택
 *
 * 컴파일: gcc -O2 -o tss_classifier tuple_space_classifier.c
 */
#include <stdio.h>
#include <stdint.h>
#include <string.h>

#define MAX_RULES_PER_TUPLE 16
#define HASH_BUCKETS        64

/* ── 마스크 패턴: 각 필드의 접두사 길이 ── */
struct mask_pattern {
    uint32_t src_mask;    /* 예: /24 → 0xFFFFFF00 */
    uint32_t dst_mask;    /* 예: /8  → 0xFF000000 */
    uint16_t sport_mask;  /* 0xFFFF = exact, 0 = wildcard */
    uint16_t dport_mask;
    uint8_t  proto_mask;  /* 0xFF = exact, 0 = wildcard */
};

struct rule {
    char     name[8];
    int      priority;
    uint32_t src_ip, dst_ip;
    uint16_t src_port, dst_port;
    uint8_t  proto;
    int      action;     /* 1=ALLOW, 0=DROP */
};

/* ── 해시 테이블 엔트리 ── */
struct hash_entry {
    uint32_t key[5];     /* 마스킹된 5-tuple 키 */
    struct rule *rule;
    struct hash_entry *next; /* 체이닝 */
};

/* ── 튜플: 같은 마스크 패턴의 규칙 그룹 ── */
struct tuple {
    struct mask_pattern  mask;
    struct hash_entry   *buckets[HASH_BUCKETS];
    int                 rule_count;
    int                 hit_count;  /* 최근 히트 수 (정렬 우선순위) */
};

/* ── 간단한 해시 함수 ── */
static uint32_t hash5(uint32_t key[5]) {
    uint32_t h = 2166136261u;
    for (int i = 0; i < 5; i++)
        h = (h ^ key[i]) * 16777619u;
    return h % HASH_BUCKETS;
}

/* ── 규칙을 튜플의 해시 테이블에 삽입 ── */
static void tuple_insert(struct tuple *t, struct rule *r)
{
    /* 1. 규칙의 필드를 마스킹하여 해시 키 생성 */
    uint32_t key[5] = {
        r->src_ip   & t->mask.src_mask,
        r->dst_ip   & t->mask.dst_mask,
        r->src_port & t->mask.sport_mask,
        r->dst_port & t->mask.dport_mask,
        r->proto    & t->mask.proto_mask
    };

    /* 2. 해시 → 버킷 선택 → 체인에 추가 */
    uint32_t idx = hash5(key);
    static struct hash_entry pool[256];
    static int pool_idx = 0;
    struct hash_entry *e = &pool[pool_idx++];
    memcpy(e->key, key, sizeof(key));
    e->rule = r;
    e->next = t->buckets[idx];
    t->buckets[idx] = e;
    t->rule_count++;
}

/* ── 패킷을 튜플에서 룩업 ── */
static struct rule *tuple_lookup(struct tuple *t,
    uint32_t src_ip, uint32_t dst_ip,
    uint16_t sport, uint16_t dport, uint8_t proto)
{
    /* 1. 패킷 필드를 이 튜플의 마스크로 마스킹 */
    uint32_t key[5] = {
        src_ip  & t->mask.src_mask,
        dst_ip  & t->mask.dst_mask,
        sport   & t->mask.sport_mask,
        dport   & t->mask.dport_mask,
        proto   & t->mask.proto_mask
    };

    /* 2. 해시 → 버킷 → 키 비교 (O(1) 평균) */
    uint32_t idx = hash5(key);
    for (struct hash_entry *e = t->buckets[idx]; e; e = e->next) {
        if (memcmp(e->key, key, sizeof(key)) == 0) {
            t->hit_count++;
            return e->rule;  /* O(1) 매칭! */
        }
    }
    return NULL;  /* 이 튜플에서는 매칭 없음 */
}

/* ── 전체 TSS 분류 ── */
static struct rule *tss_classify(
    struct tuple tuples[], int nt,
    uint32_t src_ip, uint32_t dst_ip,
    uint16_t sport, uint16_t dport, uint8_t proto)
{
    struct rule *best = NULL;
    int lookups = 0;

    for (int i = 0; i < nt; i++) {
        lookups++;
        struct rule *r = tuple_lookup(&tuples[i],
                            src_ip, dst_ip, sport, dport, proto);
        if (r && (!best || r->priority < best->priority))
            best = r;
    }
    printf("  [TSS] %d개 튜플 룩업 수행 (vs 선형 탐색 8회 비교)\n", lookups);
    return best;
}

int main(void)
{
    /* 규칙 정의 (예시 규칙 집합) */
    struct rule rules[] = {
        {"R1", 1, 0xC0A80100, 0x0A000000, 0, 80, 6, 1},
        {"R2", 2, 0xC0A80100, 0x0A000000, 0, 443, 6, 1},
        {"R3", 3, 0xC0A80000, 0, 0, 53, 17, 1},
        {"R4", 4, 0xAC100000, 0xAC100000, 0, 0, 0, 1},
        {"R5", 5, 0, 0, 0, 22, 6, 0},
        {"R8", 8, 0, 0, 0, 0, 0, 0},
    };

    /* 튜플 정의: 각 규칙의 마스크 패턴별로 분류 */
    struct tuple tuples[6];
    memset(tuples, 0, sizeof(tuples));

    /* Tuple 0: (24, 8, 0, exact, exact) — R1, R2 */
    tuples[0].mask = (struct mask_pattern){
        0xFFFFFF00, 0xFF000000, 0, 0xFFFF, 0xFF};
    tuple_insert(&tuples[0], &rules[0]);  /* R1 */
    tuple_insert(&tuples[0], &rules[1]);  /* R2 */

    /* Tuple 1: (16, 0, 0, exact, exact) — R3 */
    tuples[1].mask = (struct mask_pattern){
        0xFFFF0000, 0, 0, 0xFFFF, 0xFF};
    tuple_insert(&tuples[1], &rules[2]);

    /* Tuple 2: (12, 12, 0, 0, 0) — R4 */
    tuples[2].mask = (struct mask_pattern){
        0xFFF00000, 0xFFF00000, 0, 0, 0};
    tuple_insert(&tuples[2], &rules[3]);

    /* Tuple 3: (0, 0, 0, exact, exact) — R5 */
    tuples[3].mask = (struct mask_pattern){
        0, 0, 0, 0xFFFF, 0xFF};
    tuple_insert(&tuples[3], &rules[4]);

    /* Tuple 4: (0, 0, 0, 0, 0) — R8 */
    tuples[4].mask = (struct mask_pattern){0, 0, 0, 0, 0};
    tuple_insert(&tuples[4], &rules[5]);

    /* 테스트 패킷 분류 */
    printf("=== 튜플 공간 탐색 분류기 ===\n");
    printf("패킷: src=192.168.1.100 dst=10.1.2.3 sport=49152 dport=80 TCP\n\n");

    struct rule *result = tss_classify(tuples, 5,
        0xC0A80164, 0x0A010203, 49152, 80, 6);

    if (result)
        printf("  결과: %s (우선순위 %d) → %s\n",
               result->name, result->priority,
               result->action ? "ALLOW" : "DROP");

    return 0;
}

/* 실행 결과:
 * === 튜플 공간 탐색 분류기 ===
 * 패킷: src=192.168.1.100 dst=10.1.2.3 sport=49152 dport=80 TCP
 *
 *   [TSS] 5개 튜플 룩업 수행 (vs 선형 탐색 8회 비교)
 *   결과: R1 (우선순위 1) → ALLOW
 *
 * 규칙 10,000개, 튜플 50개인 경우:
 *   선형 탐색: 10,000회 비교 (×5 필드 = 50,000 연산)
 *   TSS:      50회 해시 룩업 (각 O(1) = 50 연산)  → 1,000배 빠름!
 */
초급자 포인트 — 왜 해시 테이블이 빠른가?
해시 테이블은 "키 → 값" 매핑을 O(1)(상수 시간)에 수행합니다. 배열의 인덱싱과 비슷하게, 키를 해시 함수에 넣으면 배열의 어디에 저장되어 있는지 바로 계산됩니다. 선형 탐색이 "하나씩 뒤지는 것"이라면, 해시 테이블은 "바로 그 자리로 이동하는 것"입니다. 튜플 공간 탐색은 이 해시 테이블을 마스크별로 여러 개 만들어 놓고, 각 테이블에서 O(1) 룩업을 수행합니다.

Open vSwitch와 Megaflow Cache

Open vSwitch(OVS)의 megaflow cache는 튜플 공간 탐색의 대표적 실제 적용입니다. OVS는 플로우 테이블을 접두사 길이/마스크 조합별 해시 테이블로 관리하며, 패킷을 순차적으로 각 테이블에서 탐색합니다.

OVS의 3계층 캐시 구조

  1. Microflow Cache (EMC): 정확한 5-tuple 해시 → O(1) 직접 매칭. 가장 빠르지만 용량이 제한적 (보통 8,192 엔트리)
  2. Megaflow Cache (SMC/TSS): 튜플 공간 탐색 기반의 와일드카드 매칭. EMC 미스 시 사용. 마스크별 해시 테이블로 O(T) 룩업
  3. Userspace Flow Table: 커널 캐시 미스 시 유저스페이스 ovs-vswitchd에 upcall. OpenFlow 테이블 전체 평가 후 megaflow 엔트리 설치
Tuple Priority Sorting: OVS는 최근에 매칭에 성공한 튜플을 리스트 앞쪽으로 이동시켜, 다음 패킷에서 먼저 탐색합니다. 네트워크 트래픽의 지역성(locality) 덕에 대부분의 패킷이 첫 1~3개 튜플 내에서 매칭됩니다.
/* 의사코드: 튜플 공간 탐색 */
struct tuple_table {
    mask[d];          /* 각 필드의 접두사 마스크 */
    hash_table;       /* 마스킹된 키 → 규칙 해시 테이블 */
};

function tuple_space_classify(packet, tuples[T]):
    best_match = DEFAULT_RULE
    for each tuple in tuples:
        key = apply_mask(packet, tuple.mask)
        rule = hash_lookup(tuple.hash_table, key)
        if rule != NULL and rule.priority > best_match.priority:
            best_match = rule
    return best_match

Open vSwitch 커널 구현 상세

OVS(Open vSwitch)는 net/openvswitch/ 디렉토리에 커널 모듈을 포함합니다. 튜플 공간 탐색이 어떻게 커널 자료구조로 구현되는지 살펴봅니다.

/* net/openvswitch/flow_table.h — 핵심 자료구조 */

struct sw_flow {
    struct rcu_head         rcu;
    struct {
        struct hlist_node   node[2];  /* table_instance 해시 버킷 */
    } flow_table, ufid_table;
    int                     stats_last_writer;
    struct sw_flow_key      key;      /* 추출된 패킷 키 */
    struct sw_flow_id       id;       /* flow ID (mask + unmasked_key) */
    struct sw_flow_mask     *mask;    /* 해당 마스크 포인터 */
    struct sw_flow_actions __rcu *sf_acts;  /* 액션 배열 */
    struct sw_flow_stats    *stats;   /* per-CPU 통계 */
};

struct table_instance {
    struct flex_array  *buckets;   /* 해시 버킷 배열 */
    unsigned int       n_buckets;  /* 2의 거듭제곱 */
    struct rcu_head    rcu;
    int                node_ver;   /* 버전 (리사이즈 추적) */
    u32                hash_seed;  /* 해시 시드 */
    bool               keep_flows; /* 리사이즈 시 플로우 유지 여부 */
};

struct mask_array {
    struct rcu_head    rcu;
    int                count;      /* 현재 마스크 수 */
    int                max;        /* 최대 마스크 수 */
    struct sw_flow_mask __rcu *masks[];  /* 마스크 배열 */
};
/* net/openvswitch/flow_table.c — ovs_flow_tbl_lookup_stats() */

struct sw_flow *ovs_flow_tbl_lookup_stats(
    struct flow_table *tbl,
    const struct sw_flow_key *key,
    u32 skb_hash,
    u32 *n_mask_hit,
    u32 *n_cache_hit)
{
    struct mask_array *ma = rcu_dereference(tbl->mask_array);
    struct table_instance *ti = rcu_dereference(tbl->ti);
    struct sw_flow_mask *mask;
    struct sw_flow *flow;
    int i;

    /* 1. EMC (Exact Match Cache) 먼저 시도 */
    flow = emc_lookup(&tbl->emc_cache, key);
    if (flow) {
        (*n_cache_hit)++;
        return flow;
    }

    /* 2. SMC (Slow Miss Cache) 시도 */
    flow = smc_lookup(tbl, key);
    if (flow) {
        (*n_cache_hit)++;
        return flow;
    }

    /* 3. 마스크 배열 순회 (Tuple Space Search) */
    for (i = 0; i < ma->max; i++) {
        mask = rcu_dereference(ma->masks[i]);
        if (!mask)
            continue;
        (*n_mask_hit)++;
        /* 패킷 키에 마스크 적용 후 해시 룩업 */
        flow = masked_flow_lookup(ti, key, mask, skb_hash);
        if (flow) {
            /* EMC 업데이트 — 다음 패킷 빠른 경로 */
            emc_insert(&tbl->emc_cache, flow);
            return flow;
        }
    }
    return NULL;   /* 미스 → upcall → 유저스페이스 처리 */
}

메가플로우 캐시 무효화: OVS에서 규칙이 변경되면 관련 megaflow 엔트리가 무효화됩니다. EMC/SMC는 단순히 비워지고(emc_clear()), 마스크 배열은 원자적으로 교체됩니다. 이후 첫 패킷은 upcall 경로를 거쳐 새 megaflow 엔트리를 설치합니다.

OVS 3계층 캐시: EMC → SMC → Datapath → Upcall Open vSwitch의 EMC(Exact Match Cache), SMC(Slow Miss Cache), 커널 datapath 플로우 테이블, 유저스페이스 upcall 순서의 패킷 처리 계층 구조를 보여줍니다. OVS 패킷 분류 계층: EMC → SMC → Datapath → Upcall 패킷 skb ① EMC Exact Match Cache 8K 엔트리, 5-tuple 직접 해시 히트율 ~90%, O(1) HIT MISS ② SMC Slow Miss Cache (65K 슬롯) 마스크 ID → 해시 히트율 ~5%, O(1) HIT MISS ③ Datapath TSS mask_array 순회 각 마스크 → hash lookup 히트율 ~4%, O(T) HIT MISS ④ Upcall → ovs-vswitchd 각 계층 특성 ① EMC (Exact Match Cache) 구조: 배열 기반 고정 크기 해시 (8K) 키: 5-tuple 직접 해시 (마스크 없음) 성능: 20~30ns (L1/L2 캐시 내) ② SMC (Slow Miss Cache) 구조: 65K 슬롯 배열 키: skb 해시 + 마스크 인덱스 성능: 50~80ns (L2/L3 캐시) ③ Datapath TSS (Tuple Space Search) 구조: mask_array → table_instance 키: key & mask → rcu_hash_lookup 성능: 100ns ~ 몇 μs (마스크 수 비례) 마스크 수: 보통 1~20개 ④ Upcall (매우 느림) 경로: kernel → netlink → ovs-vswitchd 성능: 수백 μs ~ 수 ms 빈도: 플로우 첫 패킷만 (이후 캐시) TSS 우선순위 최적화: 자주 히트하는 마스크를 배열 앞으로 이동

튜플 공간 최적화 기법

튜플 병합 (Tuple Merge)

여러 개의 유사한 마스크를 하나의 더 일반적인 마스크로 병합하면 튜플 수 T가 줄어들어 탐색 속도가 향상됩니다. 단, 병합 후 해시 테이블의 버킷당 규칙 수가 증가합니다.

/* 튜플 병합 예시: 두 마스크를 하나로 */
/* 마스크 A: SrcIP/24, DstIP/8, DstPort/exact, Proto/exact */
/* 마스크 B: SrcIP/24, DstIP/8, DstPort/exact (Proto 와일드카드) */

/* 마스크 B는 마스크 A의 슈퍼셋 → 마스크 A를 마스크 B로 병합 가능 */
/* 단, 마스크 B 해시테이블에 Protocol을 무시한 키로 저장해야 함 */
/* → 버킷 충돌 증가 but 마스크 수 감소 */

튜플 우선순위 정렬 (Tuple Priority)

OVS는 최근에 매칭에 성공한 마스크를 배열 앞쪽으로 이동시킵니다. 네트워크 트래픽의 공간적 지역성(spatial locality) 덕분에, 동일한 플로우 타입이 연속으로 도착하면 첫 번째 마스크에서 매칭되어 나머지 마스크 탐색을 생략합니다.

부분 튜플 제거 (Partial Tuple Elimination)

와일드카드 필드가 많은 튜플은 해시 테이블에서 충돌이 많아집니다. 이런 퇴화 튜플(degenerate tuple)을 별도 선형 탐색 목록으로 분리하면, 해시 테이블의 성능을 유지하면서 와일드카드 규칙을 효율적으로 처리할 수 있습니다.

복잡도

지표복잡도비고
분류 시간O(T)T = 고유 튜플 수 (보통 ≪ N)
메모리O(N)규칙별 해시 엔트리 하나, 추가 구조 최소
업데이트O(1)해시 삽입/삭제 — 동적 규칙셋에 이상적
최악 TO(Wd)모든 규칙이 다른 튜플 → T ≈ N (극히 드묾)

RFC 알고리즘 (Recursive Flow Classification)

Gupta & McKeown (1999)이 제안한 RFC(Recursive Flow Classification)는 메모리를 대가로 극도로 빠른 분류 속도를 달성하는 알고리즘입니다. 핵심 아이디어는 다차원 매칭 문제를 여러 단계(phase)의 메모리 룩업으로 분해하여, 각 단계에서 동치 클래스(equivalence class)를 축소하는 것입니다.

핵심 아이디어 — 우체국 비유

우체국 분류 비유: RFC를 대형 우체국의 우편물 분류 시스템으로 이해할 수 있습니다.

Phase 0 (1차 분류대): 우편물이 도착하면 각 분류대에서 하나의 기준만 봅니다. A대는 발신 지역만, B대는 수신 지역만, C대는 우편 종류만 확인합니다. 각 분류대는 미리 준비된 색상 스티커(= eqID)를 붙입니다. "서울"과 "인천"이 같은 규칙들에 해당하면 같은 색 스티커를 붙입니다(= 동치 클래스).

Phase 1 (2차 결합대): 1차에서 붙인 스티커 조합을 봅니다. "발신=빨강 + 수신=파랑"이면 새로운 "보라" 스티커를 붙입니다. 이론적으로 색 조합이 무한할 수 있지만, 실제로는 대부분의 조합이 같은 결과를 내므로 새 스티커 종류가 적습니다.

Phase 2 (최종 결합대): 남은 스티커들을 합쳐 최종 배달함(= 매칭 규칙)을 결정합니다.

핵심 개념 정의

RFC의 핵심 통찰을 정식으로 정의합니다.

동치 클래스 (Equivalence Class)

모든 규칙에 대해 동일한 매칭/비매칭 패턴을 보이는 값들의 집합입니다. 형식적으로, 필드 F에서 두 값 v1과 v2가 동치 클래스를 이루려면:

/* 동치 클래스 정의 */
v₁ ≡ v₂ (필드 F에 대해)
  ⟺ 모든 규칙 Rᵢ에 대해:
      Rᵢ.field_F가 v₁을 매칭하면, Rᵢ.field_F도 v₂를 매칭함 (역도 성립)

/* 비트맵으로 표현하면: */
bitmap(v₁) == bitmap(v₂)
  where bitmap(v)[i] = 1 iff rule Rᵢ matches value v on field F
동치 클래스 개념 시각화 Dst Port 필드에서 65,536개의 가능한 값이 규칙 매칭 패턴(비트맵)이 같은 것끼리 묶여 소수의 동치 클래스로 축소되는 과정을 단계별로 보여줍니다. 동치 클래스: "같은 비트맵 = 같은 그룹" 규칙의 Dst Port 조건 R1: dport = 80 R2: dport = 443 R3: dport = 53 R4: dport = * (와일드카드) R5: dport = 22 R6: dport = 80 R7: dport = * (와일드카드) 각 포트값에 비트맵 계산 포트 → 비트맵 (R1~R7) port=0 → 0001001(R4,R7만) port=21 → 0001001(R4,R7만) port=22 → 0011001(R4,R5,R7) port=23 → 0001001(R4,R7만) port=53 → 0011001(R3,R4,R7) port=80 → 1001101(R1,R4,R6,R7) port=443 → 0101001(R2,R4,R7) 같은 비트맵 → 같은 eqID 동치 클래스 (5개) eqID=0: port 22 (R4,R5,R7) eqID=1: port 53 (R3,R4,R7) eqID=2: port 80 (R1,R4,R6,R7) eqID=3: port 443 (R2,R4,R7) eqID=4: 나머지 (R4,R7만) 왜 축소가 일어나는가? — 포트 공간의 대부분이 "구별 불필요" 전체 포트 공간 (0~65535): 22 53 80 443 ← 나머지 65,531개 포트: 전부 같은 eqID → 축소 결과 (eqID): eqID=0 (22) eqID=1 (53) eqID=2 (80) eqID=3 (443) eqID=4 (나머지) 65,536개 → 5개 eqID (축소율 99.99%) 규칙이 구별하는 값은 4개뿐 (22, 53, 80, 443) 나머지 65,531개는 규칙 관점에서 "완전히 동일" → 하나의 eqID로 충분

예시: 규칙이 포트 80, 443, 22, 53만 구분하고 나머지를 와일드카드로 처리하면, 포트 0~21은 모두 같은 비트맵을 가집니다. 포트 23~52도 모두 같은 비트맵입니다. 이처럼 65,536개 포트 값이 단 5개의 그룹으로 축소됩니다.

eqID (Equivalence Class ID)

각 동치 클래스에 부여하는 정수 식별자입니다. 216개의 가능한 포트 값이 실제로는 수십~수백 개의 eqID로 축소됩니다. eqID는 비트맵의 대리(proxy) 역할을 합니다 — eqID만 알면 어떤 규칙들이 매칭되는지 비트맵 없이도 결정할 수 있습니다.

다단계 축소 (Multi-phase Reduction)

Phase 0에서 각 필드를 독립적으로 eqID로 변환하고, 이후 단계에서 인접 eqID들의 교차곱을 새로운(더 작은) eqID로 축소합니다. 최종 단계의 eqID가 곧 매칭 규칙 번호입니다.

트레이드오프 요약: RFC는 모든 테이블을 사전 계산(precomputation)합니다. 패킷 도착 시에는 단순한 배열 인덱싱만 수행하므로 O(D) 메모리 접근으로 분류가 끝납니다 (D = 단계 수, 보통 3~4). 대가로 테이블이 큰 메모리를 차지하며, 규칙 변경 시 전체 재계산이 필요합니다.
속성RFC의 선택대안
분류 속도극도로 빠름 — 3~4회 메모리 접근선형 탐색: N×d회 비교
메모리많이 사용 — 사전 계산 테이블해시 기반: O(N)
업데이트비쌈 — 전체 재계산해시/튜플: O(1)
최적 시나리오정적 규칙, 초고속 분류 필요동적 규칙: 튜플 공간

Phase 0 — 독립 필드 검색

Phase 0에서는 패킷 헤더의 각 필드를 독립적으로 검색하여 eqID로 변환합니다. 헤더를 청크(chunk)로 나누고, 각 청크에 대해 사전 계산된 테이블(배열)을 인덱싱합니다.

/* Phase 0: 헤더 필드 → eqID 매핑 */

/* 예: 16비트 출발지 포트를 eqID로 변환 */
eqID_srcport = phase0_table_srcport[packet.src_port];
/* 테이블 크기: 2^16 = 65536 엔트리 */
/* 출력: 0 ~ (동치 클래스 수 - 1) */

/* 32비트 IP는 16비트씩 나누어 2개 청크로 처리 */
eqID_srcip_hi = phase0_table_srcip_hi[packet.src_ip >> 16];
eqID_srcip_lo = phase0_table_srcip_lo[packet.src_ip & 0xFFFF];

5-tuple 분류의 경우, 32비트 IP 주소를 16비트씩 나누면 총 7개 청크가 됩니다.

청크필드비트테이블 크기eqID 범위 (예시)
0Src IP [31:16]1665,5360 ~ 47
1Src IP [15:0]1665,5360 ~ 23
2Dst IP [31:16]1665,5360 ~ 52
3Dst IP [15:0]1665,5360 ~ 31
4Src Port1665,5360 ~ 18
5Dst Port1665,5360 ~ 25
6Protocol82560 ~ 5

청크 분할 설계 원리

청크 너비 W는 테이블 크기(2W 엔트리)와 청크 개수(= Phase 수) 사이의 트레이드오프를 결정합니다. 너비가 넓을수록 테이블 하나는 크지만 Phase 수가 줄어 분류가 빠릅니다.

청크 너비 W테이블 크기5-tuple 청크 수Phase 0 총 메모리분류 접근 횟수
8비트256 엔트리13개 (IP 4×2, Port 4, Proto 1)~3 KBPhase 4~5
16비트 (기본)65,536 엔트리7개~448 KBPhase 3
32비트4,294,967,296 엔트리4개실용 불가Phase 2
혼합 (IP:16, Port:16, Proto:8)혼합7개~448 KBPhase 3
실무 선택: 16비트 청크가 사실상 표준입니다. 64KB 테이블은 현대 CPU의 L2 캐시(보통 256KB~1MB)에 올라갈 수 있고, Phase 수도 3으로 관리 가능합니다. 8비트로 내려가면 Phase 수가 늘어 분류 지연이 증가하며, 32비트 IP 전체를 하나의 청크로 처리하면 테이블이 4GB가 되어 실용적이지 않습니다.

접두사가 청크 경계를 넘는 경우

RFC에서 가장 중요한 설계 문제 중 하나는 IP 접두사가 청크 경계를 넘을 때 발생합니다. 32비트 SrcIP를 16비트씩 나누면, /24 접두사(예: 192.168.1.0/24)는 두 청크에 걸칩니다.

Phase 0는 두 청크를 독립적으로 처리하므로 R3의 하위 청크 비트맵은 0x0000~0xFFFF 전체(65,536개 값) 모두에 R3 비트를 설정합니다. 즉, chunk 1만 보면 R3가 과도 매칭(over-match)됩니다. 이 문제는 Phase 1의 AND 연산이 해결합니다.

단계청크 0 (0xC0A8)청크 1 (0x0164 = 1.100)의미
Phase 0 비트맵R1,R2,R3 (모두 192.168.*를 포함)R1,R2 (0x01xx 범위) + R3 (전체 범위, 과도 매칭)독립 계산이라 R3가 chunk 1에서 과도 매칭
Phase 1 AND{R1,R2,R3} AND {R1,R2,R3} = {R1,R2,R3}R3는 두 청크 모두에서 매칭 → AND 후 정상
실제 패킷 1.100{R1,R2,R3} AND {R1,R2} = {R1,R2}AND가 R3를 올바르게 제외 (192.168.0.0/16은 매칭하지만 하위 청크가 R3를 걸러냄)
핵심 통찰: Phase 0가 청크를 독립 처리하면서 생기는 "과도 매칭"을 Phase 1의 비트맵 AND 연산이 정확하게 해소합니다. RFC에 Phase 1이 반드시 필요한 이유가 바로 이것입니다 — Phase 0가 나눠놓은 의존성을 Phase 1이 재결합하는 것입니다.
접두사-청크 경계 문제: /24 접두사가 두 16비트 청크에 걸치는 경우 192.168.1.0/24 접두사가 16비트 청크 경계를 넘을 때, Phase 0에서 과도 매칭이 발생하고 Phase 1의 AND 연산이 이를 해소하는 과정을 보여줍니다. 접두사-청크 경계 문제: /24 접두사 vs 16비트 청크 SrcIP = 192.168.1.100 (R1: 192.168.1.0/24) Chunk 0: SrcIP[31:16] = 0xC0A8 (192.168) — 고정 (비트 31~16) Chunk 1: SrcIP[15:0] = 0x0164 (1.100) — /24이므로 0x0100~0x01FF /24 경계 Chunk 0 비트맵 (값=0xC0A8) R1: 192.168.1.0/24 → 상위=0xC0A8 ✓ R2: 192.168.1.0/24 → 상위=0xC0A8 ✓ R3: 192.168.0.0/16 → 상위=0xC0A8 ✓ R4: 172.16.0.0/12 → 0xAC10~0xAC1F ✗ R5~R8: * → 와일드카드 (일부 ✓) 결과: {R1, R2, R3, R5, R7, R8} 비트맵 Chunk 1 비트맵 (값=0x0164) R1: /24 → 하위 0x0100~0x01FF: 0x0164 ✓ R2: /24 → 하위 0x0100~0x01FF: 0x0164 ✓ R3: /16 → 하위 비트 무관 → 0x0164 ✓ ⚠️과도 R4: /12 → 0xAC10.* → 0x0164 ✗ R5~R8: * → 와일드카드 (일부 ✓) 결과: {R1, R2, R3, R5, R7, R8} — R3 과도 매칭! Phase 1 AND AND 결과: {R1,R2,R3,...} AND {R1,R2,R3,...} = {R1, R2, R3, R5, R7, R8} — 두 청크 모두에서 일치하는 규칙만 ✓ R3가 올바르게 포함됨 (실제 192.168.0.0/16은 이 패킷에 매칭) 다른 패킷 예: SrcIP=192.168.2.100 (chunk1=0x0264, R1/R2 /24 범위 밖) Chunk 0: {R1,R2,R3,...} AND Chunk 1: {R3,...} (R1,R2는 /24 밖이라 제외) = {R3,...} → Phase 1 AND가 R1,R2를 정확히 제외, R3(192.168.0.0/16)만 남김 ✓

Phase 0 비트맵 구축 상세 — SrcIP 상위 청크 추적

8개 규칙에 대해 Chunk 0 (SrcIP[31:16])의 비트맵 계산을 구체적으로 추적합니다. 각 16비트 값에 대해 어떤 규칙이 매칭되는지 계산합니다.

규칙SrcIP 조건Chunk 0 (SrcIP[31:16]) 매칭 범위
R1192.168.1.0/24정확히 0xC0A8 (1개 값)
R2192.168.1.0/24정확히 0xC0A8 (1개 값)
R3192.168.0.0/16정확히 0xC0A8 (1개 값)
R4172.16.0.0/120xAC10~0xAC1F (16개 값, 상위 4비트 고정)
R5* (와일드카드)0x0000~0xFFFF 전체 (65,536개 값)
R610.0.0.0/80x0A00~0x0AFF (256개 값)
R7* (와일드카드)0x0000~0xFFFF 전체 (65,536개 값)
R8* (와일드카드)0x0000~0xFFFF 전체 (65,536개 값)

이 계산의 결과로, Chunk 0 테이블에는 4종류의 비트맵만 나타납니다.

값(범위)매칭 규칙비트맵 (R8R7R6R5R4R3R2R1)eqID
0xC0A8 (192.168)R1,R2,R3,R5,R7,R8110001110
0x0A00~0x0AFF (10.x)R5,R6,R7,R8110100001
0xAC10~0xAC1F (172.16.*)R4,R5,R7,R8110010002
나머지 모든 값R5,R7,R8110000003

65,536개 → 4개 eqID! 실제 규칙이 구별하는 값은 3개 범위뿐이므로(192.168, 10.x, 172.16.*), 나머지 수만 개의 값은 모두 동일한 비트맵을 가집니다.

/* Phase 0 비트맵 구축: 접두사를 값 범위로 전개 */
function build_phase0_bitmap(rules[N], chunk_width, chunk_offset):
    bitmap = array[2^chunk_width] of 0

    for r = 0 to N-1:
        field_cond = rules[r].field_for_chunk(chunk_offset, chunk_width)

        if field_cond.is_wildcard():
            /* 와일드카드: 모든 값에 비트 설정 */
            for v = 0 to 2^chunk_width - 1:
                bitmap[v] |= (1 << r)

        else if field_cond.is_prefix(prefix, prefix_len):
            /* 접두사: 해당 청크에서의 매칭 범위 계산 */
            chunk_bits = min(prefix_len - chunk_offset, chunk_width)
            if chunk_bits <= 0:
                /* 이 청크는 완전히 와일드카드 */
                for v = 0 to 2^chunk_width - 1:
                    bitmap[v] |= (1 << r)
            else:
                /* 접두사의 해당 청크 비트만 고정, 나머지는 와일드카드 */
                base = (prefix >> chunk_offset) & ((1 << chunk_width) - 1)
                base = base & ~((1 << (chunk_width - chunk_bits)) - 1)
                count = 1 << (chunk_width - chunk_bits)
                for v = base to base + count - 1:
                    bitmap[v] |= (1 << r)

    return bitmap

범위 필드의 비트맵 계산 — 포트 범위

IP 주소는 접두사(prefix) 매칭을 사용하지만, 포트 필드는 범위(range) 매칭을 사용합니다. RFC는 임의 범위를 자연스럽게 처리하는데, 이는 TCAM 대비 큰 장점입니다.

/* SrcPort 필드의 비트맵 계산 (예시 규칙셋) */
/* R1~R5: SrcPort = * (와일드카드) → 모든 값에서 매칭 */
/* R6: SrcPort = 1024-65535 (범위 매칭) */
/* R7~R8: SrcPort = * (와일드카드) → 모든 값에서 매칭 */

port=0    → bitmap = 11011111 /* R6 제외, 나머지 모두 */  → eqID = 0
port=1023 → bitmap = 11011111 /* 동일 */                  → eqID = 0
port=1024 → bitmap = 11111111 /* R6 포함, 전부 매칭 */    → eqID = 1
port=65535→ bitmap = 11111111 /* 동일 */                  → eqID = 1

/* 결과: 65,536개 → 2개 eqID */
/* 범위 경계(1024)가 eqID 경계를 생성 */
/* 포트 0~1023: eqID=0 (R6 비매칭) */
/* 포트 1024~65535: eqID=1 (R6 매칭) */

범위 경계(1024)가 정확히 하나의 추가 eqID 분할을 만듭니다. 포트 0~1023은 모두 동일한 비트맵, 포트 1024~65535도 모두 동일한 비트맵입니다. 반면 정확한 포트 값 매칭(예: dport=80)은 해당 포트에만 비트가 설정되어 별도 eqID를 만듭니다.

RFC의 범위 처리 우위: TCAM에서 임의 포트 범위(예: 1024-65535)는 접두사 분해가 필요합니다(최대 30개의 접두사 엔트리). RFC는 범위 경계마다 eqID 하나만 추가하면 됩니다. 범위가 많은 방화벽 규칙셋에서 RFC의 eqID 수가 예상보다 훨씬 작게 유지되는 이유입니다.

핵심은 동치 클래스 축소입니다. 65,536개의 가능한 포트 값이 실제 규칙셋에서는 보통 수십 개의 동치 클래스로 축소됩니다. 예를 들어, 규칙이 포트 80, 443, 1024-65535만 구분하면, 모든 포트는 4개 eqID(80, 443, 1024-65535, 나머지)로 분류됩니다.

RFC Phase 0 — 독립 필드 검색 패킷 헤더의 7개 청크가 각각 독립적인 사전 계산 테이블을 통해 eqID로 변환되는 과정을 보여줍니다. RFC Phase 0: 독립 필드 → eqID 변환 패킷 헤더 SrcIP[31:16] SrcIP[15:0] DstIP[31:16] DstIP[15:0] Src Port Dst Port Protocol Table 0 65,536 → eqID Table 1 65,536 → eqID Table 2 65,536 → eqID Table 3 65,536 → eqID Table 4 65,536 → eqID Table 5 65,536 → eqID Table 6 256 → eqID eqID 출력 eqID₀ = 12 eqID₁ = 3 eqID₂ = 27 eqID₃ = 8 eqID₄ = 2 eqID₅ = 15 eqID₆ = 1 축소: 65K+ → 수십 개 → Phase 1로 전달 각 테이블은 사전 계산된 배열 — 패킷 도착 시 단순 인덱싱(O(1))만 수행 동치 클래스 축소가 RFC의 핵심: 입력 공간 >> eqID 공간

Phase 1~N — 교차곱 축소

Phase 0에서 7개의 eqID를 얻었다면, 이제 인접한 eqID 쌍을 결합하여 새로운 eqID로 축소합니다. 이 과정을 여러 단계에 걸쳐 반복하면, 최종적으로 하나의 classID(매칭 규칙 번호)가 남습니다.

/* Phase 1: 인접 eqID 결합 */

/* eqID₀(48종) × eqID₁(24종) → 새로운 eqID (실제로는 ≪ 48×24) */
combined_01 = phase1_table_A[eqID₀ * num_eqID₁ + eqID₁];

/* 이론적 교차곱: 48 × 24 = 1,152 조합 */
/* 실제 동치 클래스: ~60개 (규칙셋 특성에 의한 축소) */

이 축소가 가능한 이유는, 교차곱의 많은 조합이 모든 규칙에 대해 동일한 매칭 결과를 보이기 때문입니다. 예를 들어, (eqID₀=12, eqID₁=3)(eqID₀=12, eqID₁=7)이 같은 규칙들에 매칭된다면, 이 두 조합은 같은 새 eqID를 받습니다.

Phase 1 교차곱 축소 — AND 연산 Phase 0에서 얻은 두 eqID의 비트맵을 AND 연산하여 새로운 동치 클래스를 생성하는 교차곱 축소 과정을 보여줍니다. Phase 1: 교차곱 축소 — 비트맵 AND 연산 Dst Port eqID × Protocol eqID → 새로운 eqID (AND로 결합) DstPort eqID 비트맵 eqID=2 (port 80): 1001101₁ R1,R4,R6,R7 eqID=0 (port 22): 0011001 R4,R5,R7 eqID=4 (나머지): 0001001 R4,R7 ... Protocol eqID 비트맵 eqID=1 (TCP): 1101011 R1,R2,R4,R5,R6 AND 연산 예시 DstPort eqID=2 (80) × Protocol eqID=1 (TCP) 1001101← DstPort bitmap (R1,R4,R6,R7) & 1101011← Protocol bitmap (R1,R2,R4,R5,R6) = 1001001→ R1, R4, R6 매칭 → 새 eqID=B DstPort eqID=0 (22) × Protocol eqID=1 (TCP) 0011001← (R4,R5,R7) & 1101011← (R1,R2,R4,R5,R6) = 0001001→ R4, R5 매칭 → 새 eqID=A DstPort eqID=4 (나머지) × Protocol eqID=1 (TCP) 0001001 & 1101011 = 0001001→ R4만 → 새 eqID=F 교차곱 축소 결과 이론적 조합: 5 × 4 = 20 실제 고유 비트맵: 7개 → 축소율 65% eqID A: R4,R5 eqID B: R1,R4,R6 eqID C: R2,R4 eqID D: R3,R4 eqID E: R4,R7 eqID F: R4 eqID G: R4,R7 (with R8) 핵심: AND 연산으로 "두 필드를 동시에 만족하는 규칙"만 남김 → 결과 비트맵이 같은 조합들은 같은 새 eqID를 받음 → 교차곱 축소 달성

결합 그룹 선택 전략 (Phase Topology)

Phase 0에서 7개의 eqID를 얻은 후, 어떤 eqID를 어떤 순서로 결합할지 결정하는 것이 위상(topology) 선택입니다. 이 선택은 Phase 1 테이블 크기와 총 메모리에 영향을 주지만, 분류 속도(Phase 수)는 바뀌지 않습니다.

전략결합 방식장단점
의미론적 근접 (Gupta 기본)(SrcIP_hi, SrcIP_lo), (DstIP_hi, DstIP_lo), (SrcPort, DstPort, Proto)동일 필드끼리 결합 → 높은 상관관계로 교차곱 축소 잘 됨
균형 이진 트리eqID 수가 비슷한 쌍을 찾아 결합테이블 크기 균형화 → 캐시 활용 균등화
탐욕적 (Huffman형)항상 가장 작은 두 eqID 집합을 먼저 결합Phase 1 개별 테이블 최소화, 최종 테이블 클 수 있음

실제 구현에서는 대부분 의미론적 근접 전략을 사용합니다. IP 주소의 상위/하위 청크는 강한 상관관계(같은 서브넷)가 있어, 교차곱이 이론값(48×24=1,152)의 5% 수준(~60개)으로 축소됩니다.

완전 교차곱 행렬 — DstPort × Protocol (20개 전체)

5×4=20개의 모든 조합에 대해 AND 연산 결과를 완전히 전개합니다. 비트맵 표기: R8R7R6R5R4R3R2R1 (MSB=R8, LSB=R1).

DstPort ╲ ProtocoleqID=0 (ICMP)eqID=1 (TCP)eqID=2 (UDP)eqID=3 (나머지)
eqID=0 (port 22)
bitmap: 00011000
00011000 AND 11001000
= 00001000 → R4
F
00011000 AND 10110011
= 00010000 → R5
A
00011000 AND 10001100
= 00001000 → R4
F
00011000 AND 10001000
= 00001000 → R4
F
eqID=1 (port 53)
bitmap: 00001100
00001100 AND 11001000
= 00001000 → R4
F
00001100 AND 10110011
= 00000000 → 없음
null
00001100 AND 10001100
= 00000100 → R3
D
00001100 AND 10001000
= 00001000 → R4
F
eqID=2 (port 80)
bitmap: 10101001
10101001 AND 11001000
= 10001000 → R4,R8
G
10101001 AND 10110011
= 10100001 → R1,R6,R8
B
10101001 AND 10001100
= 10001000 → R4,R8
G
10101001 AND 10001000
= 10001000 → R4,R8
G
eqID=3 (port 443)
bitmap: 10001010
10001010 AND 11001000
= 10001000 → R4,R8
G
10001010 AND 10110011
= 10000010 → R2,R8
C
10001010 AND 10001100
= 10001000 → R4,R8
G
10001010 AND 10001000
= 10001000 → R4,R8
G
eqID=4 (나머지)
bitmap: 10001000
10001000 AND 11001000
= 10001000 → R4,R8
G (→ E 변형)
10001000 AND 10110011
= 10000000 → R8
H
10001000 AND 10001100
= 10001000 → R4,R8
G
10001000 AND 10001000
= 10001000 → R4,R8
G

20개 중 null(port=53, TCP는 어떤 규칙도 매칭 안 함: R3는 UDP 전용, R1,R2,R5,R6는 다른 포트)은 기본 규칙으로 처리됩니다. 나머지 19개 유효 조합이 A, B, C, D, F, G, H 7개 eqID로 축소됩니다.

DstPort × Protocol 교차곱 행렬 — 20개 조합 격자 5개 DstPort eqID와 4개 Protocol eqID의 교차곱 20개 조합이 7개 eqID로 축소되는 과정을 색상 코딩된 격자로 표시합니다. 교차곱 행렬: DstPort(5) × Protocol(4) → 7개 eqID ICMP (eqID=0) TCP (eqID=1) UDP (eqID=2) 기타 (eqID=3) port 22 (eqID=0) port 53 (eqID=1) port 80 (eqID=2) port 443 (eqID=3) 나머지 (eqID=4) F R4 A R5 F R4 F R4 F R4 null 매칭 없음 D R3 F R4 G R4,R8 B R1,R6,R8 G R4,R8 G R4,R8 G R4,R8 C R2,R8 G R4,R8 G R4,R8 G R4,R8 H R8만 G R4,R8 G R4,R8 범례 A: R5 (SSH) B: R1,R6,R8 C: R2,R8 (HTTPS) D: R3 (DNS/UDP) F: R4 (기본 허용) G: R4,R8 (복합) H: R8만 (기본) null: 매칭 없음 20개 조합 → 7개 eqID 축소 (65% 축소)

교차곱 축소의 정당성

비트맵 AND 연산이 "두 필드를 동시에 만족하는 규칙"을 올바르게 찾아냄을 형식적으로 확인할 수 있습니다.

두 (eqID_A, eqID_B) 조합의 AND 결과 비트맵이 동일하면, 그 두 조합에 대해 이후 모든 단계의 분류 결과가 같습니다. 따라서 같은 비트맵 → 같은 새 eqID 배정이 정당합니다. 이것이 RFC 교차곱 축소의 수학적 근거입니다.

전형적인 3단계 구성 (5-tuple)

단계입력결합출력 eqID 수 (예시)
Phase 0패킷 헤더 7개 청크개별 필드 → eqID48, 24, 52, 31, 18, 25, 5
Phase 1Phase 0의 7개 eqID인접 쌍 결합: (0,1), (2,3), (4,5,6)60, 83, 35
Phase 2Phase 1의 3개 eqID전체 결합: (0,1,2)N (규칙 수 이하)

Phase 2 — 최종 분류 상세

Phase 2는 Phase 1에서 나온 3개의 eqID(eqID_A: SrcIP, eqID_B: DstIP, eqID_C: Port+Proto)를 결합하여 최종 classID를 결정합니다. Phase 2 테이블의 각 엔트리는 해당 조합과 매칭되는 최고 우선순위 규칙 번호입니다.

우선순위 해소(priority resolution) 방법: 사전 계산 시 결합 비트맵에서 가장 낮은 인덱스(=가장 높은 우선순위)의 비트를 찾아 classID로 저장합니다.

/* Phase 2: classID = 최고 우선순위 규칙 (결합 비트맵의 첫 번째 1비트) */
function resolve_priority(combined_bitmap, N):
    for r = 0 to N-1:   /* 규칙 0이 최고 우선순위 */
        if combined_bitmap & (1 << r):
            return r   /* 첫 번째 매칭 규칙 = 최고 우선순위 */
    return DEFAULT_RULE  /* 비트맵=0: 매칭 규칙 없음 */

/* 예시: combined_bitmap = 00100001 (R1, R6) */
/* → r=0(R1) 먼저 검사 → 비트 1 → classID = R1 */
/* R1이 R6보다 우선순위가 높으므로 R6는 무시 */

/* Phase 2 테이블 인덱싱 */
classID = phase2[eqID_A * EB * EC + eqID_B * EC + eqID_C]
/* 테이블 크기: 60 × 83 × 35 = 174,300 엔트리 */
/* 각 엔트리: 1~2 bytes (classID = 규칙 번호) */
/* 총 메모리: 약 170~340 KB */

전체 5-tuple RFC의 메모리 산정

예시 규칙셋(1,000개 규칙 수준)에 대한 RFC 전체 메모리 소요를 단계별로 산출합니다.

단계테이블 내용크기메모리
Phase 0 (×7 청크)65,536 엔트리/테이블 (eqID, 1바이트)7 × 64KB~448 KB
Phase 1 A (SrcIP)48×24 = 1,152 엔트리 (eqID, 1바이트)1.1 KB~1 KB
Phase 1 B (DstIP)52×31 = 1,612 엔트리1.6 KB~2 KB
Phase 1 C (Port+Proto)18×25×5 = 2,250 엔트리2.2 KB~2 KB
Phase 2 (최종)60×83×35 = 174,300 엔트리 (2바이트)~340 KB~340 KB
합계~793 KB ≈ 800 KB
실제 스케일: 실제 방화벽의 1,000개 규칙 ACL은 보통 2~8 MB를 사용합니다(더 많은 IP 프리픽스, 더 많은 포트 범위로 인해 eqID가 늘어남). 10,000개 규칙의 경우 Phase 2 테이블이 Phase 1의 eqID 수에 따라 수십~수백 MB까지 성장할 수 있습니다. 이것이 RFC 메모리 최적화 기법이 필요한 이유입니다.
RFC 전체 파이프라인 RFC 알고리즘의 3단계(Phase 0 → Phase 1 → Phase 2) 파이프라인 전체 흐름을 보여줍니다. 7개 청크에서 시작하여 단계별로 동치 클래스를 축소하여 최종 classID를 산출합니다. RFC 3단계 파이프라인 (Recursive Flow Classification) Phase 0 SrcIP[31:16] 65K → 48 eqIDs SrcIP[15:0] 65K → 24 eqIDs DstIP[31:16] 65K → 52 eqIDs DstIP[15:0] 65K → 31 eqIDs Src Port 65K → 18 eqIDs Dst Port 65K → 25 eqIDs Protocol 256 → 5 eqIDs Phase 1 (교차곱 축소) 결합 A: Src IP 48 × 24 → 60 eqIDs (이론 1,152 → 실제 60) 결합 B: Dst IP 52 × 31 → 83 eqIDs (이론 1,612 → 실제 83) 결합 C: Port+Proto 18 × 25 × 5 → 35 eqIDs (이론 2,250 → 실제 35) Phase 2 (최종 분류) 최종 결합 60 × 83 × 35 classID (= 매칭 규칙 번호) 규칙 Rk → 액션 적용 성능: 총 메모리 접근 = Phase 수 = 3 (Phase 0에서 7회 병렬 가능) 핵심: 동치 클래스 축소로 교차곱 크기를 관리 가능하게 유지 트레이드오프: 모든 테이블 사전 계산 필요 → 규칙 변경 시 전체 재계산

사전 계산 과정

RFC 테이블의 사전 계산은 규칙 집합이 변경될 때 수행됩니다. 핵심은 각 필드 값에 대해 어떤 규칙들이 매칭되는지(class bitmap)를 계산하고, 동일한 비트맵을 가진 값들을 같은 eqID로 묶는 것입니다.

/* RFC 사전 계산 알고리즘 — Phase 0 */
function precompute_phase0(field_index, rules[N]):
    /* 1. 각 입력 값에 대해 매칭 규칙 비트맵 계산 */
    for value = 0 to (2^field_bits - 1):
        bitmap[value] = 0
        for r = 0 to N-1:
            if rules[r].field[field_index] matches value:
                bitmap[value] |= (1 << r)

    /* 2. 동일한 비트맵을 가진 값들을 같은 eqID로 매핑 */
    eqID_map = {}  /* bitmap → eqID 해시맵 */
    next_eqID = 0
    for value = 0 to (2^field_bits - 1):
        if bitmap[value] not in eqID_map:
            eqID_map[bitmap[value]] = next_eqID++
        table[value] = eqID_map[bitmap[value]]

    return table, eqID_count = next_eqID

/* RFC 사전 계산 알고리즘 — Phase k (k ≥ 1) */
function precompute_phase_k(eqIDs_A[], eqIDs_B[], bitmaps_A[], bitmaps_B[]):
    eqID_map = {}
    next_eqID = 0
    for a = 0 to |eqIDs_A| - 1:
        for b = 0 to |eqIDs_B| - 1:
            combined_bitmap = bitmaps_A[a] & bitmaps_B[b]
            if combined_bitmap == 0: continue  /* 매칭 규칙 없음 */
            if combined_bitmap not in eqID_map:
                eqID_map[combined_bitmap] = next_eqID++
            table[a * |eqIDs_B| + b] = eqID_map[combined_bitmap]

    return table, eqID_count = next_eqID
사전 계산 비용: Phase 0의 비트맵 계산은 O(2W · N)이고, 축소 단계의 교차곱은 최악 O(eqIDA × eqIDB × N)입니다. 규칙이 수천 개이면 초기 구축에 수 초~수십 초가 걸릴 수 있으며, 규칙 변경 시 전체 재계산이 필요합니다. 따라서 RFC는 정적이거나 드물게 변경되는 규칙셋에 가장 적합합니다.

사전 계산 단계별 시간 복잡도 분석

각 단계별 실제 연산량을 구체적인 수치로 분석합니다.

규칙 수Phase 0Phase 1Phase 2총합(추정)
100~50 ms<1 ms~5 ms~60 ms
1,000~500 ms~5 ms~50 ms~600 ms
10,000~5 sec~50 ms~500 ms~6 sec
100,000~50 sec~500 ms~5 sec~60 sec

사전 계산 최적화 기법

복잡도 분석

지표복잡도설명
분류 시간O(D) 메모리 접근D = 단계 수 (보통 3~4). Phase 0의 청크 접근은 병렬 가능
메모리 (최악)O(Nd)동치 클래스가 축소되지 않으면 교차곱이 폭발 (이론적 최악)
메모리 (실제)수 MB ~ 수십 MB실제 규칙셋에서는 동치 클래스 축소가 극적으로 발생
사전 계산O(2W·N + 교차곱)규칙 변경 시 전체 재계산 (증분 업데이트 불가)
업데이트전체 재계산정적 규칙셋에 최적, 동적 환경에는 부적합
왜 실제로 동작하는가? 이론적으로 교차곱은 지수적으로 폭발할 수 있지만, 실제 ACL/방화벽 규칙셋에서는 규칙 간의 상관관계로 인해 동치 클래스가 극적으로 축소됩니다. Gupta & McKeown의 실험에서 1,000개 규칙의 실제 ACL에 대해 Phase 1의 교차곱이 이론값의 5~10%로 축소되었습니다. 이 "구조적 축소(structural compression)"가 RFC를 실용적으로 만드는 핵심입니다.

RFC 전체 추적 예시

예시 규칙 집합(R1~R8)에 대해 RFC의 동작을 구체적인 수치로 추적합니다. 간결함을 위해 Dst Port와 Protocol 두 필드만으로 축소한 2차원 예시입니다.

예시: Dst Port × Protocol (2차원 축소)

8개 규칙의 Dst Port와 Protocol 필드만 추출합니다.

규칙Dst PortProtocol
R180TCP(6)
R2443TCP(6)
R353UDP(17)
R4**
R522TCP(6)
R680TCP(6)
R7*ICMP(1)
R8**

Phase 0: Dst Port의 동치 클래스

65,536개 포트 값의 비트맵을 계산하고 동치 클래스로 축소합니다.

포트 값매칭 규칙 비트맵eqID
2200011000 (R4, R5, R8)0
5300001100 (R3, R4, R8)1
8010101001 (R1, R4, R6, R8)2
44310001010 (R2, R4, R8)3
나머지 (0-21, 23-52, ...)10001000 (R4, R8)4

65,536개 → 5개 eqID로 축소! 포트 0~21, 23~52, 54~79, 81~442, 444~65535는 모두 같은 비트맵(R4, R8만 매칭)이므로 하나의 eqID로 통합됩니다.

Phase 0: Protocol의 동치 클래스

프로토콜매칭 규칙 비트맵eqID
ICMP (1)11001000 (R4, R7, R8)0
TCP (6)10110011 (R1, R2, R4, R5, R6, R8)1
UDP (17)10001100 (R3, R4, R8)2
나머지 (0, 2-5, 7-16, 18-255)10001000 (R4, R8)3

256개 → 4개 eqID로 축소!

Phase 1: 교차곱 축소

이론적 교차곱: 5 × 4 = 20 조합. 각 조합의 비트맵을 AND 연산합니다.

DstPort eqIDProto eqID비트맵 AND 결과매칭 규칙최종 eqID
0 (port=22)1 (TCP)00010000R5A
2 (port=80)1 (TCP)00100001R1, R6B
3 (port=443)1 (TCP)00000010R2C
1 (port=53)2 (UDP)00000100R3D
4 (나머지)0 (ICMP)01000000R7E
4 (나머지)1 (TCP)00001000R4 (via R8)F
4 (나머지)3 (나머지)10001000R4, R8G
... 나머지 13개 조합도 위 7개 중 하나로 매핑 ...

20개 조합 → 7개 동치 클래스로 축소! 테스트 패킷 (DstPort=80, Proto=TCP): Phase 0에서 DstPort eqID=2, Proto eqID=1 → Phase 1 테이블에서 (2,1)을 조회하면 eqID=B → 최우선 매칭 규칙은 R1.

2D RFC 예시: 테스트 패킷 분류 전 과정 2차원 RFC(DstPort × Protocol)에서 테스트 패킷(port=80, proto=TCP)이 Phase 0과 Phase 1을 거쳐 R1으로 분류되는 전 과정을 한눈에 보여줍니다. 2D RFC 분류 전 과정: 패킷 (DstPort=80, Proto=TCP) → R1 테스트 패킷 port=80, TCP Phase 0: 독립 필드 룩업 phase0_dport[ 80 ] → eqID = 2 (R1,R4,R6,R8) phase0_proto[ TCP=6 ] → eqID = 1 (R1,R2,R4,R5,R6,R8) eqID (2, 1) Phase 1: 교차곱 테이블 룩업 phase1_table[ eqID_dp=2 × 4 + eqID_pr=1 ] = index 9 eqID = B bitmap: R1,R6 R1 (ALLOW) 최우선 매칭 규칙 분류에 소요된 총 연산 Phase 0: 메모리 접근 2회 phase0_dport[80], phase0_proto[6] Phase 1: 메모리 접근 1회 phase1_table[9] 총 3회 메모리 접근으로 완료! (Phase 0 병렬 시 실질 2회) 선형 탐색이었다면: 8 규칙 × 2 필드 = 16회 비교 필요 RFC는 규칙이 10,000개로 늘어도 여전히 3회 메모리 접근 이것이 RFC의 핵심 가치: O(N) → O(D) (D=Phase 수, 보통 2~4)

전체 5-tuple RFC 분류 추적

이제 실제 5-tuple로 테스트 패킷 (Src=192.168.1.100, Dst=10.1.2.3, SrcPort=49152, DstPort=80, Proto=TCP)의 RFC 분류 과정을 추적합니다.

/* ═══════════════════════════════════════════════════════════ */
/* Phase 0: 7개 청크 → 7개 eqID (1회 병렬 메모리 접근)       */
/* ═══════════════════════════════════════════════════════════ */

/* Chunk 0: SrcIP[31:16] = 0xC0A8 (192.168) */
eqID₀ = phase0[0][0xC0A8]  → eqID = 12
  /* bitmap: R1,R2,R3 매칭 (192.168.x.x는 R1,R2,R3의 SrcIP에 포함) */

/* Chunk 1: SrcIP[15:0] = 0x0164 (1.100) */
eqID₁ = phase0[1][0x0164]  → eqID = 3
  /* bitmap: R1,R2에만 매칭 (/24이므로 1.x는 매칭, /16은 모두 매칭) */

/* Chunk 2: DstIP[31:16] = 0x0A01 (10.1) */
eqID₂ = phase0[2][0x0A01]  → eqID = 27
  /* bitmap: R1,R2 매칭 (10.x.x.x는 R1,R2의 DstIP /8에 포함) */

/* Chunk 3: DstIP[15:0] = 0x0203 (2.3) */
eqID₃ = phase0[3][0x0203]  → eqID = 8

/* Chunk 4: SrcPort = 49152 (0xC000) */
eqID₄ = phase0[4][0xC000]  → eqID = 2
  /* bitmap: R6 매칭 (1024-65535 범위) + R4,R8 (와일드카드) */

/* Chunk 5: DstPort = 80 (0x0050) */
eqID₅ = phase0[5][0x0050]  → eqID = 15
  /* bitmap: R1,R6 매칭 (port 80) + R4,R8 (와일드카드) */

/* Chunk 6: Protocol = 6 (TCP) */
eqID₆ = phase0[6][6]       → eqID = 1
  /* bitmap: R1,R2,R5,R6 매칭 (TCP) + R4,R8 (와일드카드) */

/* ═══════════════════════════════════════════════════════════ */
/* Phase 1: 3개 결합 (1회 메모리 접근)                        */
/* ═══════════════════════════════════════════════════════════ */

/* 결합 A: SrcIP 청크 결합 */
combined_A = phase1[A][eqID₀ * E₁ + eqID₁]   /* [12 * 24 + 3] */
           = phase1[A][291]  → eqID_A = 7
  /* SrcIP 전체로 보면: R1,R2 매칭 (192.168.1.0/24) */

/* 결합 B: DstIP 청크 결합 */
combined_B = phase1[B][eqID₂ * E₃ + eqID₃]   /* [27 * 32 + 8] */
           = phase1[B][872]  → eqID_B = 19
  /* DstIP 전체: R1,R2 매칭 (10.0.0.0/8) */

/* 결합 C: Port + Protocol 결합 */
combined_C = phase1[C][eqID₄ * E₅ * E₆ + eqID₅ * E₆ + eqID₆]
           → eqID_C = 5
  /* SrcPort=49152, DstPort=80, TCP: R1,R6 매칭 */

/* ═══════════════════════════════════════════════════════════ */
/* Phase 2: 최종 결합 (1회 메모리 접근)                       */
/* ═══════════════════════════════════════════════════════════ */

classID = phase2[eqID_A * EB * EC + eqID_B * EC + eqID_C]
        = phase2[7 * 83 * 35 + 19 * 35 + 5]
        → classID = R1 (ALLOW)

/* R1과 R8이 모두 매칭되지만, classID는 최우선 규칙 R1을 가리킴 */
/* 총 메모리 접근: Phase 0(7회 병렬) + Phase 1(3회 병렬) + Phase 2(1회) */
/* = 실질적으로 3회 순차 메모리 접근으로 분류 완료! */
속도 비교: 선형 탐색은 이 패킷에 대해 8규칙 × 5필드 = 40회 비교가 필요했습니다. RFC는 실질 3회 메모리 접근으로 동일한 결과를 얻습니다. 규칙이 10,000개로 늘어나도 RFC의 접근 횟수는 여전히 3회입니다. 이것이 RFC가 하드웨어 분류기의 기반이 되는 이유입니다.
RFC 비트맵 기반 동치 클래스 축소 Dst Port 필드에서 65,536개 값이 비트맵 비교를 통해 5개 동치 클래스로 축소되는 과정을 보여줍니다. RFC 동치 클래스 축소: Dst Port 필드 (65,536 → 5 eqIDs) 포트 값 (0~65535) port = 0 → bitmap = 10001000 port = 1 → bitmap = 10001000 ... port = 22 → bitmap = 00011000 port = 23 → bitmap = 10001000 ... port = 53 → bitmap = 00001100 ... port = 80 → bitmap = 10101001 ... port = 443 → bitmap = 10001010 port = 444~65535 → 10001000 같은 bitmap → 같은 eqID 동치 클래스 (5개) eqID=0: port 22 (R4,R5,R8) eqID=1: port 53 (R3,R4,R8) eqID=2: port 80 (R1,R4,R6,R8) eqID=3: port 443 (R2,R4,R8) eqID=4: 나머지 (R4,R8) 축소 통계 입력: 65,536 값 고유 비트맵: 5개 축소율: 99.99% 테이블: 65,536 × 1B = 64KB 65,531개 포트(0-21, 23-52, ...)가 모두 같은 비트맵 → 하나의 eqID 이것이 RFC가 실용적인 핵심 이유: 실제 규칙셋은 전체 공간의 극히 일부만 구별함

완전한 C 구현 — 2차원 RFC 분류기

아래는 RFC 알고리즘의 핵심 아이디어를 Dst Port × Protocol 2차원으로 구현한 완전한 C 코드입니다. Phase 0(독립 필드 검색)과 Phase 1(교차곱 축소)의 전체 과정을 단계별로 추적할 수 있습니다.

/* rfc_classifier.c — 2차원 RFC 패킷 분류기
 *
 * Phase 0: Dst Port(65536값) → eqID, Protocol(256값) → eqID
 * Phase 1: (eqID_port × eqID_proto) → 최종 규칙
 *
 * 컴파일: gcc -O2 -o rfc_classifier rfc_classifier.c
 * 실행:   ./rfc_classifier
 */
#include <stdio.h>
#include <stdint.h>
#include <string.h>

#define MAX_RULES     16
#define PORT_SPACE    65536  /* 16비트 포트: 0 ~ 65535 */
#define PROTO_SPACE   256    /* 8비트 프로토콜: 0 ~ 255 */
#define MAX_EQID      64    /* 동치 클래스 최대 수 */

/* ── 규칙 구조체 ── */
struct rule {
    char     name[8];
    int      priority;
    uint16_t dport;        /* 0xFFFF = 와일드카드 */
    uint8_t  proto;        /* 0 = 와일드카드 */
    int      action;       /* 1=ALLOW, 0=DROP */
};

/* ── Phase 0 테이블 ── */
static uint8_t phase0_dport[PORT_SPACE];    /* 포트 → eqID */
static uint8_t phase0_proto[PROTO_SPACE];   /* 프로토콜 → eqID */
static int num_dport_eqid, num_proto_eqid;

/* ── Phase 1 테이블 ── */
static int phase1_table[MAX_EQID][MAX_EQID]; /* → 매칭 규칙 인덱스 */

/* ── 비트맵: 각 값에 대해 어떤 규칙이 매칭되는지 ── */
typedef uint16_t bitmap_t;  /* 최대 16개 규칙 지원 */

/* ── Phase 0 사전 계산: 입력값 → 비트맵 → eqID ── */
static int precompute_phase0_dport(
    const struct rule rules[], int nr)
{
    /* Step 1: 각 포트 값에 대해 매칭 규칙 비트맵 계산 */
    bitmap_t bitmaps[PORT_SPACE];
    for (int port = 0; port < PORT_SPACE; port++) {
        bitmaps[port] = 0;
        for (int r = 0; r < nr; r++) {
            if (rules[r].dport == 0xFFFF || rules[r].dport == port)
                bitmaps[port] |= (1 << r);
        }
    }

    /* Step 2: 동일 비트맵 → 동일 eqID 매핑 */
    bitmap_t unique[MAX_EQID];
    int n_eq = 0;
    for (int port = 0; port < PORT_SPACE; port++) {
        int found = -1;
        for (int e = 0; e < n_eq; e++) {
            if (unique[e] == bitmaps[port]) { found = e; break; }
        }
        if (found < 0) {
            found = n_eq;
            unique[n_eq++] = bitmaps[port];
        }
        phase0_dport[port] = found;
    }

    printf("  Phase 0 DstPort: %d개 포트 → %d개 eqID (축소율 %.2f%%)\n",
           PORT_SPACE, n_eq, (1.0 - (double)n_eq / PORT_SPACE) * 100);
    for (int e = 0; e < n_eq; e++) {
        printf("    eqID=%d: bitmap=%04X → 규칙: ", e, unique[e]);
        for (int r = 0; r < nr; r++)
            if (unique[e] & (1 << r)) printf("%s ", rules[r].name);
        printf("\n");
    }
    return n_eq;
}

static int precompute_phase0_proto(
    const struct rule rules[], int nr)
{
    bitmap_t bitmaps[PROTO_SPACE];
    for (int p = 0; p < PROTO_SPACE; p++) {
        bitmaps[p] = 0;
        for (int r = 0; r < nr; r++) {
            if (rules[r].proto == 0 || rules[r].proto == p)
                bitmaps[p] |= (1 << r);
        }
    }

    bitmap_t unique[MAX_EQID];
    int n_eq = 0;
    for (int p = 0; p < PROTO_SPACE; p++) {
        int found = -1;
        for (int e = 0; e < n_eq; e++) {
            if (unique[e] == bitmaps[p]) { found = e; break; }
        }
        if (found < 0) {
            found = n_eq;
            unique[n_eq++] = bitmaps[p];
        }
        phase0_proto[p] = found;
    }

    printf("  Phase 0 Protocol: %d개 값 → %d개 eqID (축소율 %.2f%%)\n",
           PROTO_SPACE, n_eq, (1.0 - (double)n_eq / PROTO_SPACE) * 100);
    return n_eq;
}

/* ── Phase 1 사전 계산: eqID 교차곱 → 최종 규칙 ── */
static void precompute_phase1(
    const struct rule rules[], int nr,
    int n_dport_eq, int n_proto_eq)
{
    /* 각 (eqID_dport, eqID_proto) 조합에 대해
     * 비트맵 AND → 최우선 매칭 규칙 결정 */
    bitmap_t dp_bitmaps[MAX_EQID], pr_bitmaps[MAX_EQID];

    /* eqID의 비트맵 수집 (Phase 0에서 이미 계산됨) */
    for (int port = 0; port < PORT_SPACE; port++)
        dp_bitmaps[phase0_dport[port]] = 0;
    for (int port = 0; port < PORT_SPACE; port++) {
        bitmap_t bm = 0;
        for (int r = 0; r < nr; r++)
            if (rules[r].dport == 0xFFFF || rules[r].dport == port)
                bm |= (1 << r);
        dp_bitmaps[phase0_dport[port]] = bm;
    }
    for (int p = 0; p < PROTO_SPACE; p++)
        pr_bitmaps[phase0_proto[p]] = 0;
    for (int p = 0; p < PROTO_SPACE; p++) {
        bitmap_t bm = 0;
        for (int r = 0; r < nr; r++)
            if (rules[r].proto == 0 || rules[r].proto == p)
                bm |= (1 << r);
        pr_bitmaps[phase0_proto[p]] = bm;
    }

    /* 교차곱 계산: AND 연산 → 최우선 규칙 */
    printf("\n  Phase 1: %d × %d = %d 조합 → 교차곱 축소\n",
           n_dport_eq, n_proto_eq, n_dport_eq * n_proto_eq);

    for (int d = 0; d < n_dport_eq; d++) {
        for (int p = 0; p < n_proto_eq; p++) {
            bitmap_t combined = dp_bitmaps[d] & pr_bitmaps[p];
            int best = -1;
            for (int r = 0; r < nr; r++) {
                if ((combined & (1 << r)) &&
                    (best < 0 || rules[r].priority < rules[best].priority))
                    best = r;
            }
            phase1_table[d][p] = best;
        }
    }
}

/* ── RFC 분류: 단 2회 메모리 접근! ── */
static int rfc_classify(uint16_t dport, uint8_t proto)
{
    /* Phase 0: 배열 인덱싱 (각 O(1)) */
    uint8_t eq_dport = phase0_dport[dport];    /* 1회 메모리 접근 */
    uint8_t eq_proto = phase0_proto[proto];    /* 1회 메모리 접근 */

    /* Phase 1: 교차곱 테이블 조회 (O(1)) */
    return phase1_table[eq_dport][eq_proto];   /* 1회 메모리 접근 */
    /* 총 3회 메모리 접근으로 분류 완료! */
}

int main(void)
{
    /* 예시 규칙 (DstPort × Protocol 2차원만) */
    struct rule rules[] = {
        {"R1", 1, 80,     6,  1},  /* dport=80,  TCP,  ALLOW */
        {"R2", 2, 443,    6,  1},  /* dport=443, TCP,  ALLOW */
        {"R3", 3, 53,     17, 1},  /* dport=53,  UDP,  ALLOW */
        {"R4", 4, 0xFFFF, 0,  1},  /* dport=*,   *,    ALLOW */
        {"R5", 5, 22,     6,  0},  /* dport=22,  TCP,  DROP  */
        {"R6", 6, 80,     6,  1},  /* dport=80,  TCP,  ALLOW */
        {"R7", 7, 0xFFFF, 1,  1},  /* dport=*,   ICMP, ALLOW */
        {"R8", 8, 0xFFFF, 0,  0},  /* dport=*,   *,    DROP  */
    };
    int nr = 8;

    printf("=== RFC 분류기 (2D: DstPort × Protocol) ===\n\n");
    printf("── 사전 계산 단계 ──\n");

    /* Phase 0 사전 계산 */
    int n_dport = precompute_phase0_dport(rules, nr);
    int n_proto = precompute_phase0_proto(rules, nr);

    /* Phase 1 사전 계산 */
    precompute_phase1(rules, nr, n_dport, n_proto);

    /* 분류 실행 — 매우 빠름! */
    printf("\n── 패킷 분류 (런타임) ──\n");

    struct { uint16_t dport; uint8_t proto; const char *desc; } tests[] = {
        {80,  6,  "dport=80, TCP  → 기대: R1(ALLOW)"},
        {443, 6,  "dport=443, TCP → 기대: R2(ALLOW)"},
        {53,  17, "dport=53, UDP  → 기대: R3(ALLOW)"},
        {22,  6,  "dport=22, TCP  → 기대: R5(DROP)"},
        {8080, 6, "dport=8080, TCP→ 기대: R4(ALLOW)"},
        {0,   1,  "dport=0, ICMP  → 기대: R7(ALLOW)"},
        {12345, 99, "dport=12345   → 기대: R8(DROP)"},
    };

    for (int i = 0; i < 7; i++) {
        int idx = rfc_classify(tests[i].dport, tests[i].proto);
        printf("  %s\n    → %s (%s)\n",
               tests[i].desc,
               idx >= 0 ? rules[idx].name : "NONE",
               idx >= 0 ? (rules[idx].action ? "ALLOW" : "DROP") : "DEFAULT");
    }

    printf("\n── 메모리 사용량 ──\n");
    printf("  Phase 0 DstPort 테이블: %zu bytes\n", sizeof(phase0_dport));
    printf("  Phase 0 Protocol 테이블: %zu bytes\n", sizeof(phase0_proto));
    printf("  Phase 1 교차곱 테이블: %zu bytes\n",
           (size_t)n_dport * n_proto * sizeof(int));
    printf("  총합: %zu bytes\n",
           sizeof(phase0_dport) + sizeof(phase0_proto) +
           (size_t)n_dport * n_proto * sizeof(int));
    printf("  (참고: 선형 탐색은 %zu bytes만 필요)\n",
           nr * sizeof(struct rule));

    return 0;
}

/* 실행 결과 (발췌):
 *
 * === RFC 분류기 (2D: DstPort × Protocol) ===
 *
 * ── 사전 계산 단계 ──
 *   Phase 0 DstPort: 65536개 포트 → 5개 eqID (축소율 99.99%)
 *     eqID=0: bitmap=00B8 → 규칙: R4 R5 R8
 *     eqID=1: bitmap=008C → 규칙: R3 R4 R8
 *     eqID=2: bitmap=00E9 → 규칙: R1 R4 R6 R8
 *     eqID=3: bitmap=009A → 규칙: R2 R4 R8
 *     eqID=4: bitmap=0088 → 규칙: R4 R8
 *   Phase 0 Protocol: 256개 값 → 4개 eqID (축소율 98.44%)
 *
 *   Phase 1: 5 × 4 = 20 조합 → 교차곱 축소
 *
 * ── 패킷 분류 (런타임) ──
 *   dport=80, TCP  → 기대: R1(ALLOW)
 *     → R1 (ALLOW)          ← 단 3회 메모리 접근!
 *   dport=22, TCP  → 기대: R5(DROP)
 *     → R5 (DROP)
 *
 * ── 메모리 사용량 ──
 *   Phase 0 DstPort 테이블: 65536 bytes
 *   Phase 0 Protocol 테이블: 256 bytes
 *   Phase 1 교차곱 테이블: 80 bytes
 *   총합: 65872 bytes (~64 KB)
 *   (참고: 선형 탐색은 128 bytes만 필요)
 *
 * → RFC는 64KB 메모리를 사용하지만,
 *   분류가 O(3) 상수 시간! (선형 탐색은 O(N×d))
 */
RFC의 트레이드오프를 직접 확인: 위 코드를 실행하면 RFC의 핵심 트레이드오프를 직접 확인할 수 있습니다. 메모리는 65 KB를 사용하지만(선형 탐색의 128바이트 대비 500배), 분류 속도는 단 3회 배열 접근(선형 탐색의 8회 규칙 비교 × 5 필드 대비 13배 빠름)입니다. 5차원(5-tuple) 전체로 확장하면 Phase 0 테이블 7개 + Phase 1~2 교차곱 테이블 3개 = 총 10회 정도의 메모리 접근으로 수만 개 규칙을 분류할 수 있습니다.

RFC 메모리 최적화 기법

RFC의 가장 큰 약점인 메모리 사용량을 줄이기 위한 여러 최적화 기법이 있습니다.

청크 크기 조절

Phase 0에서 16비트 청크 대신 8비트 청크를 사용하면 테이블 크기가 216(64K) → 28(256)으로 줄어듭니다. 대신 청크 수가 늘어나 Phase 단계가 더 필요합니다.

청크 크기Phase 0 테이블 크기청크 수 (5-tuple)필요 Phase 수총 메모리
16비트65,536/테이블73~4 MB (대표값)
8비트256/테이블134~5~0.5 MB
혼합 (IP:16, Port:8, Proto:8)혼합93~4~2 MB

희소 교차곱 테이블

Phase 1 이상에서 교차곱 테이블의 많은 엔트리가 비어 있거나(매칭 규칙 없음) 동일한 eqID를 가집니다. 이를 해시 테이블이나 압축 배열로 대체하면 메모리를 절약할 수 있습니다. 단, 해시 룩업의 추가 지연이 발생합니다.

2단계 RFC (Two-phase RFC)

Gupta & McKeown의 원 논문에서도 제안된 변형으로, Phase 수를 2단계로 줄이는 대신 Phase 1의 교차곱 테이블이 커집니다. 반대로 4단계 이상으로 늘리면 각 테이블은 작아지지만 분류 시간이 증가합니다.

/* 2단계 RFC: 모든 Phase 0 eqID를 한 번에 결합 */
Phase 0: 7개 청크 → 7개 eqID (e₀, e₁, ..., e₆)
Phase 1: combined_eqID = table[e₀ × E₁ × ... + eₖ]  /* 단일 거대 테이블 */
         → classID (최종 규칙)

/* 장점: 2회 메모리 접근으로 분류 완료 */
/* 단점: Phase 1 테이블이 E₀×E₁×...×E₆ 크기 → 메모리 폭발 위험 */

/* 4단계 RFC: 더 세밀한 분할 */
Phase 0: 13개 청크 (8비트) → 13개 eqID
Phase 1: (e₀,e₁,e₂), (e₃,e₄,e₅), (e₆,e₇,e₈), (e₉,e₁₀,e₁₁,e₁₂)
Phase 2: (p1₀,p1₁), (p1₂,p1₃)
Phase 3: (p2₀,p2₁) → classID

/* 장점: 각 테이블이 작음 (메모리 절약) */
/* 단점: 4회 메모리 접근 (분류 시간 증가) */

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

RFC 테이블을 메모리에 배치하는 방식이 분류 성능에 직접 영향을 줍니다. CPU 캐시 계층 구조와 RFC 테이블 크기를 매핑하면 설계 선택을 최적화할 수 있습니다.

RFC 테이블메모리 크기전형적 캐시 위치접근 지연(추정)
Phase 1 테이블 합계 (A+B+C)~5 KBL1 캐시 (항상 상주)~4 ns
Phase 0 테이블 개별 (64KB)64 KB/개 × 7L2 캐시 (일부 상주)~10~15 ns
Phase 0 테이블 전체~448 KBL2/L3 경계~15~30 ns
Phase 2 테이블~170~340 KBL2~L3 (트래픽 지역성 의존)~10~30 ns
/* RFC 분류의 실제 메모리 접근 패턴 분석 */

/* Phase 0: 7개 청크 → 7회 독립 랜덤 접근 (64KB 테이블 7개) */
/* → 병렬 처리 가능, 각각 별도 64KB 영역 → 7개 잠재적 L2 미스 */
/* → 실제: 핫 트래픽은 동일 서브넷 → 같은 캐시 라인 재사용 많음 */

/* Phase 1: 3개 테이블, 합계 ~5KB → 거의 항상 L1 캐시 히트 */
/* → 3회 접근이지만 4ns × 3 = ~12ns에 불과 */

/* Phase 2: 1회 접근, 174,300 엔트리 × 2B = 340KB */
/* → 트래픽 지역성 있으면 L2 히트, 없으면 L3까지 */

/* 최악 시나리오 (모든 미스) 지연 추정: */
/*   Phase 0: 7 × 15ns(L2) = 105ns (병렬 시 15ns) */
/*   Phase 1: 3 × 4ns(L1)  = 12ns                */
/*   Phase 2: 1 × 15ns(L2) = 15ns                */
/*   → 순차 합계: ~132ns, 병렬 Phase 0 시: ~42ns  */

/* 10Gbps, 64-byte 패킷: 14.88Mpps 필요 → 패킷당 67ns 예산 */
/* Phase 0 병렬화(SIMD 또는 out-of-order 실행) 시 충분히 가능 */
RFC 테이블과 CPU 캐시 계층 매핑 RFC의 각 Phase 테이블이 CPU의 L1/L2/L3 캐시와 DRAM 계층 중 어디에 위치하는지, 그리고 각 계층의 접근 지연을 보여줍니다. RFC 테이블 ↔ CPU 캐시 계층 매핑 L1 캐시 — 32 KB (전형적) 접근 지연: ~4 ns L2 캐시 — 256 KB ~ 1 MB (전형적) 접근 지연: ~10~15 ns L3 캐시 — 수 MB ~ 수십 MB (전형적) 접근 지연: ~30~40 ns DRAM — 수 GB 접근 지연: ~60~100 ns (캐시 미스 시) Phase 1 테이블 ~5 KB 항상 L1 상주 (핫 경로) Phase 0 개별 테이블 64 KB/개 → L2 Phase 0 전체 ~448 KB L2/L3 경계 Phase 2 ~170~340 KB 지역성 의존 → L2~L3 단일 코어 분류 성능 추정 (10 GbE 기준: 67ns/패킷) 최선 (모두 캐시 히트): Phase1(5KB L1=12ns) + Phase0(7×4ns=28ns) + Phase2(4ns) ≈ 44ns → 22Mpps 일반 (트래픽 지역성): Phase0(7×15ns L2, 순차) + Phase1(12ns) + Phase2(15ns) ≈ 132ns → 7.5Mpps 최선 (Phase0 병렬화): 15ns + 12ns + 15ns ≈ 42ns → 23Mpps 이상 → 10GbE 단일코어 달성 가능
캐시 최적화 실전 전략: Phase 1 테이블(~5 KB)은 항상 L1 캐시에 상주합니다. Phase 0 테이블 중 자주 접근하는 청크(예: Protocol 256B, Port 64KB)를 캐시 친화적으로 배치하고, Phase 0 7개 접근을 out-of-order 실행으로 병렬화하면 10GbE 선속도 분류가 단일 코어에서도 가능합니다. 실제 DPDK 기반 RFC 구현에서 이 기법으로 15~20 Mpps를 달성한 사례가 있습니다.

RFC 변형 및 발전 알고리즘

RFC의 핵심 아이디어(동치 클래스 기반 다단계 축소)는 이후 많은 연구에서 개선되었습니다. 각 변형은 RFC의 특정 약점(메모리 폭발, 업데이트 불가, IPv6 확장 등)을 해결하려는 시도입니다.

ABV (Aggregated Bit Vector)

Baboescu & Varghese (2001)이 제안한 ABV는 RFC의 비트맵(bit vector)을 집계(aggregation)하여 메모리를 절약하는 기법입니다.

핵심 아이디어

RFC에서 각 eqID는 N비트 비트맵을 유지합니다 (N = 규칙 수). 규칙이 10,000개이면 비트맵 하나가 1.25KB입니다. ABV는 이 긴 비트맵을 A비트 집계 비트맵으로 요약합니다.

/* ABV: 비트맵 집계 */
/* 원본 비트맵 (N=16 규칙 예시): */
bitmap = 0100 0000 1000 0010

/* 4비트 그룹별 집계 (A=4): */
/* 그룹0: 0100 → 1 (하나 이상 1) */
/* 그룹1: 0000 → 0 */
/* 그룹2: 1000 → 1 */
/* 그룹3: 0010 → 1 */
aggregated = 1011  /* N/4 = 4비트로 축소 */

/* 교차곱 시: aggregated 비트맵끼리 AND → 빠른 사전 필터링 */
/* 0인 그룹은 원본 비트맵 비교를 건너뜀 → 속도 향상 */
지표RFCABV
비트맵 크기N비트N/A 비트 (집계) + 원본
교차곱 속도O(N) 비트 연산O(N/A) 사전 필터 + 히트 그룹만 O(A)
메모리기준유사 (집계 오버헤드 미미)
주요 이점사전 계산 속도 향상 (희소 비트맵에서 극적)

DRFC (Distributed RFC)

RFC를 여러 병렬 처리 유닛에 분산하여, 단일 메모리 한계를 극복하는 변형입니다. FPGA나 NPU(Network Processing Unit)에서 자주 사용됩니다.

분산 전략

DRFC 파이프라인 병렬 구조 RFC의 Phase를 파이프라인으로 구성하여 여러 패킷을 동시에 처리하는 분산 RFC 구조를 보여줍니다. DRFC: 파이프라인 병렬 처리 시간 → T=1 T=2 T=3 T=4 Phase 0 Phase 1 Phase 2 Pkt 1: Phase 0 Pkt 2: Phase 0 Pkt 1: Phase 1 Pkt 3: Phase 0 Pkt 2: Phase 1 Pkt 1: Phase 2 ✓ Pkt 4: Phase 0 Pkt 3: Phase 1 Pkt 2: Phase 2 ✓ 파이프라인 효과 지연(latency): 3 클럭 (Phase 수, 변화 없음) 처리량(throughput): 1 패킷/클럭 (Phase 수만큼 향상) → 와이어 스피드 달성

Incremental RFC (증분 RFC)

원 RFC의 가장 큰 실무적 약점은 규칙 변경 시 전체 재계산이 필요하다는 것입니다. 증분 RFC(Incremental RFC)는 규칙 추가/삭제 시 영향받는 eqID만 부분 갱신하여 이 문제를 완화합니다.

증분 업데이트 원리

/* 증분 규칙 추가: 규칙 Rnew 추가 시 */

/* Step 1: Phase 0 — 영향받는 eqID 식별 */
for each chunk table T:
    for each value v in Rnew.field_range:
        old_bitmap = T.bitmap[v]
        new_bitmap = old_bitmap | (1 << Rnew.id)
        if new_bitmap != old_bitmap:
            /* 이 값의 비트맵이 변경됨 → eqID 재할당 필요 */
            T.bitmap[v] = new_bitmap
            if new_bitmap not in eqID_map:
                eqID_map[new_bitmap] = allocate_new_eqID()
            T.table[v] = eqID_map[new_bitmap]
            changed_eqIDs.add(T.table[v])

/* Step 2: Phase 1+ — 영향받는 교차곱만 재계산 */
for each changed_eqID in Phase k:
    /* 이 eqID가 포함된 교차곱 엔트리만 갱신 */
    for each partner_eqID in adjacent_table:
        recalculate_combined(changed_eqID, partner_eqID)
        /* 새 combined_bitmap → 새 eqID 할당 or 기존 eqID 재사용 */
지표원 RFC증분 RFC
규칙 추가 비용O(2W·N + 교차곱 전체)O(영향 범위 × Phase 수)
메모리 오버헤드없음역매핑(eqID→값 목록) 추가 필요
분류 속도O(D)O(D) — 동일
구현 복잡도낮음높음 (eqID 분할/병합 관리)
적합 시나리오정적 규칙셋드문 업데이트 (분당 수회)
eqID 분할 문제: 규칙 추가로 기존 동치 클래스가 분할될 수 있습니다. 예를 들어, eqID=4("나머지 포트")에 새 규칙이 포트 8080을 지정하면, eqID=4는 "8080"과 "나머지-8080"으로 분할됩니다. 이 분할이 연쇄적으로 후속 Phase에 전파되므로, 최악의 경우 전체 재계산과 비슷한 비용이 됩니다. 따라서 증분 RFC는 규칙 변경이 드물고 규모가 작은 환경에만 실용적입니다.

확장 RFC (Extended RFC)

원 RFC는 IPv4 5-tuple(104비트)에 최적화되어 있지만, 현대 네트워크에서는 더 많은 필드가 필요합니다.

IPv6 확장

IPv6 주소는 128비트로, 16비트 청크로 나누면 출발지+목적지만 16개 청크가 필요합니다. Phase 0 테이블만 16 × 64KB = 1MB이지만, 이후 Phase에서 동치 클래스 축소가 IPv4보다 더 극적으로 발생합니다(IPv6 주소 공간의 대부분이 미사용이므로).

프로토콜총 헤더 비트16비트 청크 수Phase 0 메모리실제 eqID 수
IPv4 5-tuple10477 × 64KB = 448KB수십~수백
IPv6 5-tuple2961919 × 64KB = 1.2MB수십~수백 (유사)
IPv6 + VLAN + DSCP~3202121 × 64KB = 1.3MB수백

다중 필드 확장 (Beyond 5-tuple)

SDN/OpenFlow 환경에서는 5-tuple 외에도 VLAN ID, MPLS 라벨, 터널 ID, 메타데이터 등 최대 40+개 필드를 매칭해야 합니다. RFC를 이런 환경에 확장할 때의 고려사항:

RFC의 하드웨어 구현

RFC의 규칙적인 메모리 접근 패턴은 하드웨어 구현에 매우 적합합니다. FPGA, ASIC, NPU에서의 구현 전략입니다.

FPGA 기반 RFC

/* FPGA에서의 RFC 구현 구조 */

/* Phase 0: BRAM(Block RAM)에 룩업 테이블 저장 */
/* 7개 청크 × 64KB = 448KB → FPGA BRAM에 적합 */
/* 7개 테이블을 7개 BRAM 포트에서 동시 접근 → 1 클럭 */

/* Phase 1: 3개 결합 테이블도 BRAM 저장 */
/* 결합 A: 48×24 = 1,152 엔트리 → 소형 BRAM */
/* 결합 B: 52×31 = 1,612 엔트리 → 소형 BRAM */
/* 결합 C: 18×25×5 = 2,250 엔트리 → 소형 BRAM */

/* Phase 2: 60×83×35 → 해시 기반 or 직접 테이블 */

/* 파이프라인: Phase 0 → 레지스터 → Phase 1 → 레지스터 → Phase 2 */
/* 총 지연: 3 클럭, 처리량: 1 패킷/클럭 */
/* 200MHz FPGA에서: 200M 패킷/초 ≈ 100Gbps (64B 패킷) */

SmartNIC ASIC (P4 + RFC)

현대 프로그래머블 스위치 ASIC(Intel Tofino, NVIDIA Spectrum 등)은 MAT(Match-Action Table) 파이프라인을 제공합니다. 이 파이프라인은 RFC의 다단계 구조와 자연스럽게 매핑됩니다:

RFC와 다른 알고리즘의 하이브리드

RFC + Tuple Space (Hybrid)

RFC의 빠른 분류 속도와 튜플 공간의 빠른 업데이트를 결합하는 하이브리드 접근입니다.

RFC + Decision Tree (CutSplit)

Li et al. (2016)의 CutSplit은 규칙셋을 큰 필드(IP 주소)와 작은 필드(Port, Protocol)로 분리하여, 큰 필드는 결정 트리(HiCuts)로, 작은 필드는 RFC-like 교차곱으로 처리합니다.

CutSplit 하이브리드 구조 CutSplit이 규칙의 큰 필드(IP)는 결정 트리로, 작은 필드(Port/Proto)는 RFC-like 교차곱으로 분류한 뒤 결과를 합성하는 구조를 보여줍니다. CutSplit: 큰 필드 (Decision Tree) + 작은 필드 (RFC-like) 패킷 SrcIP, DstIP SrcPort, DstPort Protocol 큰 필드 작은 필드 Decision Tree (HiCuts) SrcIP, DstIP → 공간 분할 IP 와일드카드 규칙은 별도 트리로 분리 → 후보 규칙 집합 S₁ RFC-like 교차곱 SrcPort × DstPort × Protocol 값 공간 작음 → 교차곱 관리 가능 → 후보 규칙 집합 S₂ 결과 합성 S₁ ∩ S₂ → 최우선 규칙 R1 ALLOW CutSplit 장점 • IP 와일드카드 분리 → 규칙 복제 최소화 • Port/Proto는 RFC로 → 빠른 교차곱 분류 • 대규모 규칙셋(10K+)에서 우수한 성능 • 메모리와 속도 모두 개선

NeuroCuts — 강화학습 기반 결정 트리

Liang et al. (2019)의 NeuroCuts강화학습(Reinforcement Learning)으로 결정 트리의 분할 전략을 학습합니다. HiCuts/HyperCuts의 휴리스틱(차원 선택, 분할 수)을 신경망이 대체하여, 규칙셋 특성에 맞는 최적 트리를 자동 생성합니다.

패킷 분류 알고리즘 발전 연대기

연도알고리즘저자핵심 기여RFC와의 관계
1998Grid-of-TriesSrinivasan et al.2차원 트라이 결합RFC 이전, 다차원 한계
1998Tuple Space SearchSrinivasan et al.마스크별 해시 테이블RFC와 보완적 (동적 업데이트)
1999RFCGupta & McKeown동치 클래스 다단계 축소기준점
2000HiCutsGupta & McKeown단일 차원 순차 분할RFC와 다른 접근 (공간 분할)
2001ABVBaboescu & Varghese비트벡터 집계RFC 비트맵 최적화
2003HyperCutsSingh et al.다차원 동시 분할HiCuts 확장
2010EffiCutsVamanan et al.규칙 분리로 복제 감소HyperCuts 최적화
2013CutSplitLi et al.대/소 필드 분리 분류RFC + 결정 트리 하이브리드
2019NeuroCutsLiang et al.강화학습 기반 트리 구축ML로 분할 전략 학습
2020+SmartNIC RFC Pipeline산업계HW MAT 기반 RFC 구현RFC의 ASIC 직접 구현
어떤 변형을 선택할까?
  • 정적 규칙 + HW 구현: 원 RFC 또는 DRFC (FPGA/ASIC 파이프라인)
  • 드문 업데이트: 증분 RFC (분당 수회 규칙 변경)
  • 빈번한 업데이트: RFC 포기 → 튜플 공간(cls_flower) 또는 하이브리드
  • 대규모 규칙셋(10K+): CutSplit 또는 RFC + 결정 트리 하이브리드
  • IPv6 환경: 확장 RFC (8비트 청크 + 4단계)

TCAM (Ternary Content-Addressable Memory)

TCAM(Ternary Content-Addressable Memory)은 하드웨어 수준에서 모든 엔트리를 동시에(병렬로) 비교하는 특수 메모리입니다. 각 비트 셀은 0, 1, X(don't care, 와일드카드) 세 가지 상태를 가질 수 있어, 접두사와 와일드카드를 자연스럽게 표현합니다.

동작 원리

  1. 엔트리 저장: 각 규칙을 (Value, Mask) 쌍으로 TCAM에 프로그래밍합니다. Mask의 X 비트는 해당 위치를 무시합니다
  2. 병렬 비교: 검색 키(패킷 헤더)가 입력되면, 모든 엔트리가 동시에 비교를 수행합니다
  3. 우선순위 인코더: 매칭된 엔트리 중 가장 낮은 인덱스(= 가장 높은 우선순위)를 선택합니다
  4. 액션 조회: 매칭 인덱스로 연관 SRAM(Associated RAM)에서 액션을 읽습니다
TCAM 병렬 매칭 구조 TCAM의 엔트리가 0/1/X 비트로 구성되며, 검색 키와 모든 엔트리를 동시에 비교하여 최우선 매칭을 찾는 과정을 보여줍니다. TCAM: Ternary Content-Addressable Memory 검색 키 (패킷 헤더) 1100 1010 0000 0001 ... 브로드캐스트 TCAM 엔트리 (우선순위 순) [0] 1100 1010 XXXX XXXX ... 비교중 [1] 1100 XXXX 0000 0001 ... 비교중 [2] 1100 1010 0000 XXXX ... 매칭! [3] XXXX XXXX XXXX XXXX ... 매칭! ... (N개 엔트리, 모두 동시 비교) ← 1 클럭에 모든 엔트리 병렬 비교 → 우선순위 인코더 최소 인덱스 선택 매칭 인덱스 = 2 (가장 높은 우선순위) Associated SRAM → 액션: ALLOW X = don't care (0 또는 1 모두 매칭) 한계: 엔트리당 ~16비트/셀, 용량 수K~수M, 높은 전력 소모

TCAM의 한계

항목제약영향
용량수천~수백만 엔트리대규모 규칙셋에서 공간 부족
전력엔트리당 ~0.5mW (병렬 비교)1만 엔트리 TCAM ≈ 5W 소모
범위 매칭범위를 접두사 집합으로 확장 필요포트 범위 1024-65535 → 최대 15개 엔트리
비용SRAM 대비 수십 배 비쌈대용량 TCAM은 비용 문제

범위-접두사 확장 (Range-to-Prefix Expansion)

TCAM은 접두사(prefix)만 자연스럽게 표현하므로, 포트 범위를 TCAM에 넣으려면 여러 접두사 엔트리로 분해해야 합니다. 이 확장이 실제 엔트리 소모를 크게 늘릴 수 있습니다.

예시: 포트 범위 1024-65535의 접두사 분해

/* 범위 1024-65535 (0x0400 - 0xFFFF) → 접두사로 분해 */
0x0400-0x07FF → 0000 01XX XXXX XXXX  /* /6  (1024-2047)  */
0x0800-0x0FFF → 0000 1XXX XXXX XXXX  /* /5  (2048-4095)  */
0x1000-0x1FFF → 0001 XXXX XXXX XXXX  /* /4  (4096-8191)  */
0x2000-0x3FFF → 001X XXXX XXXX XXXX  /* /3  (8192-16383) */
0x4000-0x7FFF → 01XX XXXX XXXX XXXX  /* /2  (16384-32767)*/
0x8000-0xFFFF → 1XXX XXXX XXXX XXXX  /* /1  (32768-65535)*/

/* 총 6개 TCAM 엔트리 필요 (1개 범위 → 6개로 확장) */
포트 범위TCAM 엔트리 수확장 비율
80 (단일 포트)1
0-1023 (Well-known)1010×
1024-65535 (Ephemeral)6
8000-9000 (임의 범위)최대 3030×
실무 영향: 5-tuple 규칙에서 Src Port와 Dst Port 모두 범위를 사용하면, 확장은 곱셈적입니다. srcport=1024-65535, dstport=8000-9000인 규칙은 최대 6 × 30 = 180개 TCAM 엔트리로 확장될 수 있습니다. 실제 TCAM 용량 산정 시 이 확장을 반드시 고려해야 합니다. 고급 TCAM은 범위 매칭을 직접 지원하는 확장 모드를 제공하기도 합니다.

TCAM 최적화 알고리즘

TCAM 최소화 (TCAM Minimization)

동일한 분류 결과를 내면서 TCAM 엔트리 수를 최소화하는 것은 비용과 전력 절감에 직결됩니다. 주요 기법들입니다.

접두사 최소화 (Prefix Minimization): 인접하거나 겹치는 접두사 규칙을 더 짧은 접두사로 통합합니다.

/* 접두사 최소화 예시: ACCEPT 규칙 집합 */
/* 원본: 192.168.1.0/24, 192.168.2.0/24, 192.168.3.0/24 → 3개 엔트리 */
/* 최소화: 세 접두사가 192.168.0.0/22에 포함 → 1개 엔트리로 통합 */
/* 단, 192.168.0.0/24는 다른 규칙이면 분리 유지 필요 */

/* 중복 제거 (Redundancy Removal): */
/* R1: 192.168.0.0/16 → ALLOW */
/* R2: 192.168.1.0/24 → ALLOW  ← R1의 부분집합이고 같은 액션 → 중복! */
/* R2 제거 가능 — R1이 R2의 모든 패킷을 같은 액션으로 처리 */

삼진 시뮬레이션 (Ternary Simulation): 규칙 집합 전체에서 don't-care 비트를 최대화하여 엔트리 수를 줄이는 기법입니다. 이진 결정 다이어그램(BDD)으로 구현할 수 있습니다.

범위 인코딩 기법 (Range Encoding)

포트 범위를 TCAM 접두사로 변환할 때 필요한 엔트리 수를 최소화하는 인코딩 방법들입니다.

인코딩 방식엔트리 수 (최악)장점단점
직접 접두사 분해 (기본)2(W-1) = 30개 (16비트)구현 단순엔트리 폭발
DIRPE (Database-Independent)W-1 = 15개정적 인코딩, 규칙셋 독립인코딩 테이블 필요
DIRPE with Gray codeW/2 = 8개 (평균)인접 값이 1비트만 다름구현 복잡
HiCuts 범위 트리O(log W)결정 트리와 통합소프트웨어 전용

DIRPE(Database-Independent Range Pre-Encoding)는 모든 16비트 범위 [a, b]를 최대 W-1=15개의 TCAM 접두사로 표현할 수 있도록 주소 공간을 사전에 재매핑합니다. 이 인코딩은 규칙셋과 독립적으로 전체 포트 공간에 적용되므로 "데이터베이스 독립적"입니다.

TCAM 에너지 절약

TCAM의 가장 큰 약점은 병렬 비교로 인한 높은 전력 소모입니다. 에너지 절약 기법들입니다.

리눅스 커널 구현

리눅스 커널의 TC(Traffic Control) 서브시스템은 세 가지 주요 분류기(classifier)를 제공합니다. 각각 다른 패킷 분류 전략을 구현합니다.

cls_u32 내부

cls_u32(net/sched/cls_u32.c)는 리눅스 커널의 가장 오래된 범용 분류기로, 오프셋 기반 패턴 매칭해시 테이블을 결합합니다.

/* net/sched/cls_u32.c — 핵심 구조체 */

struct tc_u_hnode {
    struct tc_u_hnode __rcu  *next;
    u32                      handle;
    u32                      prio;
    int                      refcnt;
    unsigned int             divisor;   /* 해시 테이블 크기 */
    struct tc_u_knode __rcu  *ht[1];    /* 해시 버킷 배열 */
};

struct tc_u_knode {
    struct tc_u_knode __rcu  *next;
    u32                      handle;
    struct tc_u_hnode        *ht_up;    /* 상위 해시 노드 */
    struct tcf_exts          exts;      /* 액션 */
    struct tcf_result        res;
    struct tc_u_hnode        *ht_down;  /* 다음 레벨 해시 */
    struct tc_u32_sel        sel;       /* 매칭 셀렉터 */
};

struct tc_u32_sel {
    unsigned char  flags;
    unsigned char  offshift;    /* 오프셋 시프트 */
    unsigned char  nkeys;       /* 키 개수 */
    __be16         offmask;     /* 오프셋 마스크 */
    __u16          off;         /* 기본 오프셋 */
    short          offoff;      /* 오프셋의 오프셋 */
    short          hoff;        /* 해시 오프셋 */
    __be32         hmask;       /* 해시 마스크 */
    struct tc_u32_key keys[];   /* 매칭 키 배열 */
};

cls_u32의 분류 과정은 다음과 같습니다.

  1. 패킷 데이터에서 지정된 오프셋의 32비트 값을 읽습니다
  2. 해당 값에 마스크를 적용하고 해시 테이블의 버킷을 선택합니다
  3. 버킷 내의 키 노드(tc_u_knode)를 순차 비교합니다
  4. 모든 키가 매칭되면 액션을 적용하거나 다음 레벨 해시로 이동합니다
cls_u32 다단계 해시 구조 cls_u32의 해시 테이블 기반 분류에서 패킷 오프셋으로 해시 버킷을 선택하고, 키 노드를 순차 비교하며, 다음 레벨 해시로 체이닝하는 구조를 보여줍니다. cls_u32: 다단계 해시 분류 패킷 데이터 offset + hmask hash tc_u_hnode (Root HT) divisor = 256 bucket[0]: (empty) bucket[1]: knode → bucket[80]: knode → ★ ... bucket[255]: knode → tc_u_knode sel: DstIP match ht_down → Level 2 ht_down Level 2 HT SrcIP 기반 해시 → Action: ALLOW 최종 매칭! next tc_u_knode sel: Proto match 분류 순서 1. 패킷[hoff] & hmask → 해시 → bucket 선택 2. 버킷 내 knode 순차 비교 (sel.keys[]) 3. 매칭 시: ht_down? → 다음 레벨 / res → 액션 적용
분류 전략: cls_u32는 해시 테이블로 첫 단계 분류를 수행하고(O(1)), 버킷 내에서 선형 비교를 합니다. 적절한 해시 키를 설정하면 효과적이지만, 해시 충돌이 많으면 선형 탐색으로 퇴화합니다. 다차원 분류를 위해 다단계 해시 테이블을 체이닝(ht_down)할 수 있으며, 이는 RFC의 다단계 축소와 개념적으로 유사합니다.

cls_u32 사용 예시

# 2단계 u32 분류: DstPort로 1차 해시, SrcIP로 2차 매칭

# Root 해시 테이블 (divisor=256, DstPort 하위 8비트로 해시)
$ tc filter add dev eth0 parent 1:0 prio 1 protocol ip \
    handle 1: u32 divisor 256

# DstPort=80 → bucket 80으로 분류하는 해시 키
$ tc filter add dev eth0 parent 1:0 prio 1 protocol ip \
    u32 ht 800:: match ip dport 80 0xffff \
    hashkey mask 0x000000ff at 20 \
    link 1:

# Bucket 80에서 SrcIP=192.168.1.0/24 매칭 → class 1:10
$ tc filter add dev eth0 parent 1:0 prio 1 protocol ip \
    u32 ht 1:50: match ip src 192.168.1.0/24 \
    flowid 1:10

cls_flower 내부

cls_flower(net/sched/cls_flower.c)는 마스크 기반 해시를 사용하는 현대적인 분류기로, tc flower 명령으로 사용됩니다.

/* net/sched/cls_flower.c — 핵심 구조체 */

struct cls_fl_head {
    struct rhashtable       ht;           /* 리사이즈 가능 해시 */
    struct list_head        masks;        /* 마스크 목록 */
    struct rcu_head         rcu;
};

struct fl_flow_key {
    struct flow_dissector_key_control control;
    struct flow_dissector_key_basic   basic;     /* n_proto, ip_proto */
    struct flow_dissector_key_ipv4_addrs ipv4;   /* src, dst */
    struct flow_dissector_key_ports   tp;        /* src, dst port */
    struct flow_dissector_key_vlan    vlan;
    struct flow_dissector_key_eth_addrs eth;
    /* ... 더 많은 필드 */
};

struct fl_flow_mask {
    struct fl_flow_key      key;          /* 마스크 값 */
    struct list_head        list;
    struct rhashtable       ht;           /* 이 마스크의 해시 테이블 */
    struct rhashtable_params filter_ht_params;
};

cls_flower의 분류는 튜플 공간 탐색과 유사합니다.

  1. 패킷에서 fl_flow_key를 추출합니다 (fl_classifyskb_flow_dissect)
  2. 등록된 각 마스크(fl_flow_mask)에 대해 키를 마스킹합니다
  3. 마스킹된 키로 해당 마스크의 해시 테이블(rhashtable)에서 룩업합니다
  4. 매칭되면 해당 필터의 액션을 반환합니다
cls_flower 마스크 기반 분류 흐름 cls_flower가 패킷에서 fl_flow_key를 추출하고, 등록된 마스크별로 rhashtable 룩업을 수행하여 매칭 필터를 찾는 과정을 보여줍니다. cls_flower: 마스크 기반 해시 분류 (= 튜플 공간 탐색) sk_buff 패킷 skb_flow_dissect → fl_flow_key 추출 (SrcIP, DstIP, Ports, Proto, ...) 마스크 목록 순회 (cls_fl_head.masks) Mask 1: SrcIP/24 + DstPort/exact key & mask → rhashtable lookup → flow_dissector_key hash → O(1) → 매칭 필터 발견! → 액션 반환 Mask 2: DstIP/16 + Proto/exact key & mask → rhashtable lookup (Mask 1에서 이미 매칭 — 도달 안 함) Mask 3: wildcard all key & mask → rhashtable lookup 마스크 수 = 3 → 최대 3회 해시 룩업 마스크 순서 ≈ 튜플 순서 tcf_result classid + action rhashtable은 리눅스 커널의 리사이징 가능 해시 — 부하에 따라 자동 확장/축소 → 균일한 O(1) 룩업 보장
cls_flower와 튜플 공간: cls_flower는 마스크별로 해시 테이블을 관리하므로, 튜플 공간 탐색(Tuple Space Search)과 본질적으로 같은 구조입니다. 마스크 수 = 튜플 수이며, 최악의 경우 모든 마스크를 순회합니다. 실제로는 마스크 수가 소수(보통 1~10개)이므로 매우 빠릅니다.

cls_flower 사용 예시와 마스크 최적화

# 같은 마스크(SrcIP/24 + DstPort/exact)를 사용하는 규칙들
# → 하나의 마스크/해시 테이블로 통합 → 효율적
$ tc filter add dev eth0 ingress protocol ip flower \
    src_ip 192.168.1.0/24 ip_proto tcp dst_port 80 \
    action ok

$ tc filter add dev eth0 ingress protocol ip flower \
    src_ip 192.168.2.0/24 ip_proto tcp dst_port 80 \
    action ok

# 다른 마스크(DstIP/16)를 사용하는 규칙
# → 별도 마스크/해시 테이블 생성 → 마스크 수 증가
$ tc filter add dev eth0 ingress protocol ip flower \
    dst_ip 10.0.0.0/16 ip_proto udp dst_port 53 \
    action ok

# 마스크 수 확인
$ tc -s filter show dev eth0 ingress
# 마스크가 2개이면 최대 2회 해시 룩업
마스크 수 관리: cls_flower의 성능은 마스크(튜플) 수에 직접 비례합니다. 규칙을 추가할 때마다 새로운 마스크가 생성되지 않도록, 가능하면 동일한 마스크 패턴(같은 필드, 같은 접두사 길이)으로 규칙을 통일하는 것이 중요합니다. 마스크 수가 100개를 넘으면 성능이 눈에 띄게 저하됩니다.

fl_classify() 커널 코드 분석

fl_classify()(net/sched/cls_flower.c)는 cls_flower의 핵심 분류 함수입니다. 마스크 순회와 rhashtable 룩업의 전체 흐름을 분석합니다.

/* net/sched/cls_flower.c — fl_classify() 핵심 흐름 */

static int fl_classify(struct sk_buff *skb, const struct tcf_proto *tp,
                      struct tcf_result *res)
{
    struct cls_fl_head *head = rcu_dereference_bh(tp->root);
    bool post_filter = !tc_skip_sw(tp->cls_flags);
    struct fl_flow_key skb_key;
    struct fl_flow_mask *mask;
    struct cls_fl_filter *f;

    /* 1단계: skb에서 fl_flow_key 추출 (flow dissector 사용) */
    fl_clear_hw_stats(skb, tp, post_filter);
    skb_flow_dissect_meta(skb, &skb_key);
    /* 패킷 헤더에서 MAC, VLAN, IP, L4 등 모든 필드 추출 */
    skb_flow_dissect(skb, &head->dissector,
                     &skb_key, FLOW_DISSECTOR_F_STOP_BEFORE_ENCAP);

    /* 2단계: 등록된 마스크 목록을 순차 순회 */
    list_for_each_entry_rcu(mask, &head->masks, list) {
        struct fl_flow_key mkey;

        /* 3단계: 패킷 키에 현재 마스크 적용 */
        fl_set_masked_key(&mkey, &skb_key, mask);

        /* 4단계: rhashtable에서 마스킹된 키로 O(1) 룩업 */
        f = rhashtable_lookup_fast(&mask->ht, &mkey,
                                   mask->filter_ht_params);
        if (f && f->flags & TCA_CLS_FLAGS_SKIP_SW)
            continue;

        if (f) {
            /* 매칭! 결과 저장 후 액션 평가 */
            *res = f->res;
            return tcf_exts_exec(skb, &f->exts, res);
        }
    }
    return -1;  /* 미스 — 다음 tc filter로 진행 */
}

/* fl_set_masked_key: skb_key & mask → mkey */
static void fl_set_masked_key(struct fl_flow_key *mkey,
                             struct fl_flow_key *key,
                             struct fl_flow_mask *mask)
{
    const long *lkey = (long *) key;
    const long *lmask = (long *) &mask->key;
    long *ldst = (long *) mkey;
    int i;

    /* 워드 단위 AND 연산으로 마스킹 — 매우 빠름 */
    for (i = 0; i < sizeof(*mkey) / sizeof(long); i++)
        ldst[i] = lkey[i] & lmask[i];
}

flow_dissect 통합: skb_flow_dissect()는 패킷 파싱을 담당하며, L2(Ethernet/VLAN), L3(IPv4/IPv6/MPLS), L4(TCP/UDP/ICMP), 터널(VXLAN/GRE) 헤더를 모두 파싱합니다. 결과는 fl_flow_key 구조체에 저장되며, 이후 마스킹과 해시 룩업에 사용됩니다.

u32_classify() 커널 코드 분석

u32_classify()(net/sched/cls_u32.c)는 해시 기반 분류를 수행합니다. 해시 계산과 버킷 선택, 키 매칭 루프를 분석합니다.

/* net/sched/cls_u32.c — u32_classify() 핵심 흐름 */

static int u32_classify(struct sk_buff *skb, const struct tcf_proto *tp,
                       struct tcf_result *res)
{
    struct {
        struct tc_u_knode *knode;
        unsigned int       off;
    } stack[TC_U32_MAXDEPTH];

    struct tc_u_hnode *ht = rcu_dereference_bh(tp->root);
    unsigned int       off = skb_network_offset(skb);  /* IP 헤더 오프셋 */
    struct tc_u_knode *n;
    int                sdepth = 0, depth = 0;

next_ht:
    /* 현재 해시 테이블에서 버킷 선택 */
    n = ht->ht[TC_U32_HASH(ht->handle)];  /* 루트 버킷으로 시작 */

next_knode:
    if (n) {
        struct tc_u32_key *key = n->sel.keys;
        int                i;

        /* 패킷 헤더에서 해시 값 계산 */
        if (n->sel.hmask) {
            unsigned int hval;
            unsigned int *data;

            /* hoff 오프셋의 32비트를 hmask로 AND → 해시 버킷 인덱스 */
            data  = skb_header_pointer(skb, off + n->sel.hoff,
                                       sizeof(hval), &hval);
            if (!data)
                goto out;
            hval = (__force unsigned int) *data & n->sel.hmask;
            hval ^= hval >> 16;
            hval ^= hval >>  8;
            n = ht->ht[TC_U32_HASH(hval)];  /* 버킷 선택 */
            goto next_knode;
        }

        /* 키 배열 순차 비교: 모든 key가 매칭되어야 함 */
        for (i = n->sel.nkeys; i > 0; i--, key++) {
            unsigned int *data;
            __be32        hdata;

            data = skb_header_pointer(skb, off + key->off,
                                      sizeof(hdata), &hdata);
            if (!data)
                goto next;
            /* (패킷 데이터 & key->mask) == key->val */
            if ((*data & key->mask) != key->val)
                goto next;      /* 불매칭 → 다음 knode */
        }

        /* 모든 키 매칭: ht_down이 있으면 다음 레벨로 */
        if (n->ht_down) {
            stack[sdepth].knode = n;
            stack[sdepth].off   = off;
            sdepth++;
            ht  = n->ht_down;
            off = off + n->sel.offmask;  /* 오프셋 조정 */
            goto next_ht;
        }

        /* 최종 매칭: 액션 적용 */
        *res = n->res;
        return tcf_exts_exec(skb, &n->exts, res);

next:
        n = rcu_dereference_bh(n->next);   /* 체인의 다음 knode */
        goto next_knode;
    }
out:
    return -1;
}

nftables 패킷 분류와의 비교

nftables는 TC 분류기와는 다른 방식으로 패킷 분류를 구현합니다. nf_tables_eval()을 통한 규칙 평가와 내장 set/map 인프라를 이해하면 TC 분류기와의 적절한 선택 기준을 알 수 있습니다.

/* net/netfilter/nf_tables_core.c — nf_tables_eval() */

unsigned int nft_do_chain(struct nft_pktinfo *pkt, void *priv)
{
    const struct nft_chain *chain = priv;
    const struct nft_rule_dp *rule = nft_rule_first(chain);
    unsigned int stackptr = 0;

next_rule:
    /* 규칙을 순차 평가 — 기본적으로 선형 탐색 */
    while (!nft_rule_is_last(chain, rule)) {
        const struct nft_expr *expr = nft_expr_first(rule);

        while (nft_expr_more(rule, expr)) {
            /* 각 매치/액션 표현식 평가 */
            if (expr->ops->eval(pkt, pkt->regs, expr) ==
                NFT_BREAK)
                goto next_rule;
            expr = nft_expr_next(expr);
        }
        /* 모든 표현식 통과 → verdict 반환 */
        switch (pkt->regs.verdict.code) {
        case NFT_CONTINUE:
            rule = nft_rule_next(rule);
            continue;
        case NFT_JUMP: case NFT_GOTO:
            /* 체인 점프: nft_set lookup으로 구현 */
            chain = pkt->regs.verdict.chain;
            goto next_rule;
        }
    }
    return nft_rule_default_verdict(chain);
}

/* nftables set: 다양한 백엔드 지원 */
/* hash set:   O(1) 룩업 — exact match */
/* rbtree set: O(log N) — ranges, intervals */
/* bitmap set: O(1) — small integer domains */
/* pipapo:     Piece-wise Incrementally Pruning AND-ing of Partial Overlaps */
/*             — 다차원 매칭 최적화 (kernel 5.6+) */

pipapo(Piece-wise Incrementally Pruning AND-ing of Partial Overlaps)는 nftables의 고급 set 백엔드로, 다차원 매칭(RFC와 유사한 비트맵 교차)을 구현합니다. 단순 hash/rbtree보다 복잡한 규칙셋에서 우수한 성능을 보입니다.

nftables 패킷 분류 파이프라인 nftables의 체인 평가, set/map 룩업, pipapo 다차원 매칭 파이프라인을 보여줍니다. TC 분류기와의 차이점을 강조합니다. nftables 분류 파이프라인 vs TC Classifiers nftables Netfilter Hook PREROUTING/INPUT/... 체인 선형 평가 nft_do_chain() Set/Map 백엔드 (다차원 매칭 핵심) hash O(1) exact rbtree O(logN) range bitmap O(1) small pipapo 다차원 AND verdict: accept/drop/jump/goto TC Classifiers (cls_flower) TC egress/ingress tc qdisc hook 마스크별 rhashtable fl_classify() Tuple Space Search • mask_list 순회: O(T) (T = 마스크 수) • 각 마스크: skb_key & mask → rhashtable O(1) • HW offload: tc flower rules → NIC TCAM action: ok/drop/redirect/...
특성nftablesTC cls_flower
훅 포인트Netfilter (prerouting/input/output/...)TC egress/ingress
분류 알고리즘선형 평가 + set/map (hash/rbtree/pipapo)Tuple Space Search
다차원 매칭pipapo (kernel 5.6+)마스크별 rhashtable
HW 오프로드제한적 (일부 NIC, offload-to-tc)광범위 (mlx5, ice, bnxt)
업데이트 비용O(1) (set 삽입)O(1) (rhashtable 삽입)
용도방화벽, NAT, 정책 라우팅QoS, 분류, 오프로드

XDP 기반 초고속 분류

XDP(eXpress Data Path)는 NIC 드라이버에서 패킷을 받자마자 처리하는 가장 빠른 경로입니다. 커널 네트워크 스택을 우회하므로 TC보다 훨씬 낮은 오버헤드로 패킷을 분류할 수 있습니다.

XDP 분류의 이점:

/* XDP BPF 프로그램: LPM 트라이 + 해시 맵으로 5-tuple 분류 */
/* kernel/samples/bpf/ 스타일 예시 */

/* BPF 맵 정의 */
struct {
    __uint(type, BPF_MAP_TYPE_LPM_TRIE);  /* Src/Dst IP 접두사 */
    __uint(max_entries, 1024);
    __type(key, struct bpf_lpm_trie_key_u8);
    __type(value, __u32);                 /* 정책 ID */
    __uint(map_flags, BPF_F_NO_PREALLOC);
} ip_policy_map SEC(".maps");

struct {
    __uint(type, BPF_MAP_TYPE_HASH);      /* 5-tuple → 정책 */
    __uint(max_entries, 100000);
    __type(key, struct flow_key5);
    __type(value, __u32);
} flow_policy_map SEC(".maps");

struct flow_key5 {
    __u32 src_ip, dst_ip;
    __u16 src_port, dst_port;
    __u8  proto;
};

SEC("xdp")
int xdp_classifier(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;
    struct flow_key5 key = {};
    __u32 *policy;

    /* 헤더 파싱 */
    if (eth + 1 > (struct ethhdr *)data_end)
        return XDP_DROP;
    if (eth->h_proto != bpf_htons(ETH_P_IP))
        return XDP_PASS;

    ip = (struct iphdr *)(eth + 1);
    if (ip + 1 > (struct iphdr *)data_end)
        return XDP_DROP;

    key.src_ip = ip->saddr;
    key.dst_ip = ip->daddr;
    key.proto  = ip->protocol;

    if (ip->protocol == IPPROTO_TCP) {
        tcp = (struct tcphdr *)((u8 *)ip + (ip->ihl << 2));
        if (tcp + 1 > (struct tcphdr *)data_end)
            return XDP_DROP;
        key.src_port = tcp->source;
        key.dst_port = tcp->dest;
    }

    /* 1단계: 5-tuple 해시 룩업 (정확 매칭) */
    policy = bpf_map_lookup_elem(&flow_policy_map, &key);
    if (policy)
        return (*policy == POLICY_DROP) ? XDP_DROP : XDP_PASS;

    /* 2단계: IP 접두사 LPM 트라이 룩업 */
    struct {
        struct bpf_lpm_trie_key_u8 lpm;
        __u32 addr;
    } lpm_key = { .lpm.prefixlen = 32, .addr = ip->daddr };

    policy = bpf_map_lookup_elem(&ip_policy_map, &lpm_key.lpm);
    if (policy && *policy == POLICY_DROP)
        return XDP_DROP;

    return XDP_PASS;
}
분류 위치오버헤드처리량 (64B)지연적합 용도
XDP native최소 (no skb)~100Mpps/코어~100nsDDoS 방어, 고속 필터링
TC ingress낮음 (skb 있음)~30Mpps/코어~300nsQoS, 일반 필터
iptables중간 (conntrack)~10Mpps/코어~1μs방화벽, NAT
nftables중간 (체인 평가)~15Mpps/코어~700ns방화벽, NAT (현대적)
XDP vs TC 선택 기준: DDoS 방어나 100Gbps+ 라인 레이트 필터링에는 XDP가 필수입니다. 그러나 XDP는 sk_buff가 없어 conntrack, NF_ACCEPT, 소켓 정보 접근이 불가합니다. 일반 방화벽/QoS에는 TC 또는 nftables가 더 적합합니다. 현실적으로는 XDP로 1차 빠른 필터링(DDoS 완화), TC로 2차 정밀 분류(QoS/정책)를 조합하는 계층적 설계가 최선입니다.

BPF 기반 다차원 분류

cls_bpf(net/sched/cls_bpf.c)는 BPF 프로그램으로 패킷을 분류합니다. BPF의 프로그래밍 자유도를 활용하면 RFC 아이디어를 포함한 다양한 분류 전략을 구현할 수 있습니다.

BPF 맵을 활용한 RFC-like 패턴

/* RFC 아이디어를 BPF 맵으로 구현하는 설계 예시 */

/* Phase 0 테이블: 각 필드별 해시 맵 */
struct {
    __uint(type, BPF_MAP_TYPE_HASH);
    __uint(max_entries, 65536);
    __type(key, __u16);        /* 필드 값 (예: dst port) */
    __type(value, __u8);       /* eqID */
} phase0_dport SEC(".maps");

/* Phase 1 테이블: 결합된 eqID → 최종 classID */
struct {
    __uint(type, BPF_MAP_TYPE_HASH);
    __uint(max_entries, 4096);
    __type(key, struct combined_key);  /* {eqID_src, eqID_dst, eqID_port} */
    __type(value, __u32);              /* classID → TC class */
} phase1_combined SEC(".maps");

SEC("classifier")
int rfc_classify(struct __sk_buff *skb)
{
    /* Phase 0: 각 필드에서 eqID 조회 */
    __u16 dport = skb->data[/* dst port offset */];
    __u8 *eq_dport = bpf_map_lookup_elem(&phase0_dport, &dport);

    /* ... 다른 필드도 동일하게 */

    /* Phase 1: 결합 조회 */
    struct combined_key ckey = { .eq_src = *eq_src, .eq_dst = *eq_dst, .eq_port = *eq_dport };
    __u32 *classid = bpf_map_lookup_elem(&phase1_combined, &ckey);

    return classid ? *classid : TC_ACT_OK;
}

이 패턴의 장점은 BPF 맵을 유저스페이스에서 업데이트할 수 있으므로, RFC의 사전 계산을 유저스페이스 데몬이 수행하고 커널의 BPF 프로그램은 순수한 룩업만 수행하는 구조가 가능합니다.

하드웨어 분류기

최신 NIC/SmartNIC은 하드웨어 수준의 패킷 분류를 제공하며, TC flower offload를 통해 리눅스 커널과 통합됩니다.

NIC TCAM / Flow Table

SmartNIC/DPU의 RFC-like 파이프라인

NVIDIA BlueField-3 같은 DPU는 하드웨어 내에 다단계 매칭 파이프라인을 구현하여, RFC 알고리즘과 유사한 방식으로 와이어 스피드 분류를 수행합니다.

TC flower → HW 오프로드 파이프라인 TC flower 규칙이 커널 cls_flower를 거쳐 NIC 드라이버로 전달되고, 하드웨어의 다단계 매칭 파이프라인에서 와이어 스피드로 분류되는 전체 흐름을 보여줍니다. TC flower HW 오프로드: 규칙 설치 → 와이어 스피드 분류 ─── 규칙 설치 경로 (control plane) ─── tc filter add flower + action cls_flower.c fl_change() fl_hw_replace_filter() → ndo_setup_tc() NIC 드라이버 mlx5/ice/bnxt NIC / SmartNIC 하드웨어 (와이어 스피드) ─── 패킷 분류 경로 (data plane, CPU 바이패스) ─── 패킷 IN Parser 헤더 필드 추출 TCAM + Hash Flow Table 매칭 (RFC Phase 0~1 대응) Action Engine drop/fwd/encap 패킷 OUT 처리량: 100Gbps+ (패킷당 ~6.7ns @ 100GbE, 64B 패킷) 지연: ~1μs (하드웨어 파이프라인) vs ~10μs (소프트웨어) 한계: TCAM/Flow Table 용량, 지원 매치 필드, 액션 종류 제한 확인: tc -s filter show → in_hw / not_in_hw 플래그
/* TC flower offload → HW 분류기 매핑 흐름 */

/* 사용자 공간: tc flower 규칙 추가 */
$ tc filter add dev eth0 ingress protocol ip flower \
    src_ip 192.168.1.0/24 dst_port 80 \
    action drop

/* 커널 경로: */
/* 1. cls_flower.c: fl_change() → 규칙 파싱 */
/* 2. cls_flower.c: fl_hw_replace_filter() → NIC 드라이버 호출 */
/* 3. 드라이버: ndo_setup_tc() → HW flow table에 규칙 삽입 */
/* 4. HW에서 패킷 분류 수행 — CPU 바이패스 */

/* 오프로드 상태 확인 */
$ tc -s filter show dev eth0 ingress
/* in_hw → HW에서 처리 중 (CPU 바이패스) */
/* not_in_hw → SW 폴백 (CPU 처리) */

/* 소프트웨어 폴백 금지 (오프로드 실패 시 에러) */
$ tc filter add dev eth0 ingress protocol ip flower \
    skip_sw \
    src_ip 192.168.1.0/24 dst_port 80 \
    action drop
/* skip_sw: SW 경로 건너뛰기 → 오프로드 실패 시 EOPNOTSUPP */
오프로드 한계: 모든 TC flower 규칙이 오프로드 가능한 것은 아닙니다. NIC의 TCAM/flow table 용량, 지원되는 매치 필드, 액션 종류에 따라 소프트웨어 폴백이 발생합니다. 주요 제한 사항:
NICFlow Table 용량지원 매치 필드주요 액션
mlx5 (CX-6+)~1M+ (FDB)L2-L4, VXLAN, GRE, CTdrop, fwd, encap/decap, mirror, count
ice (E810+)~16K (ACL), ~2K (FDir)L2-L4, VLAN, GTPdrop, fwd, queue, mark
bnxt (P5+)~8K (EM), ~1K (WC)L2-L4, VLAN, VxLANdrop, fwd, decap, count

알고리즘 비교

알고리즘 분류 속도 메모리 업데이트 비용 구현 복잡도 실제 사용처
선형 탐색 O(N·d) O(N·d) O(1) 매우 낮음 TC filter chain, iptables
결정 트리 (HiCuts) O(트리 깊이) O(N·2d) 최악 O(트리 깊이) 중간 학술 연구, 일부 ASIC
튜플 공간 O(T) (T = 튜플 수) O(N) O(1) 중간 Open vSwitch, cls_flower
RFC O(D) (D ≈ 3~4) 수 MB~수십 MB 전체 재계산 높음 ASIC, SmartNIC 파이프라인
TCAM O(1) 하드웨어 제한 O(1) 하드웨어 NIC, 스위치 ASIC

규칙 수별 권장 전략

규칙 수권장 접근근거
~100개선형 탐색 / cls_u32캐시 효율로 복잡한 알고리즘보다 빠를 수 있음
100~1,000개cls_flower (튜플 공간)해시 기반으로 O(T) 분류, 마스크 수가 적으면 효율적
1,000~10,000개cls_bpf + BPF 맵 / HW 오프로드소프트웨어 한계, 하드웨어 활용 시작
10,000개+HW TCAM / SmartNIC 파이프라인소프트웨어만으로는 와이어 스피드 불가

conntrack과 패킷 분류의 관계

연결 추적(Connection Tracking, conntrack)은 패킷 분류의 오버헤드를 극적으로 줄이는 핵심 최적화 메커니즘입니다. 첫 번째 패킷만 전체 분류 과정을 거치고, 이후 동일 연결의 패킷들은 conntrack 항목에서 직접 결과를 조회합니다.

연결 추적 기반 빠른 경로

conntrack은 각 TCP/UDP 연결에 대한 상태 항목(nf_conn)을 유지합니다. 분류 결과를 이 항목에 캐싱하면 후속 패킷은 전체 규칙셋을 다시 탐색할 필요가 없습니다.

/* conntrack 기반 분류 최적화 흐름 */

/* 첫 번째 패킷 (NEW 상태): */
/* 1. conntrack 항목 생성 (nf_conntrack_in) */
/* 2. 전체 분류기 체인 평가 (cls_flower TSS) */
/* 3. 결과(classid, mark)를 nf_conn에 저장 */
/* 4. 패킷 처리 */

/* 후속 패킷 (ESTABLISHED 상태): */
/* 1. conntrack 해시에서 nf_conn 조회 → O(1) */
/* 2. nf_conn.mark로 즉시 분류 결정 */
/* 3. 패킷 처리 (분류기 체인 평가 생략!) */

/* 예시: ct mark 기반 빠른 분류 */
/* 첫 패킷: tc flower로 분류 → action connmark save */
$ tc filter add dev eth0 ingress protocol ip flower \
    src_ip 192.168.1.0/24 ip_proto tcp dst_port 80 \
    action connmark save mask 0xffff \     # ct mark 저장
    action mark set 0x0100 pipe \          # skb mark 설정
    action gact ok

/* 후속 패킷: connmark restore로 바로 결정 */
$ tc filter add dev eth0 ingress protocol ip prio 1 flower \
    action connmark restore mask 0xffff    # ct mark → skb mark 복원

ct zone과 mark 기반 빠른 경로

conntrack zone을 사용하면 서로 다른 네임스페이스나 VRF에서 동일한 IP 주소를 독립적으로 추적할 수 있습니다. zone + mark 조합으로 복잡한 정책도 첫 번째 패킷에만 평가하고 이후 패킷은 O(1)로 처리합니다.

/* ct zone 기반 분리 추적 */
$ tc filter add dev eth0 ingress protocol ip flower \
    action ct zone 10 commit            # zone 10에 연결 추적

/* conntrack 항목 직접 확인 */
$ conntrack -L -p tcp --dport 80
tcp 6 431999 ESTABLISHED src=192.168.1.100 dst=10.1.2.3 sport=49152 dport=80 \
    src=10.1.2.3 dst=192.168.1.100 sport=80 dport=49152 [ASSURED] \
    mark=256 zone=10 use=2

/* nf_conn.mark를 이용한 정책 우회 */
$ iptables -t mangle -A PREROUTING -m connmark --mark 0x100/0x100 \
    -j ACCEPT   # mark된 연결은 즉시 ACCEPT
conntrack을 이용한 패킷 분류 최적화 흐름 첫 번째 패킷은 전체 분류기 체인을 통과하여 conntrack 항목에 결과를 저장하고, 이후 패킷은 conntrack 해시에서 직접 결과를 조회하는 빠른 경로를 보여줍니다. conntrack 기반 분류 최적화: 첫 패킷 vs 후속 패킷 ── 첫 번째 패킷 (TCP SYN, NEW 상태) ── SYN 패킷 → NEW conntrack 생성 nf_conn 항목 생성 전체 분류기 평가 TSS: 마스크 순회 O(T) — 느림 결과 저장 nf_conn.mark = classid connmark save 패킷 처리 액션 적용 ── 후속 패킷 (ESTABLISHED 상태) ── ACK 패킷 → ESTABLISHED conntrack 해시 조회 O(1) — hash 직접 조회 connmark restore nf_conn.mark → skb.mark 분류기 체인 완전 생략! 즉시 패킷 처리 mark 기반 정책 적용 (분류기 불필요) conntrack 기반 최적화 효과 • TCP 연결 첫 패킷: 전체 분류 → 약 1μs (마스크 수에 비례) • TCP 연결 후속 패킷: conntrack 조회 → 약 50ns (97x 가속) • 실제 트래픽에서 연결당 평균 수십~수백 패킷 → 전체 분류 오버헤드의 1% 미만 • 주의: UDP/ICMP 같이 conntrack이 없는 경우 매 패킷마다 분류 필요 nf_conn 구조체 핵심 필드 struct nf_conn { struct nf_conntrack_tuple_hash tuplehash[IP_CT_DIR_MAX]; /* 해시 체인 */ unsigned long status; /* IPS_CONFIRMED | IPS_ASSURED | ... */ u32 mark; /* connmark — 분류 결과 저장 */ /* ct zone: 독립 추적 영역 */ }

conntrack 최적화 실용 패턴

/* 패턴 1: TC + connmark 조합 */
/* 첫 패킷에만 cls_flower 평가, 이후 connmark로 바이패스 */
$ tc filter add dev eth0 ingress prio 10 protocol ip flower \
    ct_state tracked                         # 추적된 연결은 connmark 확인
    action connmark restore mask 0xff00 pipe \
    action goto chain 100                    # chain 100에서 mark 기반 처리

/* 추적 안 된 패킷: 전체 분류 수행 후 mark 설정 */
$ tc filter add dev eth0 ingress prio 20 protocol ip flower \
    src_ip 192.168.1.0/24 ip_proto tcp dst_port 80 \
    action ct commit mark 0x0100 mask 0xff00 \  # ct mark 저장
    action ok

/* 패턴 2: BPF + sk_lookup으로 소켓 기반 분류 */
/* 이미 소켓이 있는 연결은 sk->sk_priority로 즉시 분류 */
SEC("tc") int classify_by_socket(struct __sk_buff *skb) {
    struct bpf_sock *sk = bpf_skc_lookup_tcp(skb, ...);
    if (sk) {
        int prio = sk->priority;   /* 소켓 우선순위로 분류 */
        bpf_sk_release(sk);
        return TC_ACT_OK;
    }
    /* 새 연결: 전체 분류 */
    return full_classify(skb);
}

패킷 분류 분야는 SDN, 클라우드 네트워킹, 스마트NIC의 발전에 따라 지속적으로 연구가 이루어지고 있습니다. 최근 주요 연구 성과와 실무 동향을 살펴봅니다.

PartitionSort (2017)

Daly et al. (2017)이 제안한 PartitionSort는 규칙 집합을 겹치지 않는 부분으로 분할하여 각 파티션 내에서 정렬 기반 탐색을 수행합니다. 파티션 내 규칙들은 우선순위 순서로 정렬되어 이진 탐색이 가능합니다.

NuevoMatch (2020)

Shalom et al. (2020)의 NuevoMatch는 신경망을 이용해 규칙 집합을 인코딩합니다. 규칙 집합의 구조적 패턴을 ReLU 네트워크가 학습하여, 분류를 행렬 곱셈으로 표현합니다.

P4 프로그래머블 파이프라인

P4(Programming Protocol-independent Packet Processors)는 고정된 파이프라인을 프로그래밍 가능하게 만든 언어입니다. P4 스위치(Intel Tofino, NVIDIA Spectrum)에서 RFC-like 다단계 매칭 파이프라인을 구현할 수 있습니다.

/* P4 코드 예시: RFC Phase 0~1 패턴 */
table phase0_dstport {
    key   = { hdr.tcp.dstPort : exact; }  /* Phase 0: DstPort → eqID */
    actions = { set_dport_class; }
    size  = 65536;
}

table phase0_proto {
    key   = { hdr.ipv4.protocol : exact; } /* Phase 0: Protocol → eqID */
    actions = { set_proto_class; }
    size  = 256;
}

table phase1_combined {
    key = {                               /* Phase 1: 두 eqID 결합 */
        meta.dport_class : exact;
        meta.proto_class : exact;
    }
    actions = { apply_policy; }
    size  = 4096;
}

ML 기반 규칙 배치와 TCAM 최적화

최근 연구에서는 강화학습(RL)을 이용해 TCAM 엔트리 배치를 최적화합니다.

eBPF + 하드웨어 co-design 동향

현재 가장 활발한 실무 동향은 eBPF와 SmartNIC의 협력 설계입니다.

연도연구/기술핵심 기여
2017PartitionSort겹치지 않는 파티션 + 정렬 기반 탐색
2019NeuroCuts강화학습 기반 결정 트리 최적화
2020NuevoMatch신경망으로 규칙셋 인코딩 + GPU 분류
2021pipapo (nftables)커널 내 다차원 비트맵 교차 매칭
2022+P4 + eBPF co-design프로그래머블 HW + 소프트 제어 통합
현재SmartNIC BPF offloadBPF 프로그램 NIC에서 직접 실행

실습 랩 — 직접 만들어보는 패킷 분류

이 섹션에서는 리눅스 네트워크 네임스페이스를 활용하여 실제 물리 인터페이스 없이도 패킷 분류기를 실습할 수 있는 환경을 구축하고, cls_flower, cls_u32, cls_bpf 세 가지 분류기를 직접 설정합니다.

실습 환경 구축

#!/bin/bash
# lab_setup.sh — 패킷 분류 실습 환경 구축
# 루트 권한 필요: sudo bash lab_setup.sh
#
# 구조:
#   ns-client (192.168.100.1) ←veth→ ns-router ←veth→ ns-server (10.0.0.1)
#   ns-router에서 TC 분류기를 설정하여 패킷을 분류합니다.

set -e

# 1. 네임스페이스 생성
ip netns add ns-client
ip netns add ns-router
ip netns add ns-server

# 2. veth 페어 생성 및 연결
ip link add veth-c type veth peer name veth-c-r
ip link set veth-c netns ns-client
ip link set veth-c-r netns ns-router

ip link add veth-s type veth peer name veth-s-r
ip link set veth-s netns ns-server
ip link set veth-s-r netns ns-router

# 3. IP 주소 설정
ip netns exec ns-client ip addr add 192.168.100.1/24 dev veth-c
ip netns exec ns-client ip link set veth-c up
ip netns exec ns-client ip link set lo up

ip netns exec ns-router ip addr add 192.168.100.254/24 dev veth-c-r
ip netns exec ns-router ip addr add 10.0.0.254/24 dev veth-s-r
ip netns exec ns-router ip link set veth-c-r up
ip netns exec ns-router ip link set veth-s-r up
ip netns exec ns-router ip link set lo up
# IP 포워딩 활성화
ip netns exec ns-router sysctl -w net.ipv4.ip_forward=1 > /dev/null

ip netns exec ns-server ip addr add 10.0.0.1/24 dev veth-s
ip netns exec ns-server ip link set veth-s up
ip netns exec ns-server ip link set lo up

# 4. 라우팅 설정
ip netns exec ns-client ip route add default via 192.168.100.254
ip netns exec ns-server ip route add default via 10.0.0.254

# 5. 연결 확인
echo "=== 연결 테스트 ==="
ip netns exec ns-client ping -c 1 10.0.0.1 && echo "✓ 연결 성공!" || echo "✗ 연결 실패"

echo ""
echo "=== 환경 구축 완료 ==="
echo "ns-client (192.168.100.1) ↔ ns-router ↔ ns-server (10.0.0.1)"
echo ""
echo "다음 단계: ns-router에서 TC 분류기를 설정합니다."

실습 1: cls_flower — 5-tuple 기반 분류

cls_flower는 가장 사용하기 쉬운 현대적 분류기입니다. 필드 이름을 직관적으로 지정하여 규칙을 작성합니다.

#!/bin/bash
# lab_flower.sh — cls_flower 분류기 실습
# ns-router에서 실행: sudo bash lab_flower.sh

NS="ip netns exec ns-router"

# 1. ingress qdisc 추가 (TC 분류기의 진입점)
$NS tc qdisc add dev veth-c-r ingress

echo "=== cls_flower 규칙 추가 ==="

# 규칙 1: HTTP 트래픽 (TCP/80) → 허용 (mark 0x10)
$NS tc filter add dev veth-c-r ingress \
    protocol ip prio 1 flower \
    ip_proto tcp dst_port 80 \
    action skbedit mark 0x10 pipe \
    action ok
echo "R1: TCP/80 → ALLOW (mark 0x10)"

# 규칙 2: HTTPS 트래픽 (TCP/443) → 허용 (mark 0x20)
$NS tc filter add dev veth-c-r ingress \
    protocol ip prio 2 flower \
    ip_proto tcp dst_port 443 \
    action skbedit mark 0x20 pipe \
    action ok
echo "R2: TCP/443 → ALLOW (mark 0x20)"

# 규칙 3: DNS 트래픽 (UDP/53) → 허용
$NS tc filter add dev veth-c-r ingress \
    protocol ip prio 3 flower \
    ip_proto udp dst_port 53 \
    action ok
echo "R3: UDP/53 → ALLOW"

# 규칙 4: SSH 트래픽 (TCP/22) → 차단
$NS tc filter add dev veth-c-r ingress \
    protocol ip prio 4 flower \
    ip_proto tcp dst_port 22 \
    action drop
echo "R4: TCP/22 → DROP"

# 규칙 5: 나머지 모든 트래픽 → 차단 (기본 정책)
$NS tc filter add dev veth-c-r ingress \
    protocol ip prio 100 matchall \
    action drop
echo "R5: 기본 → DROP"

# 2. 규칙 확인
echo ""
echo "=== 설치된 규칙 확인 ==="
$NS tc -s filter show dev veth-c-r ingress

# 3. 테스트
echo ""
echo "=== 분류 테스트 ==="

# HTTP 테스트 — 통과해야 함
echo -n "TCP/80 (HTTP):  "
ip netns exec ns-server python3 -c "
import socket; s=socket.socket(); s.setsockopt(socket.SOL_SOCKET,socket.SO_REUSEADDR,1)
s.bind(('0.0.0.0',80)); s.listen(1); s.settimeout(2)
try: c,_=s.accept(); c.close()
except: pass
s.close()
" &
sleep 0.2
ip netns exec ns-client timeout 1 bash -c \
  "echo test | nc -w1 10.0.0.1 80" 2>/dev/null \
  && echo "✓ 통과 (ALLOW)" || echo "✗ 차단됨 (DROP)"
wait 2>/dev/null

# SSH 테스트 — 차단되어야 함
echo -n "TCP/22 (SSH):   "
ip netns exec ns-client timeout 1 bash -c \
  "echo test | nc -w1 10.0.0.1 22" 2>/dev/null \
  && echo "✓ 통과 (ALLOW)" || echo "✗ 차단됨 (DROP) ← 정상!"

# 4. 패킷 카운터 확인
echo ""
echo "=== 패킷 카운터 ==="
$NS tc -s filter show dev veth-c-r ingress | grep -A2 'flower\|matchall'

실습 2: cls_u32 — 오프셋 기반 분류

cls_u32는 패킷의 특정 바이트 오프셋에서 값을 읽어 비교합니다. 유연하지만 설정이 복잡합니다. 아래 예시에서 IP 헤더의 프로토콜 필드와 TCP 목적지 포트를 직접 오프셋으로 지정합니다.

#!/bin/bash
# lab_u32.sh — cls_u32 분류기 실습
# u32는 패킷의 바이트 오프셋을 직접 지정하여 매칭합니다.
#
# IP 헤더 구조 (오프셋은 IP 헤더 시작 기준):
#   오프셋 0:  Version(4b) + IHL(4b) + DSCP(8b) + Total Length(16b)
#   오프셋 8:  TTL(8b) + Protocol(8b) + Checksum(16b)
#   오프셋 12: Source IP (32b)
#   오프셋 16: Destination IP (32b)
#
# TCP/UDP 헤더 (IP 헤더 다음):
#   오프셋 0:  Source Port(16b) + Destination Port(16b)

NS="ip netns exec ns-router"

# 이전 규칙 정리
$NS tc qdisc del dev veth-c-r ingress 2>/dev/null || true
$NS tc qdisc add dev veth-c-r ingress

echo "=== cls_u32 규칙 추가 ==="

# 규칙 1: IP 프로토콜 = TCP(6) AND 목적지 포트 = 80
# match u8 6 0xff at 9     → IP 헤더 오프셋 9에서 1바이트 = Protocol
# match u16 80 0xffff at 22 → IP 헤더 오프셋 20 + TCP 오프셋 2 = Dst Port
$NS tc filter add dev veth-c-r ingress \
    protocol ip prio 1 u32 \
    match ip protocol 6 0xff \
    match ip dport 80 0xffff \
    action ok
echo "R1: protocol=TCP(6) AND dport=80 → ALLOW"

# 규칙 2: 출발지 IP가 192.168.100.0/24 → mark 설정
$NS tc filter add dev veth-c-r ingress \
    protocol ip prio 2 u32 \
    match ip src 192.168.100.0/24 \
    action skbedit mark 0xFF pipe \
    action ok
echo "R2: src=192.168.100.0/24 → ALLOW (mark 0xFF)"

echo ""
echo "=== 설치된 u32 규칙 ==="
$NS tc filter show dev veth-c-r ingress
초급자 포인트 — cls_flower vs cls_u32: cls_flowerdst_port 80처럼 이름으로 필드를 지정하지만, cls_u32match u16 80 0xffff at 22처럼 바이트 오프셋으로 직접 지정합니다. cls_flower가 훨씬 사용하기 쉽고, 하드웨어 오프로드도 지원하므로 새로운 환경에서는 cls_flower를 우선 사용합니다. cls_u32는 비표준 헤더 필드를 매칭해야 할 때 유용합니다.

실습 3: cls_bpf — BPF 프로그램으로 분류

BPF는 가장 유연한 분류기로, 임의의 분류 로직을 프로그래밍할 수 있습니다. 아래는 RFC 아이디어를 활용한 XDP 기반 분류기의 완전한 구현입니다.

/* xdp_classifier.c — XDP 기반 패킷 분류기
 *
 * 이 프로그램은 XDP(eXpress Data Path)에서 동작하여
 * NIC 드라이버 수준에서 패킷을 분류합니다.
 *
 * 컴파일:
 *   clang -O2 -target bpf -c xdp_classifier.c -o xdp_classifier.o
 *
 * 로드:
 *   ip link set dev eth0 xdp obj xdp_classifier.o sec xdp
 *
 * 해제:
 *   ip link set dev eth0 xdp off
 */
#include <linux/bpf.h>
#include <linux/if_ether.h>
#include <linux/ip.h>
#include <linux/tcp.h>
#include <linux/udp.h>
#include <bpf/bpf_helpers.h>
#include <bpf/bpf_endian.h>

/* ── 분류 결과를 저장하는 BPF 맵 ── */

/* Phase 0 맵: 목적지 포트 → 동치 클래스 ID */
struct {
    __uint(type, BPF_MAP_TYPE_HASH);
    __uint(max_entries, 256);       /* 주요 포트만 등록 */
    __type(key, __u16);             /* 포트 번호 */
    __type(value, __u8);            /* eqID */
} port_class SEC(".maps");

/* Phase 1 맵: (eqID_port, protocol) → 액션 */
struct phase1_key {
    __u8 port_eq;
    __u8 proto;
};
struct phase1_val {
    __u32 action;   /* XDP_PASS=2, XDP_DROP=1 */
    __u64 packets;  /* 통계 카운터 */
};

struct {
    __uint(type, BPF_MAP_TYPE_HASH);
    __uint(max_entries, 1024);
    __type(key, struct phase1_key);
    __type(value, struct phase1_val);
} action_table SEC(".maps");

/* 통계: 프로토콜별 패킷 카운터 */
struct {
    __uint(type, BPF_MAP_TYPE_PERCPU_ARRAY);
    __uint(max_entries, 256);
    __type(key, __u32);
    __type(value, __u64);
} stats SEC(".maps");

SEC("xdp")
int xdp_classify(struct xdp_md *ctx)
{
    /* 1. 패킷 데이터 경계 확인 (BPF 검증기 필수) */
    void *data     = (void *)(long)ctx->data;
    void *data_end = (void *)(long)ctx->data_end;

    /* 2. 이더넷 헤더 파싱 */
    struct ethhdr *eth = data;
    if ((void *)(eth + 1) > data_end)
        return XDP_PASS;
    if (eth->h_proto != bpf_htons(ETH_P_IP))
        return XDP_PASS;   /* IPv4가 아니면 통과 */

    /* 3. IP 헤더 파싱 */
    struct iphdr *ip = (void *)(eth + 1);
    if ((void *)(ip + 1) > data_end)
        return XDP_PASS;

    /* 4. 프로토콜별 통계 갱신 */
    __u32 proto_key = ip->protocol;
    __u64 *cnt = bpf_map_lookup_elem(&stats, &proto_key);
    if (cnt)
        (*cnt)++;

    /* 5. TCP/UDP 목적지 포트 추출 */
    __u16 dport = 0;
    if (ip->protocol == IPPROTO_TCP) {
        struct tcphdr *tcp = (void *)ip + ip->ihl * 4;
        if ((void *)(tcp + 1) > data_end)
            return XDP_PASS;
        dport = bpf_ntohs(tcp->dest);
    } else if (ip->protocol == IPPROTO_UDP) {
        struct udphdr *udp = (void *)ip + ip->ihl * 4;
        if ((void *)(udp + 1) > data_end)
            return XDP_PASS;
        dport = bpf_ntohs(udp->dest);
    }

    /* 6. Phase 0: 포트 → eqID 변환 */
    __u8 port_eq = 0xFF;  /* 기본: "나머지" 클래스 */
    __u8 *eq = bpf_map_lookup_elem(&port_class, &dport);
    if (eq)
        port_eq = *eq;

    /* 7. Phase 1: (eqID, protocol) → 액션 조회 */
    struct phase1_key key = {
        .port_eq = port_eq,
        .proto   = ip->protocol
    };
    struct phase1_val *val = bpf_map_lookup_elem(&action_table, &key);
    if (val) {
        val->packets++;
        return val->action;  /* XDP_PASS 또는 XDP_DROP */
    }

    /* 8. 기본 정책: 통과 */
    return XDP_PASS;
}

char _license[] SEC("license") = "GPL";
#!/usr/bin/python3
# xdp_loader.py — XDP 분류기 규칙 로더 (유저스페이스)
#
# 이 스크립트는 BPF 맵에 분류 규칙을 삽입합니다.
# RFC의 "사전 계산"에 해당하는 단계입니다.
#
# 사용법: sudo python3 xdp_loader.py
# 전제: xdp_classifier.o가 이미 로드되어 있어야 합니다.

import struct
import subprocess

def populate_maps():
    # Phase 0: 포트 → eqID 매핑
    port_classes = {
        22:  0,   # SSH    → eqID 0
        53:  1,   # DNS    → eqID 1
        80:  2,   # HTTP   → eqID 2
        443: 3,   # HTTPS  → eqID 3
    }
    # 나머지 포트 → eqID 0xFF (기본)

    # Phase 1: (eqID, protocol) → 액션
    # XDP_PASS=2, XDP_DROP=1
    actions = {
        (2, 6):    2,   # port_eq=2(HTTP) + TCP  → PASS
        (3, 6):    2,   # port_eq=3(HTTPS) + TCP → PASS
        (1, 17):   2,   # port_eq=1(DNS) + UDP   → PASS
        (0, 6):    1,   # port_eq=0(SSH) + TCP   → DROP
        (0xFF, 6): 2,   # 기타 포트 + TCP        → PASS
    }

    print("Phase 0: 포트 → eqID 매핑")
    for port, eq in port_classes.items():
        print(f"  port {port:5d} → eqID {eq}")
        # bpftool map update 명령으로 맵에 삽입
        # bpftool map update pinned /sys/fs/bpf/port_class \
        #   key hex $(printf '%04x' port) \
        #   value hex $(printf '%02x' eq)

    print("\nPhase 1: (eqID, protocol) → 액션")
    for (eq, proto), act in actions.items():
        act_name = "PASS" if act == 2 else "DROP"
        print(f"  (eqID={eq}, proto={proto}) → {act_name}")

populate_maps()
초급자 포인트 — BPF의 2계층 구조: BPF 분류기는 커널 프로그램(데이터 플레인)유저스페이스 제어(컨트롤 플레인)으로 나뉩니다. 커널 프로그램은 패킷당 수 나노초 만에 실행되지만, 복잡한 로직은 넣을 수 없습니다(BPF 검증기 제한). 대신 복잡한 계산(RFC 사전 계산 등)은 유저스페이스에서 수행하고, 결과를 BPF 맵에 저장합니다. 커널 프로그램은 맵을 조회만 합니다.

실습 환경 정리

#!/bin/bash
# lab_cleanup.sh — 실습 환경 정리

ip netns del ns-client 2>/dev/null
ip netns del ns-router 2>/dev/null
ip netns del ns-server 2>/dev/null
echo "실습 환경이 정리되었습니다."

분류기 선택 가이드 — 실습 요약

분류기난이도유연성HW 오프로드언제 사용하는가
cls_flower쉬움중간지원대부분의 경우 (기본 선택)
cls_u32어려움높음제한적비표준 헤더, 레거시 환경
cls_bpf매우 어려움매우 높음SmartNIC복잡한 로직, 동적 규칙, DDoS 방어
nftables중간높음미지원호스트 방화벽, 규칙 < 수백 개

성능 측정과 디버깅

분류기 성능 측정

# TC 필터 통계 확인 (패킷/바이트 카운터)
$ tc -s filter show dev eth0 ingress
filter protocol ip pref 1 flower chain 0 handle 0x1
  eth_type ipv4
  ip_proto tcp
  dst_port 80
  in_hw in_hw_count 1
  action order 1: gact action drop
   random type none pass val 0
   index 1 ref 1 bind 1 installed 35 sec used 2 sec
   Action statistics:
    Sent 1523456 bytes 12847 pkt (dropped 12847, overlimits 0 requeues 0)

# bpftrace로 cls_flower 분류 지연 측정
$ bpftrace -e '
kprobe:fl_classify { @start[tid] = nsecs; }
kretprobe:fl_classify /@start[tid]/ {
    @latency_ns = hist(nsecs - @start[tid]);
    delete(@start[tid]);
}
interval:s:5 { exit(); }'

# perf로 TC 분류 핫스팟 프로파일링
$ perf record -g -e cycles -a -- sleep 10
$ perf report --sort comm,dso,sym | grep -E 'cls_|tcf_'

상세 벤치마크 방법론

분류기 성능을 정확하게 측정하려면 실제 트래픽 패턴과 유사한 부하를 발생시키고, CPU 캐시·NUMA·IRQ 어피니티 등 시스템 요인을 제거해야 합니다.

pktgen을 이용한 패킷 생성

pktgen(커널 내장 고성능 패킷 생성기)은 수백만 PPS를 발생시킬 수 있어 분류기 포화 테스트에 적합합니다.

#!/bin/bash
# pktgen으로 멀티 스트림 트래픽 발생 — 5-tuple 무작위화 포함

# pktgen 스크립트 작성
cat > /tmp/pktgen_test.sh << 'PGSCRIPT'
#!/bin/bash
# CPU 0,1에서 각 1Gbps 트래픽 발생
for cpu in 0 1; do
  echo "rem_device_all" > /proc/net/pktgen/kpktgend_$cpu
  echo "add_device eth0@$cpu"   > /proc/net/pktgen/kpktgend_$cpu
  pgdev=/proc/net/pktgen/eth0@$cpu

  echo "count 5000000"         > $pgdev   # 500만 패킷
  echo "clone_skb 1"           > $pgdev   # SKB 재사용 (캐시 압박 최소화)
  echo "pkt_size 64"           > $pgdev   # 64바이트 최소 프레임
  echo "delay 0"               > $pgdev   # 최대 속도
  echo "dst_min 10.0.0.1"      > $pgdev
  echo "dst_max 10.0.3.255"    > $pgdev   # 목적지 IP 분산 (/22 범위)
  echo "src_min 192.168.0.1"   > $pgdev
  echo "src_max 192.168.3.255" > $pgdev   # 출발지 IP 분산
  echo "udp_src_min 1024"      > $pgdev
  echo "udp_src_max 65535"     > $pgdev   # Ephemeral 포트 범위
  echo "udp_dst_min 80"        > $pgdev
  echo "udp_dst_max 8080"      > $pgdev
  echo "flag UDPSRC_RND"       > $pgdev   # UDP src 포트 무작위화
  echo "flag UDPDST_RND"       > $pgdev   # UDP dst 포트 무작위화
  echo "flag IPSRC_RND"        > $pgdev   # IP src 무작위화
  echo "flag IPDST_RND"        > $pgdev   # IP dst 무작위화
done

echo "start" > /proc/net/pktgen/pgctrl
PGSCRIPT

bash /tmp/pktgen_test.sh
# 결과 확인
cat /proc/net/pktgen/eth0@0  | grep -E 'pps|errors|pkts'

tc-testing 프레임워크

커널 소스의 tools/testing/selftests/tc-testing/은 TC 분류기의 기능 검증과 기본 성능 측정을 위한 프레임워크를 제공합니다.

#!/bin/bash
# tc-testing 프레임워크를 활용한 분류기 성능 비교

# 테스트 네임스페이스 생성 (실 인터페이스 불필요)
ip netns add cls_bench
ip netns exec cls_bench ip link add veth0 type veth peer name veth1
ip netns exec cls_bench ip link set veth0 up
ip netns exec cls_bench ip link set veth1 up

# cls_flower: 100개 규칙 설치 후 처리량 측정
ip netns exec cls_bench tc qdisc add dev veth0 ingress
for i in $(seq 1 100); do
  DPORT=$(( 8000 + i ))
  ip netns exec cls_bench tc filter add dev veth0 ingress \
    protocol ip prio $i flower ip_proto tcp dst_port $DPORT \
    action drop
done
# 규칙 수 확인
ip netns exec cls_bench tc -s filter show dev veth0 ingress | grep -c 'flower'

# pktgen으로 트래픽 발생 → TC 통계 확인
# 10초 후 카운터 변화로 PPS 계산
BEFORE=$(ip netns exec cls_bench tc -s filter show dev veth0 ingress \
  | grep 'Sent' | awk '{sum+=$3} END {print sum}')
sleep 10
AFTER=$(ip netns exec cls_bench tc -s filter show dev veth0 ingress \
  | grep 'Sent' | awk '{sum+=$3} END {print sum}')
echo "10초간 처리 패킷: $(( (AFTER - BEFORE) )) packets"
echo "PPS: $(( (AFTER - BEFORE) / 10 ))"

# 정리
ip netns del cls_bench

perf stat을 이용한 캐시 메트릭 측정

분류기의 CPU 캐시 효율은 성능의 핵심입니다. perf stat으로 LLC(Last Level Cache) 미스율을 측정합니다.

# 분류기 실행 중 캐시 메트릭 측정
$ perf stat -e \
    cache-references,\
    cache-misses,\
    LLC-loads,\
    LLC-load-misses,\
    LLC-stores,\
    LLC-store-misses,\
    cycles,\
    instructions \
  -p $(pgrep ksoftirqd/0) -- sleep 5

 Performance counter stats for process id '1234':

       1,234,567      cache-references          #  123.4 M/sec
         456,789      cache-misses              #   37.0% of all cache refs
         234,567      LLC-loads                 #   23.4 M/sec
          89,012      LLC-load-misses           #   37.9% of all LLC loads
          67,890      LLC-stores                #    6.8 M/sec
           5,678      LLC-store-misses          #    8.4% of all LLC stores
      12,345,678      cycles                    #    1.234 GHz
       8,901,234      instructions              #    0.72  insn per cycle

# 분류기별 캐시 미스 차이 (일반적 관찰값)
# cls_flower (소규모, <20 마스크): LLC miss ~5-10%
# cls_u32 (해시 충돌 없음):        LLC miss ~8-15%
# cls_flower (대규모, >50 마스크): LLC miss ~25-40%
# iptables (500+ 규칙):           LLC miss ~30-50%
# perf record로 함수별 핫스팟 분석
$ perf record -g \
    -e cycles:u \
    -e cache-misses:u \
    --call-graph dwarf \
    -C 0 \
    -- sleep 10

# 분류기 관련 함수만 필터링
$ perf report --sort comm,dso,sym --no-children \
  | grep -E 'fl_classify|u32_classify|nft_do_chain|bpf_prog_run'

# flame graph 생성 (brendangregg/FlameGraph 필요)
$ perf script | stackcollapse-perf.pl | flamegraph.pl \
    --title "Packet Classifier Hot Path" \
    --width 1200 \
    > classifier_flamegraph.svg

NUMA 고려사항

멀티 소켓 서버에서 패킷 분류기는 NUMA 지역성에 민감합니다. NIC 인터럽트와 소프트IRQ 처리가 다른 NUMA 노드에서 발생하면 메모리 접근 지연이 급증합니다.

# NIC IRQ를 특정 NUMA 노드에 고정
# eth0이 PCIe 슬롯 → NUMA node 0에 연결된 경우

# IRQ 어피니티 확인
$ cat /proc/irq/$(grep eth0 /proc/interrupts | awk '{print $1}' | tr -d :)/smp_affinity_list
0-7   # 현재 모든 CPU에 분산

# NUMA node 0 CPU (0-7)에만 IRQ 핀
$ echo 0-7 > /proc/irq/XX/smp_affinity_list

# RPS (Receive Packet Steering)도 같은 노드로
$ echo ff > /sys/class/net/eth0/queues/rx-0/rps_cpus

# XPS (Transmit Packet Steering) 설정
$ echo ff > /sys/class/net/eth0/queues/tx-0/xps_cpus

# numactl로 벤치마크 실행 (NUMA node 0 메모리 강제)
$ numactl --cpunodebind=0 --membind=0 \
    ./packet_classifier_bench --rules 1000 --pps-target 10M

실측 성능 특성

다음은 일반적인 x86-64 서버 (Intel Xeon, 10GbE NIC)에서 측정한 분류기별 성능 수치입니다. 규칙 수와 트래픽 패턴에 따라 크게 달라질 수 있습니다.

분류기규칙 수처리량 (Mpps)지연 (ns/패킷)LLC 미스율특이사항
iptables (선형)1014.2702%규칙 10개 이하 최강
iptables (선형)1003.132015%규칙 비례 선형 저하
iptables (선형)10000.313,20055%캐시 워킹셋 초과
nftables (집합)10008.71158%해시셋 O(1)
nftables (pipapo)10005.219212%멀티필드 최적화
cls_flower (SW)50 마스크4.820818%마스크 수 민감
cls_flower (HW offload)4096148.86.7N/ANIC에서 처리
cls_u32 (해시)2569.310710%해시 충돌 없을 때
cls_bpf (JIT)18.5545%JIT 컴파일 필수
XDP (BPF)24.1413%드라이버 레벨 처리
XDP (native)36.8272%NIC 드라이버 직접

캐시 미스 분석

분류기 성능 저하의 주요 원인은 규칙 테이블이 CPU 캐시 용량을 초과할 때 발생하는 LLC 캐시 미스입니다.

분류기별 LLC 캐시 미스율 vs 규칙 수 0% 10% 20% 40% 60% LLC 미스율 (%) 10 50 100 500 1000 규칙 수 iptables (선형) cls_flower (SW) nftables (set) XDP/BPF 20% ← 성능 저하 임계점
분류기별 LLC 캐시 미스율과 규칙 수의 관계. 20% 이상에서 성능이 급격히 저하됩니다.

캐시 미스 최적화 전략:

분류기별 디버깅 포인트

분류기디버깅 도구핵심 확인 사항
cls_u32tc -s filter show해시 버킷 분포, 충돌율, 링크(ht_down) 체인 깊이
cls_flowertc -s filter show마스크 수, in_hw/not_in_hw, 매칭 카운터
cls_bpfbpftool prog showBPF 프로그램 로드 상태, 실행 시간, jit 여부
HW offloadethtool -S devhw_tc_* 카운터, flow table 사용률

흔한 실수와 주의사항

RFC 메모리 폭발 조건: 규칙셋이 "구조적"이지 않으면(즉, 규칙 간 상관관계가 낮으면) 동치 클래스 축소가 일어나지 않아 교차곱 테이블이 기하급수적으로 커집니다. 랜덤 생성된 합성 규칙셋에서 이 문제가 자주 발생합니다. 실제 ACL은 대부분 구조적이므로 문제가 되지 않지만, 반드시 배포 전에 메모리 사용량을 측정해야 합니다.

실전 체크리스트

상황권장 접근주의사항
규칙 < 50개, 업데이트 빈번iptables / nftables (선형)규칙 순서가 성능에 직접 영향
규칙 50~500개, 정적cls_flower (SW)마스크 수를 10개 이내로 유지
규칙 500+개, 정적cls_flower + HW offloadNIC 용량 확인, in_hw 검증
동적 규칙, 복잡한 로직cls_bpf + BPF 맵BPF 프로그램 복잡도 제한 (verifier)
100Gbps+ 와이어 스피드SmartNIC/DPU 오프로드지원 매치 필드/액션 사전 확인

실전 사례 연구

이론적 알고리즘이 실제 환경에서 어떻게 적용되는지 세 가지 대표 사례를 살펴봅니다.

사례 1: 데이터센터 ACL (10K+ 규칙)

클라우드 데이터센터의 가상머신 네트워크 격리에는 수만 개의 보안 그룹 규칙이 사용됩니다. AWS, GCP, Azure 모두 이 문제를 다양한 방식으로 해결합니다.

문제 구조:

데이터센터 ACL: 다계층 분류 파이프라인 VM 패킷 (vNIC) 계층 1: OVS EMC (정확 매칭 캐시) hash(5-tuple) O(1) 룩업 용량: 8K 플로우 히트율: ~90% 동작: 즉시 전달 (액션 캐시됨) 계층 2: OVS TSS (튜플 공간 탐색) 마스크별 해시 O(M) M=마스크 수 규칙: 수천 개 히트율: ~9% 플로우 생성 후 EMC에 캐시 계층 3: Upcall (사용자 공간) OpenFlow 매칭 전체 정책 평가 보안 그룹 10K+ 히트율: ~1% 지연: 100μs~1ms 새 플로우 설치 miss miss 즉시 전달 (ns) 전달 (μs) 전달 (ms) 성능 요약 EMC: ~20ns TSS: ~2μs Upcall: ~500μs 평균: ~25ns
데이터센터 ACL의 3계층 분류 파이프라인. EMC→TSS→Upcall 계층을 거치며, 90% 이상의 트래픽은 EMC에서 처리됩니다.

핵심 설계 결정:

사례 2: 5G UPF 패킷 분류

5G 네트워크의 UPF(User Plane Function)는 모바일 가입자의 QoS(Quality of Service) 정책을 강제합니다. 3GPP TS 23.501에 따른 QFI(QoS Flow Identifier) 기반 분류가 핵심입니다.

5G QoS 분류 요구사항:

/* 5G UPF BPF 분류기: GTP-U 파싱 + 내부 헤더 매칭 */

struct gtp_hdr {
    __u8  flags;
    __u8  msg_type;
    __be16 length;
    __be32 teid;       /* Tunnel Endpoint ID */
};

struct sdf_template {
    __be32 src_ip;
    __be32 src_mask;
    __be32 dst_ip;
    __be32 dst_mask;
    __be16 src_port_min;
    __be16 src_port_max;
    __be16 dst_port_min;
    __be16 dst_port_max;
    __u8   protocol;
    __u8   qfi;        /* 매칭 시 할당할 QFI */
    __u32  precedence; /* 우선순위 (낮을수록 우선) */
};

/* TEID → QoS 정책 맵 (다운링크) */
struct {
    __uint(type, BPF_MAP_TYPE_HASH);
    __uint(max_entries, 1 << 20);  /* 100만 가입자 */
    __type(key, __be32);            /* TEID */
    __type(value, struct sdf_template[64]); /* 가입자별 64개 SDF */
} teid_policy_map SEC(".maps");

/* 가입자별 토큰 버킷 (GBR 시행) */
struct token_bucket {
    __u64 tokens;
    __u64 last_ts;
    __u64 rate_bps;     /* 허용 비트레이트 */
    __u64 burst_bytes;  /* 최대 버스트 */
};

struct {
    __uint(type, BPF_MAP_TYPE_PERCPU_HASH);
    __uint(max_entries, 1 << 20);
    __type(key, __u64);             /* (TEID << 8) | QFI */
    __type(value, struct token_bucket);
} tbucket_map SEC(".maps");

SEC("xdp")
int upf_classify(struct xdp_md *ctx) {
    void *data     = (void *)(long)ctx->data;
    void *data_end = (void *)(long)ctx->data_end;

    /* 1. GTP-U 파싱 (UDP/2152) */
    struct ethhdr *eth = data;
    struct iphdr  *iph = data + sizeof(*eth);
    struct udphdr *udp = data + sizeof(*eth) + sizeof(*iph);
    struct gtp_hdr *gtp = data + sizeof(*eth) + sizeof(*iph) + sizeof(*udp);
    if ((void *)(gtp + 1) > data_end) return XDP_PASS;

    if (udp->dest != bpf_htons(2152)) return XDP_PASS;  /* GTP-U 포트 아님 */
    __be32 teid = gtp->teid;

    /* 2. TEID → SDF 템플릿 룩업 */
    struct sdf_template (*sdfs)[64] = bpf_map_lookup_elem(&teid_policy_map, &teid);
    if (!sdfs) return XDP_PASS;  /* 알 수 없는 TEID → 상위로 */

    /* 3. 내부 IP 헤더 파싱 */
    struct iphdr *inner_iph = (void *)(gtp + 1);
    if ((void *)(inner_iph + 1) > data_end) return XDP_DROP;

    /* 4. SDF 템플릿 순차 매칭 (우선순위 순) */
    __u8 matched_qfi = 9;  /* 기본 QFI (Best Effort) */
    for (int i = 0; i < 64; i++) {
        struct sdf_template *t = &(*sdfs)[i];
        if (!t->qfi) break;  /* 빈 슬롯 */
        if ((inner_iph->saddr & t->src_mask) != t->src_ip) continue;
        if ((inner_iph->daddr & t->dst_mask) != t->dst_ip) continue;
        if (inner_iph->protocol != t->protocol) continue;
        matched_qfi = t->qfi;
        break;
    }

    /* 5. 토큰 버킷 GBR 시행 */
    __u64 key = ((__u64)bpf_ntohl(teid) << 8) | matched_qfi;
    struct token_bucket *tb = bpf_map_lookup_elem(&tbucket_map, &key);
    if (tb) {
        __u64 now = bpf_ktime_get_ns();
        __u64 elapsed = now - tb->last_ts;
        tb->tokens += (elapsed * tb->rate_bps) / 8000000000ULL;
        if (tb->tokens > tb->burst_bytes) tb->tokens = tb->burst_bytes;
        tb->last_ts = now;
        if (tb->tokens < ctx->data_end - ctx->data)
            return XDP_DROP;  /* GBR 초과 → 폐기 */
        tb->tokens -= (ctx->data_end - ctx->data);
    }

    /* 6. QFI 메타데이터 저장 → 이후 qdisc에서 DSCP 마킹 */
    bpf_xdp_store_bytes(ctx, 0, &matched_qfi, sizeof(matched_qfi));  /* 단순화 */
    return XDP_PASS;
}

5G UPF 분류 최적화 포인트:

사례 3: DDoS 방어 시스템 — 동적 규칙 업데이트

DDoS 방어는 공격 패턴이 실시간으로 변하므로 규칙 업데이트 성능이 트래픽 처리 성능만큼 중요합니다. 분류기 선택 기준이 일반 ACL과 다릅니다.

DDoS 방어의 특수 요구사항:

DDoS 방어: XDP + BPF 맵 동적 업데이트 인터넷 (공격 포함) XDP Hook (드라이버 레벨) BPF LPM Trie 룩업 30-40ns/패킷 BPF_MAP: blocklist LPM_TRIE: src_ip/prefix max: 1M 엔트리 lock-free RCU BPF_MAP: stats PERCPU_HASH src_ip → {pkts, bytes} 원자적 카운터 BPF_MAP: ttl HASH: ip → expire_ns 만료 시 자동 삭제 제어 평면 (유저스페이스) 공격 감지 엔진 (ML 또는 임계값) 규칙 관리자 추가/삭제/TTL 갱신 통계 수집기 stats 맵 폴링/집계 경보/대시보드 Prometheus/Grafana XDP_DROP (차단) XDP_PASS (정상 트래픽) 처리 성능 차단: 100Mpps+ 규칙추가: <10μs TTL 만료: 1초 주기 1M IP 차단 지원
XDP 기반 DDoS 방어 시스템의 아키텍처. BPF 맵을 통해 유저스페이스에서 실시간으로 차단 목록을 업데이트하면서 XDP 레벨에서 100Mpps 이상을 처리합니다.
/* DDoS 방어: 공격 IP TTL 기반 자동 만료 */

struct block_entry {
    __u64 expire_ns;  /* 0이면 영구 차단 */
    __u32 reason;     /* 차단 사유 코드 */
};

struct {
    __uint(type, BPF_MAP_TYPE_LPM_TRIE);
    __uint(max_entries, 1 << 20);   /* 최대 100만 접두사 */
    __type(key, struct bpf_lpm_trie_key_u8[8]);  /* prefix_len + addr */
    __type(value, struct block_entry);
    __uint(map_flags, BPF_F_NO_PREALLOC);  /* 동적 할당 */
} blocklist SEC(".maps");

SEC("xdp")
int ddos_filter(struct xdp_md *ctx) {
    void *data     = (void *)(long)ctx->data;
    void *data_end = (void *)(long)ctx->data_end;

    struct ethhdr *eth = data;
    struct iphdr  *iph = data + sizeof(*eth);
    if ((void *)(iph + 1) > data_end) return XDP_PASS;

    /* LPM Trie 차단 목록 조회 */
    struct {
        __u32 prefix_len;
        __u8  addr[4];
    } key = { .prefix_len = 32, .addr = {} };
    __builtin_memcpy(key.addr, &iph->saddr, 4);

    struct block_entry *entry = bpf_map_lookup_elem(&blocklist, &key);
    if (entry) {
        /* TTL 확인 */
        if (entry->expire_ns == 0 ||
            bpf_ktime_get_ns() < entry->expire_ns) {
            /* 통계 갱신 (PERCPU → 잠금 불필요) */
            update_stats(iph->saddr, data_end - data);
            return XDP_DROP;
        }
        /* 만료된 항목 — 삭제 (BPF에서 직접 삭제 가능) */
        bpf_map_delete_elem(&blocklist, &key);
    }

    return XDP_PASS;
}
#!/usr/bin/python3
# 유저스페이스 DDoS 제어 평면: 공격 감지 → 규칙 추가/삭제

import bcc from BPF
import ctypes, time, struct

# 공격 감지: 초당 1000패킷 이상 출발지 IP를 차단
THRESHOLD_PPS = 1000
BLOCK_TTL_SECS = 300  # 5분 후 자동 해제

def block_ip(blocklist_map, src_ip, ttl_secs):
    expire_ns = time.time_ns() + ttl_secs * 10**9
    key = struct.pack("=I4s", 32, src_ip.to_bytes(4, 'big'))
    value = struct.pack("=QI", expire_ns, 0x01)  # reason=rate_exceeded
    blocklist_map[key] = value
    print(f"차단 추가: {src_ip} TTL={ttl_secs}s")

def cleanup_expired(blocklist_map):
    now_ns = time.time_ns()
    expired = []
    for key, val in blocklist_map.items():
        expire_ns = struct.unpack("=Q", val[:8])[0]
        if expire_ns != 0 and expire_ns < now_ns:
            expired.append(key)
    for k in expired:
        del blocklist_map[k]
    if expired:
        print(f"만료 제거: {len(expired)}개 엔트리")

# 1초마다 통계 확인 및 만료 정리
while True:
    time.sleep(1)
    stats = bpf["stats"]
    for src_ip, count in stats.items():
        total = sum(count)  # PERCPU 합산
        if total > THRESHOLD_PPS:
            block_ip(bpf["blocklist"], src_ip.value, BLOCK_TTL_SECS)
    cleanup_expired(bpf["blocklist"])
    stats.clear()

DDoS 방어 분류기 선택 기준 요약:

요구사항권장 분류기이유
단순 IP 차단 (대량)XDP + BPF_MAP_TYPE_LPM_TRIE100Mpps, LPM으로 /24, /32 혼용
5-tuple 기반 세션 차단XDP + BPF_MAP_TYPE_HASHO(1) 정확 매칭, conntrack 없이
정규표현식/페이로드 검사cls_bpf + TC ingressskb 접근 가능, XDP보다 유연
하드웨어 오프로드 필요cls_flower + SmartNICNIC에서 처리, CPU 부하 제로
동적 규칙 (초당 1000+ 변경)XDP + BPF 맵원자적 맵 업데이트, 락 없음
L7 페이로드 분류sk_lookup BPF + 소켓연결 소켓 연결, 재조립 이후 처리

참고자료

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