Netfilter 프레임워크
Netfilter 훅, nftables/iptables-nft, conntrack/NAT, NFQUEUE/NFLOG, flowtable, bridge/netdev family, TPROXY, BPF Netfilter까지 아우르는 현대 Linux 방화벽(Firewall)/패킷(Packet) 경로 가이드입니다. 훅 우선순위(Priority)와 데이터 경로, 제어 평면, 성능·디버깅(Debugging)·운영 실수를 한 문서에서 연결해 설명합니다.
핵심 요약
- 훅(hook) — 커널이 패킷을 끊어서 검사할 수 있는 시점입니다.
- family —
ip,ip6,inet,bridge,netdev처럼 규칙이 적용되는 경로 공간입니다. - base chain — 특정 훅과 priority에 묶여 실제 패킷이 진입하는 nftables 체인입니다.
- conntrack — 패킷 하나가 아니라 연결 단위로 상태를 기억하는 엔진입니다.
- NAT — 보통 첫 패킷에서 변환 정책을 정하고 이후 패킷은 conntrack 상태를 재사용합니다.
- flowtable — 일부 포워딩 트래픽을 classic hook 경로 밖 fast path로 우회시키는 메커니즘입니다.
단계별 이해
- 경로부터 구분
로컬 수신인지, 포워딩인지, 브릿지인지, netdev ingress인지 먼저 나눕니다. - 훅 시점을 고정
prerouting,input,forward,output,postrouting중 어디에서 봐야 하는지 정합니다. - 상태 엔진을 확인
conntrack, NAT, helper, flowtable이 이미 패킷 의미를 바꿨는지 봅니다. - 제어 평면을 분리
nft,iptables-nft,iptables-legacy, NFQUEUE, BPF 중 누가 규칙을 넣었는지 분리합니다. - 증상을 역추적(Backtrace)
nft monitor trace, conntrack 이벤트, NFLOG, bpftool로 실제 verdict가 어디서 나왔는지 확인합니다.
include/uapi/linux/netfilter*.h, include/uapi/linux/bpf.h), docs.kernel.org의 TPROXY/flowtable 문서, netfilter.org의 nftables man page를 기준으로 보강했습니다.
현대 운영 기준의 기본 제어 평면은 nftables이며, iptables는 주로 iptables-nft 호환 프런트엔드와 레거시 x_tables 비교 관점에서 설명합니다.
Netfilter 개요
Netfilter는 리눅스 커널의 패킷 처리 프레임워크로서, 네트워크 스택의 전략적 지점에 훅(hook)을 삽입하여 패킷을 검사, 수정, 폐기, 큐잉할 수 있습니다. 방화벽(iptables/nftables), NAT, 연결 추적(conntrack), 패킷 맹글링, 로깅 등 커널의 모든 패킷 필터링 기능은 Netfilter 위에 구축됩니다.
현대 Linux에서 Netfilter는 단순한 "방화벽 규칙 엔진"이 아닙니다. 실제 운영에서는 훅/우선순위 계층(어느 시점에서 패킷을 보는가), 상태 엔진 계층(conntrack, NAT, helper, flowtable), 제어 평면 계층(nftables, iptables-nft, NFQUEUE, nfnetlink, BPF link)을 분리해서 봐야 문제가 풀립니다. 같은 DROP이라도 prerouting raw 단계인지, forward filter 단계인지, flowtable 진입 전후인지에 따라 원인과 해결책이 완전히 달라집니다.
Netfilter의 핵심 컴포넌트: 훅 시스템(패킷 가로채기), conntrack(연결 상태 추적), NAT(주소/포트 변환 — NAT 참조), 패킷 맹글링(헤더 수정), 로깅(NFLOG/LOG).
- 데이터 경로: ingress, prerouting, input/forward/output, postrouting에서 실제 패킷이 이동합니다.
- 상태 엔진: conntrack과 NAT는 "규칙 한 줄"이 아니라 여러 패킷에 걸친 상태 머신으로 동작합니다.
- 제어 평면:
nft,iptables-nft,libnetfilter_queue,bpftool/libbpf가 서로 다른 방식으로 커널 객체를 만집니다.
/* net/netfilter/core.c — Netfilter 핵심 자료구조 */
struct nf_hook_entries {
u16 num_hook_entries;
struct nf_hook_entry hooks[]; /* 우선순위 정렬된 훅 배열 */
/* 뒤에 struct nf_hook_ops *orig_ops[] 가 이어짐 */
};
/* 훅 실행 경로: NF_HOOK() 매크로 → nf_hook() → nf_hook_slow() */
static inline int NF_HOOK(
uint8_t pf, unsigned int hook,
struct net *net, struct sock *sk,
struct sk_buff *skb,
struct net_device *in, struct net_device *out,
int (*okfn)(struct net *, struct sock *, struct sk_buff *));
훅 시스템 (Hook System)
전통적인 설명에서는 IPv4/IPv6 경로를 PRE_ROUTING, LOCAL_IN, FORWARD, LOCAL_OUT, POST_ROUTING의 5개 훅으로 요약합니다. 하지만 최신 UAPI에는 이 5개 외에도 NF_INET_INGRESS, NF_NETDEV_INGRESS, NF_NETDEV_EGRESS가 존재하며, nftables는 family와 chain type 조합에 따라 사용할 수 있는 훅이 달라집니다. 각 훅 포인트에는 priority 순서로 정렬된 콜백(Callback) 함수가 등록되고, 패킷은 해당 지점을 지날 때 차례로 verdict를 받습니다.
ip/ip6/inet family는 고전적인 5개 IP 훅을 중심으로 설명할 수 있지만, inet family는 Linux 5.10부터 ingress hook을 지원합니다.
netdev family는 ingress/egress에 직접 붙고, bridge family는 이더넷 프레임 경로를 다룹니다. flowtable은 ingress 쪽에 위치하여 classic forwarding path의 이후 훅들을 우회할 수 있습니다.
struct nf_hook_ops
/* include/linux/netfilter.h */
struct nf_hook_ops {
nf_hookfn *hook; /* 훅 콜백 함수 */
struct net_device *dev; /* 특정 디바이스에만 적용 (NULL=전체) */
void *priv; /* 콜백에 전달할 private 데이터 */
u8 pf; /* 프로토콜 패밀리 (NFPROTO_IPV4 등) */
enum nf_hook_ops_type hook_ops_type;
unsigned int hooknum; /* 훅 번호 (NF_INET_PRE_ROUTING 등) */
int priority; /* 우선순위 (낮을수록 먼저 실행) */
};
/* 훅 콜백 함수 시그니처 */
typedef unsigned int nf_hookfn(
void *priv,
struct sk_buff *skb,
const struct nf_hook_state *state);
코드 설명
- hook (nf_hookfn*)실제 패킷 처리 콜백 함수 포인터입니다. 반환값은 NF_ACCEPT/NF_DROP/NF_QUEUE 등의 verdict 코드이며, 상위 비트에 추가 인자를 인코딩할 수 있습니다. conntrack 훅, nftables 평가기, iptables 매처 모두 이 타입을 구현합니다.
- dev (struct net_device*)특정 네트워크 인터페이스에만 훅을 적용할 때 설정합니다. NULL이면 모든 인터페이스에 적용됩니다. nftables의
netdevfamily처럼 특정 디바이스에 ingress/egress 훅을 붙일 때 사용합니다. - priv (void*)콜백 호출 시 첫 번째 인자로 전달되는 불투명 포인터입니다. nftables는 여기에
struct nft_base_chain포인터를 저장하여 어느 체인에서 온 훅인지 추적합니다. 커스텀 모듈에서는 모듈 컨텍스트 구조체를 넘깁니다. - pf (u8)프로토콜 패밀리를 지정합니다. 주요 값:
NFPROTO_IPV4(2),NFPROTO_IPV6(10),NFPROTO_BRIDGE(7),NFPROTO_NETDEV(5),NFPROTO_INET(1, IPv4+IPv6 동시). 커널은 pf와 hooknum의 조합으로 훅 배열을 선택합니다. - hook_ops_type (enum nf_hook_ops_type)훅 등록 주체를 구분합니다.
NF_HOOK_OP_UNDEFINED(일반 커널 모듈),NF_HOOK_OP_NF_TABLES(nftables),NF_HOOK_OP_BPF(BPF 프로그램)로 나뉩니다. 디버깅 시/proc/net/nf_tables_hooks에서 타입별로 구분해 볼 수 있습니다. - hooknum (unsigned int)훅 포인트 번호입니다. IPv4/IPv6/inet 계열은
NF_INET_PRE_ROUTING(0)~NF_INET_POST_ROUTING(4)이며, netdev 계열은NF_NETDEV_INGRESS(0),NF_NETDEV_EGRESS(1)입니다. pf에 따라 같은 숫자가 다른 의미를 가집니다. - priority (int)같은 hooknum에 여러 콜백이 등록될 때 정렬 기준입니다. 값이 작을수록 먼저 실행됩니다. 등록 시
nf_hook_entries배열에 오름차순으로 삽입되어,nf_hook_slow()가 배열을 순서대로 순회하면 자동으로 우선순위 순서가 지켜집니다.
훅 우선순위
훅 우선순위는 같은 훅 포인트에 등록된 여러 콜백의 실행 순서를 결정합니다. 값이 작을수록 먼저 실행됩니다. 아래 값은 주로 IPv4/inet 계열에서 많이 보는 기본값이며, 공식 UAPI 헤더 include/uapi/linux/netfilter_ipv4.h의 상수와 일치합니다.
| 상수 | 값 | 용도 |
|---|---|---|
NF_IP_PRI_FIRST | INT_MIN | 최우선 실행 |
NF_IP_PRI_RAW_BEFORE_DEFRAG | -450 | raw 테이블 (defrag 이전) |
NF_IP_PRI_CONNTRACK_DEFRAG | -400 | conntrack 조각 재조립 |
NF_IP_PRI_RAW | -300 | raw 테이블 |
NF_IP_PRI_SELINUX_FIRST | -225 | SELinux 첫 번째 |
NF_IP_PRI_CONNTRACK | -200 | conntrack 연결 추적 |
NF_IP_PRI_MANGLE | -150 | mangle 테이블 |
NF_IP_PRI_NAT_DST | -100 | DNAT (목적지 NAT) |
NF_IP_PRI_FILTER | 0 | filter 테이블 (기본 방화벽) |
NF_IP_PRI_SECURITY | 50 | security 테이블 |
NF_IP_PRI_NAT_SRC | 100 | SNAT (소스 NAT) |
NF_IP_PRI_SELINUX_LAST | 225 | SELinux 마지막 |
NF_IP_PRI_CONNTRACK_HELPER | 300 | conntrack helper |
NF_IP_PRI_CONNTRACK_CONFIRM | INT_MAX | conntrack 확정 (최후) |
priority raw, priority mangle, priority filter처럼 기호 이름을 쓸 수 있지만, 실제 커널 내부에서는 정수 우선순위로 비교합니다.
또한 bridge family는 일부 priority 이름/값이 다르고, flowtable은 ingress에 별도로 배치되어 이후 classic hook이 보이지 않을 수 있습니다.
훅 Verdict
| Verdict | 값 | 설명 |
|---|---|---|
NF_DROP | 0 | 패킷 폐기 (메모리 해제) |
NF_ACCEPT | 1 | 패킷 통과 (다음 훅으로 진행) |
NF_STOLEN | 2 | 콜백이 패킷 소유권 획득 (Netfilter 처리 중단) |
NF_QUEUE | 3 | 유저스페이스 큐(NFQUEUE)로 전달 |
NF_REPEAT | 4 | 현재 훅 재실행 (주의: 무한 루프 위험) |
NF_STOP | 5 | 이후 훅 건너뛰고 즉시 수락 (deprecated) |
NF_QUEUE는 단순히 "큐로 보낸다"가 아니라 상위 비트에 queue 번호를 실어 NF_QUEUE_NR(x) 형태로 인코딩할 수 있습니다.
NF_DROP도 상위 비트에 errno를 담는 NF_DROP_ERR(x) 매크로(Macro)가 정의되어 있습니다. NF_STOP은 현재 신규 코드에서 기대할 verdict가 아니라, 사용자 공간(User Space) 호환성을 위해 남아 있는 deprecated 값으로 보는 편이 안전합니다.
각 훅에서의 패킷 상태
Netfilter 훅은 네트워크 스택의 특정 처리 단계 전후에 위치하여 패킷을 검사하거나 수정합니다. 각 훅 포인트에서 패킷이 어떤 상태인지 이해하는 것이 중요합니다.
| 훅 | 스택 위치 | 패킷 상태 | 주요 처리 | 전형적 사용 사례 |
|---|---|---|---|---|
| PREROUTING | 수신 후 라우팅 전 |
• L2/L3 헤더 파싱 완료 • 목적지 IP 확인 가능 • 라우팅 미결정 상태 • sk_buff->dev는 수신 인터페이스 |
• DNAT (목적지 변경) • conntrack 상태 추적 시작 • IP 조각 재조립 • NOTRACK (raw 테이블) |
로드밸런서 DNAT, 포트 포워딩, 투명 프록시 |
| INPUT | 라우팅 결정 후 (로컬 전달 경로) |
• 라우팅: 로컬 전달 확정 • DNAT 완료 (있는 경우) • 목적지 포트 확정 • 소켓(Socket) 매칭 직전 |
• 로컬 방화벽 • 서비스 포트 필터링 • rate limiting |
SSH/HTTP 포트 허용, DDoS 방어, fail2ban 연동 |
| FORWARD | 라우팅 결정 후 (포워딩 경로) |
• 라우팅: 다른 인터페이스로 전달 • DNAT 완료 (있는 경우) • 출력 인터페이스 결정됨 • TTL 감소 전 |
• 라우터/게이트웨이 방화벽 • 인터페이스 간 필터링 • 브리지(Bridge)/NAT 정책 |
내부 ↔ 외부 격리(Isolation), VLAN 간 통신 제어, 멀티존 방화벽 |
| OUTPUT | 로컬 생성 패킷 라우팅 전 |
• 유저 프로세스(Process)에서 생성 • L4/L3 헤더 구성 완료 • 출력 인터페이스 미결정 • sk_buff->sk 소켓 정보 보유 |
• 출력 방화벽 • 로컬 DNAT (출력 리다이렉트) • owner match (UID/GID 필터링) |
특정 프로세스 차단, 로컬 프록시 강제, audit logging |
| POSTROUTING | 라우팅 후 송신 직전 |
• 출력 인터페이스 확정 • 라우팅 완료 • 송신 IP 확정 • Qdisc/TC 직전 |
• SNAT/MASQUERADE • 소스 IP 변경 • TTL 조작 • conntrack 확정 |
NAT 게이트웨이, IP 위장, Multi-WAN 소스 선택 |
네트워크 스택 통합 플로우
Netfilter 훅은 다음과 같이 네트워크 스택의 핵심 처리 단계와 통합됩니다:
- DNAT는 반드시 PREROUTING에서 수행 (라우팅 전에 목적지 변경)
- SNAT/MASQUERADE는 POSTROUTING에서 수행 (출력 인터페이스 확정 후)
- 로컬 서비스 방화벽은 INPUT (필터링 후 소켓 전달)
- 라우터 방화벽은 FORWARD (통과 트래픽 제어(Traffic Control))
- 프로세스별 제어는 OUTPUT (소켓 메타데이터 접근 가능)
훅 등록 API
/* per-namespace 훅 등록 (현대적 API) */
int nf_register_net_hook(struct net *net, const struct nf_hook_ops *ops);
void nf_unregister_net_hook(struct net *net, const struct nf_hook_ops *ops);
/* 다수 훅 일괄 등록 */
int nf_register_net_hooks(struct net *net,
const struct nf_hook_ops *reg,
unsigned int n);
void nf_unregister_net_hooks(struct net *net,
const struct nf_hook_ops *reg,
unsigned int n);
코드 설명
- nf_register_net_hook()단일 훅 콜백을 특정 네트워크 네임스페이스에 등록합니다(
net/netfilter/core.c). 내부적으로nf_hook_entries배열을 새로 할당하여ops->priority순서에 맞게 삽입한 뒤,rcu_assign_pointer()로 원자적 교체합니다. 컨테이너 환경에서 각 netns가 독립된 훅 배열을 가지므로,&init_net대신 정확한struct net *을 넘겨야 합니다. - nf_unregister_net_hook()등록된 훅을 해제합니다. 기존 배열에서 해당
nf_hook_ops를 제거한 새 배열로 교체한 뒤,synchronize_net()(RCU grace period 대기)을 호출하여 진행 중인 패킷 처리가 완료될 때까지 기다립니다. 모듈 언로드 시 반드시 호출해야 하며, 누락하면 해제된 콜백 함수로 점프하는 커널 패닉이 발생합니다. - nf_register_net_hooks()여러
nf_hook_ops를 한 번에 등록하는 배치 API입니다. 내부적으로nf_register_net_hook()을 반복 호출하며, 중간에 실패하면 이미 등록된 훅들을 자동으로 롤백합니다. conntrack 모듈처럼 PREROUTING/OUTPUT 등 여러 훅에 동시 등록이 필요할 때 사용합니다.
NF_HOOK_COND()와 nf_hook_state 내부
NF_HOOK() 외에 커널은 NF_HOOK_COND() 매크로를 제공합니다. 이 매크로는 조건부 훅 호출에 사용되며, 특정 기능(예: 브리지 Netfilter)이 비활성화된 경우 훅 평가를 완전히 건너뛰어 불필요한 오버헤드를 제거합니다. 훅 호출 시 초기화되는 nf_hook_state 구조체는 콜백 함수들이 참조하는 컨텍스트 정보를 담습니다.
/* include/linux/netfilter.h — 조건부 훅 매크로 */
static inline int
NF_HOOK_COND(uint8_t pf, unsigned int hook,
struct net *net, struct sock *sk,
struct sk_buff *skb,
struct net_device *in,
struct net_device *out,
int (*okfn)(struct net *, struct sock *, struct sk_buff *),
bool cond)
{
int ret;
if (!cond ||
((ret = nf_hook(pf, hook, net, sk, skb, in, out, okfn)) == 1))
ret = okfn(net, sk, skb);
return ret;
}
/* include/linux/netfilter.h — 훅 콜백 컨텍스트 구조체 */
struct nf_hook_state {
unsigned int hook; /* NF_INET_PRE_ROUTING 등 */
u_int8_t pf; /* 프로토콜 패밀리 (NFPROTO_IPV4 등) */
struct net_device *in; /* 입력 장치 */
struct net_device *out; /* 출력 장치 */
struct sock *sk; /* 소켓 (가능한 경우) */
struct net *net; /* 네트워크 네임스페이스 */
int (*okfn)(struct net *, struct sock *,
struct sk_buff *);
};
코드 설명
- NF_HOOK_COND()
cond인자가false이면nf_hook()을 호출하지 않고 바로okfn()으로 진행합니다. 브리지 Netfilter(br_netfilter)에서nf_hooks_active()결과를 조건으로 넘기는 패턴이 대표적입니다. 훅이 미등록 상태면 해시 테이블 조회 자체를 건너뛰므로 성능 이점이 있습니다. - nf_hook_state모든 훅 콜백 함수(
nf_hookfn)의 세 번째 인자로 전달됩니다.state->hook으로 현재 훅 포인트를 식별하고,state->net으로 네임스페이스별 리소스에 접근합니다.in/out은 패킷의 입출력 인터페이스이며, OUTPUT 훅에서는in이 NULL입니다. - okfn 콜백훅 체인이 NF_ACCEPT를 반환하면 호출되는 다음 단계 함수입니다. 예를 들어 PREROUTING 훅의
okfn은ip_rcv_finish()이며, OUTPUT 훅에서는dst_output()입니다. 비동기(NFQUEUE) verdict 후에도 이 포인터를 통해 처리가 재개됩니다.
NF_HOOK() → nf_hook_slow() 콜 체인 분석
패킷이 Netfilter 훅 포인트를 지날 때의 실제 커널 호출 흐름을 분석합니다. 인라인 함수 NF_HOOK()에서 시작하여 등록된 훅 콜백까지 내려가는 경로입니다.
/* include/linux/netfilter.h — NF_HOOK() 인라인 진입점 */
static inline int
NF_HOOK(uint8_t pf, unsigned int hook,
struct net *net, struct sock *sk,
struct sk_buff *skb,
struct net_device *in, struct net_device *out,
int (*okfn)(struct net *, struct sock *, struct sk_buff *))
{
struct nf_hook_state state;
nf_hook_state_init(&state, hook, pf, in, out, sk, net, okfn); /* 훅 상태 초기화 */
return nf_hook(pf, hook, &state, skb);
}
/* nf_hook(): 훅 배열이 비어 있으면 즉시 okfn 호출, 등록된 훅이 있으면 nf_hook_slow()로 진입 */
static inline int
nf_hook(uint8_t pf, unsigned int hook,
struct nf_hook_state *state,
struct sk_buff *skb)
{
struct nf_hook_entries *e;
int ret = 1;
e = rcu_dereference(net->nf.hooks_pf[pf][hook]); /* RCU 읽기락으로 훅 배열 접근 */
if (e) {
ret = nf_hook_slow(skb, state, e, 0); /* 훅이 있으면 순차 실행 */
}
if (ret == 1)
ret = okfn(state->net, state->sk, skb); /* 모두 ACCEPT → 다음 단계 진행 */
return ret;
}
코드 설명
- NF_HOOK()IP 스택(ip_rcv, ip_forward 등)에서 직접 호출하는 인라인 진입점입니다. 컴파일러가 최적화할 수 있도록 inline으로 선언되어 있으며, 훅이 없는 경우
okfn을 직접 호출하여 오버헤드를 줄입니다. - nf_hook_state_init()훅 번호, 프로토콜 패밀리, 수신/송신 디바이스, 소켓, 네트워크 네임스페이스, 완료 콜백(okfn)을
nf_hook_state구조체에 묶습니다. 이후 모든 훅 콜백이 이 구조체를 통해 컨텍스트를 읽습니다. - rcu_dereference()훅 배열(
nf_hook_entries)은 RCU(Read-Copy-Update)로 관리됩니다. 패킷 처리 경로는 읽기 측이므로rcu_dereference()로 포인터를 안전하게 역참조합니다. 훅 등록/해제 시 쓰기 측이 새 배열을 원자적으로 교체합니다. - nf_hook_slow()실제로 등록된 훅 콜백을 순차 실행하는 핵심 함수입니다. 반환값 1은 ACCEPT(계속 진행), 0 이하는 DROP/QUEUE 등을 의미합니다.
- okfn()모든 훅이 ACCEPT를 반환했을 때 호출되는 "다음 단계" 함수입니다. 예: PREROUTING이 통과하면
ip_rcv_finish()가 okfn으로 라우팅을 수행합니다.
/* net/netfilter/core.c — nf_hook_slow(): 훅 배열을 순회하며 verdict 수집 */
int nf_hook_slow(struct sk_buff *skb,
struct nf_hook_state *state,
const struct nf_hook_entries *e,
unsigned int s)
{
unsigned int verdict;
do {
verdict = nf_hook_entry_hookfn(&e->hooks[s], skb, state); /* 개별 훅 콜백 호출 */
switch (verdict & NF_VERDICT_MASK) {
case NF_ACCEPT:
s++; /* 다음 훅으로 진행 */
break;
case NF_DROP:
kfree_skb_reason(skb, SKB_DROP_REASON_NETFILTER_DROP); /* 패킷 폐기 */
return NF_DROP_GETERR(verdict); /* 상위 비트에서 errno 추출 */
case NF_QUEUE:
return nf_queue(skb, state, s, verdict); /* NFQUEUE로 위임 */
case NF_REPEAT:
break; /* 현재 훅 인덱스 유지 → 재실행 */
default:
return 0;
}
} while (s < e->num_hook_entries);
return 1; /* 모든 훅 통과 → ACCEPT */
}
코드 설명
- nf_hook_entry_hookfn()우선순위 순으로 정렬된
nf_hook_entries배열에서 인덱스s번째 콜백을 호출합니다. 실제 콜백은 conntrack, NAT, nftables 평가기, iptables 매처 등 다양합니다. - NF_VERDICT_MASK반환값 하위 8비트가 verdict 종류입니다. 상위 비트에는 NF_QUEUE 큐 번호 또는 NF_DROP errno 등 추가 정보가 인코딩됩니다.
- NF_ACCEPT + s++ACCEPT이면 인덱스를 증가시켜 다음 우선순위 콜백으로 넘어갑니다. do-while 루프가 배열 끝까지 반복합니다.
- kfree_skb_reason()NF_DROP 시 sk_buff를 해제합니다. 6.0 이후
SKB_DROP_REASON_*열거값을 함께 넘겨skb:kfree_skbtracepoint에서 폐기 이유를 추적할 수 있습니다. - nf_queue()NF_QUEUE verdict이면 해당 NFQUEUE 번호로 sk_buff를 유저스페이스 큐에 넣습니다. 유저스페이스가 verdict를 내리면
nf_reinject()로 재진입합니다. - NF_REPEAT인덱스를 증가시키지 않아 같은 훅을 다시 실행합니다. NAT helper가 패킷 내용을 수정한 후 conntrack을 재평가할 때 사용되지만, 무한 루프 위험이 있습니다.
IP 조각 재조립(Defragmentation)과 Netfilter
conntrack과 NAT는 완전한 L4 헤더를 필요로 하므로, IP 조각(Fragment) 패킷은 Netfilter 훅 평가 전에 재조립되어야 합니다. IPv4에서는 nf_defrag_ipv4 모듈이 PREROUTING/OUTPUT에 높은 우선순위(priority -400)로 등록되어, conntrack(priority -200)보다 먼저 실행됩니다. IPv6는 nf_defrag_ipv6가 동일한 역할을 수행합니다.
/* net/ipv4/netfilter/nf_defrag_ipv4.c — IPv4 조각 재조립 활성화 */
static unsigned int
ipv4_conntrack_defrag(void *priv,
struct sk_buff *skb,
const struct nf_hook_state *state)
{
struct sock *sk = skb->sk;
if (sk && sk_fullsock(sk) &&
(sk->sk_family == PF_INET) &&
inet_sk(sk)->nodefrag)
return NF_ACCEPT;
#if IS_ENABLED(CONFIG_NF_CONNTRACK)
if (skb_nfct(skb) && !nf_ct_is_template(
(struct nf_conn *)skb_nfct(skb)))
return NF_ACCEPT;
#endif
if (ip_is_fragment(ip_hdr(skb))) {
enum ip_defrag_users user;
user = state->hook == NF_INET_PRE_ROUTING
? IP_DEFRAG_CONNTRACK_IN
: IP_DEFRAG_CONNTRACK_OUT;
if (nf_ct_ipv4_gather_frags(state->net, skb, user))
return NF_STOLEN; /* 재조립 진행 중 */
}
return NF_ACCEPT;
}
/* net/ipv6/netfilter/nf_conntrack_reasm.c — IPv6 조각 수집 */
int nf_ct_frag6_gather(struct net *net,
struct sk_buff *skb,
u32 user)
{
/* IPv6 Fragment 헤더 파싱 → frag queue 조회/생성 */
struct frag_queue *fq;
struct ipv6hdr *hdr = ipv6_hdr(skb);
fq = fq_find(net, fhdr->identification, &hdr->saddr,
&hdr->daddr, user);
if (fq == NULL)
return -ENOMEM;
/* 조각을 큐에 추가, 완료 시 재조립된 skb 반환 */
spin_lock_bh(&fq->q.lock);
nf_ct_frag6_queue(fq, skb, fhdr, nhoff);
/* ... 모든 조각 도착 시 nf_ct_frag6_reasm() 호출 ... */
spin_unlock_bh(&fq->q.lock);
return 0;
}
코드 설명
- ipv4_conntrack_defrag()PREROUTING/OUTPUT에 priority -400으로 등록되어 conntrack보다 먼저 실행됩니다.
ip_is_fragment()가 true이면nf_ct_ipv4_gather_frags()를 호출하여 커널의 IP 재조립 엔진(ip_defrag())에 패킷을 전달합니다. 재조립이 완료되지 않으면 NF_STOLEN을 반환하여 패킷을 가져갑니다. - nodefrag 소켓 옵션
inet_sk(sk)->nodefrag가 설정된 소켓에서 보낸 패킷은 재조립을 건너뜁니다. 주로 raw 소켓이나 IPsec 터널에서 의도적으로 조각 패킷을 다룰 때 사용됩니다. - nf_ct_frag6_gather()IPv6 전용 재조립 함수입니다. IPv6 Fragment 헤더의 identification 필드로 조각 큐를 찾거나 새로 생성합니다. 모든 조각이 도착하면
nf_ct_frag6_reasm()이 하나의 연속된 skb로 재조립합니다. IPv6에서는 라우터가 아닌 송신자만 조각화하므로 Path MTU Discovery와 밀접하게 연관됩니다. - IP_DEFRAG_CONNTRACK_IN/OUT재조립 사용자 식별자로, 같은 조각 ID라도 서로 다른 용도(입력/출력)의 재조립을 분리합니다. 이를 통해 PREROUTING과 OUTPUT의 재조립 큐가 혼재되지 않습니다.
nftables 아키텍처
nftables는 iptables/ip6tables/arptables/ebtables를 통합 대체하는 차세대 패킷 분류 프레임워크입니다. 커널 측에서는 바이트코드 VM(가상 머신)으로 규칙을 실행하며, 사용자 공간에서는 nft 유틸리티를 통해 netlink 기반으로 규칙을 전달합니다.
nftables vs iptables 비교
| 항목 | iptables | nftables |
|---|---|---|
| 커널 구조 | 프로토콜별 별도 테이블 (ip/ip6/arp/ebtables) | 통합 프레임워크 (nf_tables) |
| 규칙 평가 | 선형 매칭 (O(n)) | set/map 기반 최적화 (O(1) 가능) |
| 확장 방식 | match/target 커널 모듈(Kernel Module) | expression 기반 (커널+유저 모듈 쌍) |
| 원자적(Atomic) 규칙 교체 | iptables-restore (전체 교체) | 네이티브 트랜잭션(Transaction) 지원 |
| 사용자 도구 | iptables, ip6tables, arptables, ebtables | nft (통합 CLI) |
| 커널-유저 통신 | getsockopt/setsockopt | netlink (nfnetlink) |
| sets/maps | ipset (별도 모듈) | 네이티브 set, map, vmap |
| Flowtable | 미지원 | 하드웨어 오프로드 가능 |
nftables 내부 구조
/* net/netfilter/nf_tables_api.c — nftables 핵심 객체 */
/* 테이블: 체인들의 컨테이너 */
struct nft_table {
struct list_head list;
struct list_head chains; /* 소속 체인 목록 */
struct list_head sets; /* 소속 set 목록 */
struct list_head flowtables; /* flowtable 목록 */
u64 hgenerator;
u32 use; /* 참조 카운트 */
u16 family; /* NFPROTO_* */
u16 flags; /* NFT_TABLE_F_* */
char *name;
};
/* 체인: 규칙들의 순서 리스트 + 훅 정보 */
struct nft_chain {
struct nft_rule_blob *blob_gen_0; /* 규칙 바이트코드 세대 0 (lock-free 업데이트용) */
struct nft_rule_blob *blob_gen_1; /* 규칙 바이트코드 세대 1 (교체 시 RCU grace period 후 해제) */
struct list_head rules; /* struct nft_rule 리스트 */
struct nft_table *table;
u64 handle;
u32 use;
char *name;
};
/* 규칙: 표현식(expression)들의 배열 */
struct nft_rule {
struct list_head list;
u64 handle;
u32 dlen; /* 표현식 데이터 총 길이 */
unsigned char data[]; /* nft_expr 배열 */
};
코드 설명
- nft_tablenftables의 최상위 컨테이너 객체입니다(
net/netfilter/nf_tables_api.c).family필드가NFPROTO_INET이면 IPv4/IPv6 모두에 적용됩니다.chains,sets,flowtables리스트로 하위 객체들을 관리하며,flags에NFT_TABLE_F_DORMANT를 설정하면 테이블 전체를 비활성화할 수 있습니다. - nft_chain — blob_gen_0/blob_gen_1규칙 바이트코드를 두 세대(generation)로 관리하는 lock-free 업데이트 메커니즘입니다.
nft commit시 새 규칙을 비활성 세대에 기록하고, RCU grace period 후 활성 세대를 교체합니다. 패킷 처리 경로(nft_do_chain())는 항상 현재 활성 세대만 읽으므로 잠금 없이 원자적 규칙 갱신이 가능합니다. - nft_chain — rules
struct nft_rule의 연결 리스트입니다. 제어 경로(규칙 추가/삭제)에서 사용하며, 실제 패킷 처리 시에는 blob에 직렬화된 바이트코드를 사용합니다. base chain은nf_hook_ops를 내장하여 Netfilter 훅에 직접 연결되고, regular chain은 JUMP/GOTO verdict로만 진입합니다. - nft_rule — data[]가변 길이 배열로
nft_expr구조체들이 연속 배치됩니다.dlen은 전체 표현식 데이터의 바이트 크기이며, 평가 시nft_rule_dp_for_each_expr()매크로가 이 배열을 순회합니다. 규칙 하나가 "조건+액션" 쌍이 아니라, 표현식의 직렬 실행으로 동작하는 것이 iptables와의 핵심 차이입니다.
nftables 표현식 (Expression)
nftables 규칙은 표현식의 연속으로 구성됩니다. 각 표현식은 패킷에서 데이터를 추출(payload), 비교(cmp), 조회(lookup), 액션(verdict) 등을 수행합니다.
/* 표현식 타입 등록 */
struct nft_expr_type {
const struct nft_expr_ops *(*select_ops)(...);
const struct nft_expr_ops *ops;
const char *name;
struct module *owner;
u32 flags; /* NFT_EXPR_STATEFUL 등 */
};
/* 표현식 연산 인터페이스 */
struct nft_expr_ops {
void (*eval)(const struct nft_expr *expr,
struct nft_regs *regs,
const struct nft_pktinfo *pkt);
int (*init)(...);
void (*destroy)(...);
int (*dump)(...);
u32 type; /* NFT_EXPR_* */
unsigned int size;
};
int nft_register_expr(struct nft_expr_type *type);
void nft_unregister_expr(struct nft_expr_type *type);
코드 설명
- nft_expr_typenftables 표현식 타입의 등록 구조체입니다(
net/netfilter/nf_tables_api.c).select_ops콜백이 있으면 Netlink 속성에 따라 동적으로nft_expr_ops를 선택합니다. 예를 들어nft_payload표현식은 L2/L3/L4 레이어에 따라 서로 다른 ops를 반환합니다.flags에NFT_EXPR_STATEFUL이 설정되면 규칙별로 독립적인 상태(예: counter, limit)를 유지합니다. - nft_expr_ops — eval()패킷 처리 hot path에서 호출되는 핵심 콜백입니다.
nft_regs의 가상 레지스터를 통해 표현식 간 데이터를 전달합니다. payload 표현식은skb에서 헤더 필드를 레지스터에 로드하고, cmp 표현식은 레지스터 값을 상수와 비교하여 불일치 시NFT_BREAK를 설정합니다. - nft_expr_ops — init()/destroy()
init()은 Netlink 메시지에서 표현식 파라미터를 파싱하여 per-rule 사설 데이터를 초기화합니다.destroy()는 규칙 삭제 시 리소스를 해제합니다. set lookup 표현식의 경우init()에서 set 바인딩을 설정하고destroy()에서 해제합니다. - nft_register_expr()새 표현식 타입을 전역 리스트에 등록합니다. 커널 모듈이 커스텀 표현식을 추가할 때 사용하며, 유저스페이스의
nft도구가 해당 표현식 이름으로 규칙을 생성할 수 있게 됩니다. 등록 시 이름 충돌 검사를 수행합니다.
nft_pktinfo와 nft_regs 가상 머신
nftables의 규칙 평가 엔진은 두 가지 핵심 구조체를 중심으로 동작합니다. nft_pktinfo는 현재 처리 중인 패킷과 훅 컨텍스트를 감싸며, nft_regs는 표현식 간 데이터를 전달하는 가상 레지스터 파일입니다. 이 두 구조체가 nftables VM의 실행 상태를 구성합니다.
/* include/net/netfilter/nf_tables.h — 패킷 정보 래퍼 */
struct nft_pktinfo {
struct sk_buff *skb;
const struct nf_hook_state *state;
u8 flags;
u8 tprot; /* L4 프로토콜 번호 */
u16 thoff; /* L4 헤더 오프셋 */
u16 fragoff; /* 조각 오프셋 */
u16 xt_thoff; /* xt 호환성용 오프셋 */
};
/* include/net/netfilter/nf_tables.h — 가상 레지스터 파일 */
struct nft_regs {
union {
u32 data[20]; /* NFT_REG32_00 ~ NFT_REG32_19 */
struct nft_verdict verdict;
};
};
/* 레지스터 접근 헬퍼 */
static inline void *
nft_reg_store(const struct nft_regs *regs,
unsigned int reg)
{
return (void *)®s->data[reg];
}
코드 설명
- nft_pktinfo
sk_buff와nf_hook_state를 하나로 묶어 nftables 체인 내에서 일관된 패킷 컨텍스트를 제공합니다.tprot은 L4 프로토콜(TCP/UDP/ICMP 등),thoff는 transport 헤더 시작 오프셋으로, payload 표현식이 L4 필드를 추출할 때 사용됩니다.nft_set_pktinfo()가 훅 진입 시 이 구조체를 초기화합니다. - nft_regs — data[20]20개의 32비트 레지스터로 구성된 가상 레지스터 파일입니다. payload 표현식이 패킷 필드를 특정 레지스터에 로드하면, cmp 표현식이 해당 레지스터를 읽어 비교합니다. 128비트(IPv6 주소) 값은 연속 4개 레지스터를 사용합니다.
verdict와 union으로 공유하여 최종 판정값도 레지스터에 저장됩니다. - nft_verdict규칙 평가 결과를 담습니다.
code필드가 NF_ACCEPT/NF_DROP 같은 기본 verdict이거나 NFT_JUMP/NFT_GOTO로 다른 체인으로 분기합니다.chain필드는 jump/goto 시 대상 체인 포인터를 가리킵니다.
주요 nftables 표현식 eval() 내부
nftables VM의 핵심은 각 표현식의 eval() 콜백입니다. 패킷 헤더 추출(nft_payload), 값 비교(nft_cmp), 집합 검색(nft_lookup) 등 자주 사용되는 표현식의 평가 로직을 분석합니다.
/* net/netfilter/nft_payload.c — 패킷 헤더 필드 추출 */
void nft_payload_eval(const struct nft_expr *expr,
struct nft_regs *regs,
const struct nft_pktinfo *pkt)
{
const struct nft_payload *priv = nft_expr_priv(expr);
const struct sk_buff *skb = pkt->skb;
u32 *dest = ®s->data[priv->dreg];
int offset;
switch (priv->base) {
case NFT_PAYLOAD_LL_HEADER:
offset = skb_mac_header(skb) - skb->data;
break;
case NFT_PAYLOAD_NETWORK_HEADER:
offset = skb_network_offset(skb);
break;
case NFT_PAYLOAD_TRANSPORT_HEADER:
offset = pkt->thoff;
break;
}
offset += priv->offset;
if (skb_copy_bits(skb, offset, dest, priv->len) < 0)
goto err;
return;
err:
regs->verdict.code = NFT_BREAK;
}
/* net/netfilter/nft_cmp.c — 레지스터 값 비교 */
void nft_cmp_eval(const struct nft_expr *expr,
struct nft_regs *regs,
const struct nft_pktinfo *pkt)
{
const struct nft_cmp_expr *priv = nft_expr_priv(expr);
int d;
d = memcmp(®s->data[priv->sreg], &priv->data,
priv->len);
switch (priv->op) {
case NFT_CMP_EQ:
if (d != 0) goto mismatch;
break;
case NFT_CMP_NEQ:
if (d == 0) goto mismatch;
break;
case NFT_CMP_LT:
if (d >= 0) goto mismatch;
break;
/* GT, LTE, GTE 동일 패턴 */
}
return;
mismatch:
regs->verdict.code = NFT_BREAK;
}
/* net/netfilter/nft_lookup.c — set 검색 */
void nft_lookup_eval(const struct nft_expr *expr,
struct nft_regs *regs,
const struct nft_pktinfo *pkt)
{
const struct nft_lookup *priv = nft_expr_priv(expr);
const struct nft_set *set = priv->set;
const struct nft_set_ext *ext = NULL;
bool found;
found = set->ops->lookup(
pkt->state->net, set,
®s->data[priv->sreg], &ext);
if (!found) {
regs->verdict.code = NFT_BREAK;
return;
}
/* map인 경우 data extension에서 값 복사 */
if (set->flags & NFT_SET_MAP)
nft_data_copy(®s->data[priv->dreg],
nft_set_ext_data(ext),
set->dlen);
}
코드 설명
- nft_payload_eval()패킷에서 지정된 레이어(L2/L3/L4)의 특정 오프셋에서
len바이트를 읽어 목적지 레지스터(priv->dreg)에 저장합니다.skb_copy_bits()는 비선형(paged) skb도 안전하게 복사합니다. 실패 시NFT_BREAK로 해당 규칙 평가를 중단합니다. - nft_cmp_eval()소스 레지스터의 값을 표현식에 내장된 상수(
priv->data)와memcmp()로 비교합니다. EQ/NEQ/LT/GT 등 연산자에 따라 불일치 시NFT_BREAK를 설정합니다. iptables의 match 함수와 동일한 역할이지만, 범용 비교 연산으로 일반화되어 있습니다. - nft_lookup_eval()집합(set)의
ops->lookup()을 호출하여 소스 레지스터 값이 집합에 존재하는지 검사합니다. 집합이 map인 경우(NFT_SET_MAP), 찾은 요소의 데이터를 목적지 레지스터에 복사하여 이후 표현식에서 활용합니다. 이 단일 함수가 해시, rbtree, pipapo 등 모든 set 백엔드를 추상화합니다.
nft 명령어 예제
# 테이블 생성 (inet = IPv4 + IPv6 통합)
nft add table inet my_filter
# base chain 생성 (훅에 직접 연결)
nft add chain inet my_filter input \
'{ type filter hook input priority 0; policy accept; }'
nft add chain inet my_filter forward \
'{ type filter hook forward priority 0; policy drop; }'
# 규칙 추가
nft add rule inet my_filter input tcp dport 22 accept
nft add rule inet my_filter input tcp dport 80 accept
nft add rule inet my_filter input ct state established,related accept
nft add rule inet my_filter input counter drop
# NAT 테이블
nft add table ip my_nat
nft add chain ip my_nat postrouting \
'{ type nat hook postrouting priority 100; }'
nft add rule ip my_nat postrouting oifname "eth0" masquerade
# set 활용 (고속 매칭)
nft add set inet my_filter blocked_ips '{ type ipv4_addr; }'
nft add element inet my_filter blocked_ips '{ 10.0.0.1, 10.0.0.2 }'
nft add rule inet my_filter input ip saddr @blocked_ips drop
# map 활용 (키→액션 매핑)
nft add map inet my_filter port_vmap \
'{ type inet_service : verdict; }'
nft add element inet my_filter port_vmap \
'{ 22 : accept, 80 : accept, 443 : accept }'
nft add rule inet my_filter input tcp dport vmap @port_vmap
# flowtable (커넥션 fast path)
nft add flowtable inet my_filter f \
'{ hook ingress priority 0; devices = { eth0, eth1 }; }'
nft add rule inet my_filter forward ct state established \
flow add @f
# 규칙 확인
nft list ruleset
iptables 레거시 아키텍처
iptables는 Netfilter의 전통적인 사용자 공간 인터페이스로, 커널 내에서 xt_table 구조와 match/target 확장 모듈을 사용합니다. 현재는 nftables가 권장되지만, 여전히 많은 시스템에서 운용 중이며 Docker, Kubernetes 등 컨테이너(Container) 생태계가 의존하고 있습니다.
현대 배포판에서 iptables 명령어는 두 가지 백엔드 중 하나로 동작합니다: nf_tables 백엔드(iptables-nft, 권장)는 내부적으로 nftables 커널 모듈을 사용하고, legacy 백엔드(iptables-legacy)는 x_tables 커널 모듈을 직접 사용합니다. iptables -V로 확인할 수 있습니다.
테이블 종류
| 테이블 | 커널 모듈 | 용도 | 기본 체인 (훅 포인트) |
|---|---|---|---|
raw | iptable_raw | conntrack 바이패스 (NOTRACK) | PREROUTING, OUTPUT |
mangle | iptable_mangle | 패킷 헤더 수정 (TOS, TTL, MARK) | PREROUTING, INPUT, FORWARD, OUTPUT, POSTROUTING |
nat | iptable_nat | 주소/포트 변환 | PREROUTING, INPUT, OUTPUT, POSTROUTING |
filter | iptable_filter | 패킷 허용/차단 (기본 방화벽) | INPUT, FORWARD, OUTPUT |
security | iptable_security | SELinux SECMARK 규칙 | INPUT, FORWARD, OUTPUT |
체인 구조와 규칙 평가
각 테이블에는 빌트인 체인(훅 포인트에 직접 연결)과 사용자가 만드는 커스텀 체인이 있습니다. 빌트인 체인에는 기본 정책(ACCEPT/DROP)이 있으며, 어떤 규칙도 매치되지 않으면 기본 정책이 적용됩니다.
규칙 평가 핵심: 규칙은 위에서 아래로 순서대로 평가됩니다(O(n) 선형 탐색). terminating target(ACCEPT, DROP, REJECT)에 매치되면 즉시 체인을 빠져나갑니다. non-terminating target(LOG, ULOG, MARK, CONNMARK, TOS)은 패킷을 계속 진행시킵니다. -j <custom_chain>은 서브루틴 호출처럼 동작합니다.
iptables 내부 구조
/* net/netfilter/x_tables.c — xtables 핵심 */
struct xt_table {
struct list_head list;
unsigned int valid_hooks; /* 유효한 훅 비트마스크 */
struct xt_table_info *private; /* per-CPU 규칙 데이터 */
struct module *me;
u_int8_t af; /* 프로토콜 패밀리 */
int priority;
const char name[XT_TABLE_MAXNAMELEN];
};
/* xt_table_info: per-CPU 규칙 블롭과 카운터 */
struct xt_table_info {
unsigned int size; /* 규칙 데이터 총 크기 */
unsigned int number; /* 엔트리 개수 */
unsigned int initial_entries;
unsigned int hook_entry[NF_INET_NUMHOOKS]; /* 체인 시작 오프셋 */
unsigned int underflow[NF_INET_NUMHOOKS]; /* 기본 정책 오프셋 */
unsigned int stacksize;
void ***jumpstack; /* 커스텀 체인 점프 스택 */
unsigned char entries[]; /* ipt_entry 배열 */
};
/* net/ipv4/netfilter/ip_tables.c — 개별 규칙 엔트리 */
struct ipt_entry {
struct ipt_ip ip; /* src/dst IP, 인터페이스, 프로토콜 */
unsigned int nfcache;
__u16 target_offset; /* match 데이터 끝 = target 시작 */
__u16 next_offset; /* 다음 엔트리까지 거리 */
unsigned int comefrom; /* 체인 추적 (역추적용) */
struct xt_counters counters; /* 패킷/바이트 카운터 */
unsigned char elems[]; /* [match...][target] */
};
/* per-CPU 카운터로 lock-free 패킷/바이트 집계 */
struct xt_counters {
__u64 pcnt; /* 패킷 카운트 */
__u64 bcnt; /* 바이트 카운트 */
};
코드 설명
- xt_tableiptables 레거시(
x_tables) 프레임워크의 테이블 구조체입니다(net/netfilter/x_tables.c).valid_hooks비트마스크가 이 테이블이 어떤 훅에 체인을 가지는지 정의합니다. 예를 들어 filter 테이블은 INPUT/FORWARD/OUTPUT(비트 0x0E)에, nat 테이블은 PREROUTING/OUTPUT/POSTROUTING에 체인을 가집니다. - xt_table_info — hook_entry/underflow
hook_entry[hook]는 각 훅 체인의 시작 바이트 오프셋이고,underflow[hook]는 기본 정책(ACCEPT/DROP) 엔트리의 오프셋입니다. 패킷 처리 시entries[]배열을 이 오프셋부터 순회하며, underflow에 도달하면 기본 정책이 적용됩니다. - ipt_entry — target_offset/next_offset가변 길이 엔트리의 내부 레이아웃을 정의합니다.
target_offset까지가 match 확장 데이터이고, 그 이후부터 target 데이터입니다.next_offset은 다음ipt_entry까지의 거리로, 이를 통해 규칙 배열을 순차 탐색합니다. 이 바이트 오프셋 기반 설계가 iptables의 O(n) 선형 탐색 특성을 결정합니다. - xt_countersper-CPU로 복제되어 lock 없이 패킷/바이트를 집계합니다.
iptables -L -v로 조회하면 모든 CPU의 카운터를 합산하여 표시합니다.iptables -Z로 초기화할 수 있으며, 이때 원자적 읽기+초기화를 위해replace연산이 내부적으로 사용됩니다.
match / target 확장 모듈
/* match 확장 구조 */
struct xt_match {
const char name[XT_EXTENSION_MAXNAMELEN];
u_int8_t revision; /* 버전 (하위 호환) */
u_int8_t family; /* NFPROTO_* */
/* 패킷 매칭 콜백 — true 반환 시 매치 */
bool (*match)(const struct sk_buff *skb,
struct xt_action_param *par);
/* 규칙 등록 시 유효성 검증 */
int (*checkentry)(const struct xt_mtchk_param *par);
void (*destroy)(const struct xt_mtdtor_param *par);
unsigned int matchsize; /* per-rule 사설 데이터 크기 */
unsigned int hooks; /* 사용 가능 훅 비트마스크 */
struct module *me;
};
/* target 확장 구조 */
struct xt_target {
const char name[XT_EXTENSION_MAXNAMELEN];
u_int8_t revision;
u_int8_t family;
/* 패킷 처리 콜백 — verdict 반환 */
unsigned int (*target)(struct sk_buff *skb,
const struct xt_action_param *par);
int (*checkentry)(const struct xt_tgchk_param *par);
void (*destroy)(const struct xt_tgdtor_param *par);
unsigned int targetsize;
unsigned int hooks;
struct module *me;
};
/* 등록/해제 API */
int xt_register_match(struct xt_match *match);
void xt_unregister_match(struct xt_match *match);
int xt_register_matches(struct xt_match *match, unsigned int n);
int xt_register_target(struct xt_target *target);
void xt_unregister_target(struct xt_target *target);
int xt_register_targets(struct xt_target *target, unsigned int n);
xt 확장 등록 내부 동작
xt_register_match()는 match 모듈이 커널에 등록될 때 호출됩니다. 프로토콜 패밀리별 전역 리스트에 삽입되며, 리비전(Revision) 협상을 통해 동일 match의 여러 버전이 공존할 수 있습니다. iptables 유저스페이스 도구는 SO_GET_REVISION_MATCH 소켓 옵션으로 커널이 지원하는 최신 리비전을 확인합니다.
/* net/netfilter/x_tables.c — match 등록 */
int xt_register_match(struct xt_match *match)
{
struct xt_af *af;
int ret;
ret = xt_check_proc_name(match->name,
sizeof(match->name));
if (ret)
return ret;
/* 프로토콜 패밀리별 리스트에 삽입 */
af = &xt[match->family];
mutex_lock(&xt[match->family].mutex);
list_add(&match->list, &af->match);
mutex_unlock(&xt[match->family].mutex);
return 0;
}
/* 리비전 조회 — 유저스페이스 호환성 협상 */
int xt_find_revision(u8 af, const char *name,
u8 revision, int target,
int *err)
{
/* af 패밀리의 match/target 리스트를 순회하며
name이 일치하는 엔트리 중 최대 revision 반환 */
struct list_head *list;
int have_rev = 0;
list = target ? &xt[af].target : &xt[af].match;
list_for_each_entry(m, list, list) {
if (strcmp(m->name, name) == 0) {
if (m->revision > have_rev)
have_rev = m->revision;
if (m->revision == revision)
*err = 0;
}
}
return have_rev;
}
코드 설명
- xt_register_match()
xt[]배열은NFPROTO_NUMPROTO크기의 per-family 구조체 배열입니다.match->family에 해당하는 슬롯의match리스트에 새 엔트리를 추가합니다.NFPROTO_UNSPEC(0)으로 등록하면 모든 패밀리에서 사용 가능한 범용 match가 됩니다. mutex로 보호되므로 동시 등록이 안전합니다. - xt_find_revision()iptables 유저스페이스가
getsockopt(SO_GET_REVISION_MATCH)를 호출하면 이 함수가 실행됩니다. 동일 이름의 모든 match를 순회하여 요청된 리비전이 존재하는지 확인하고, 커널이 지원하는 최대 리비전을 반환합니다. 이를 통해 구버전 iptables 바이너리와 신버전 커널 간 호환성을 유지합니다.
ipt_do_table() 규칙 평가 엔진
ipt_do_table()은 iptables의 핵심 규칙 평가 함수로, nftables의 nft_do_chain()에 해당합니다. xt_table_info에 저장된 규칙 블롭(blob)을 순차적으로 순회하며, 각 규칙의 match를 평가하고 target을 실행합니다.
/* net/ipv4/netfilter/ip_tables.c — iptables 규칙 평가 (요약) */
unsigned int
ipt_do_table(struct sk_buff *skb,
const struct nf_hook_state *state,
struct xt_table *table)
{
const struct xt_table_info *private;
struct ipt_entry *e, **jumpstack;
unsigned int verdict = NF_DROP;
void *table_base;
unsigned int stackidx = 0;
/* per-CPU 규칙 영역 획득 */
private = READ_ONCE(table->private);
table_base = private->entries;
jumpstack = (struct ipt_entry **)
private->jumpstack[raw_smp_processor_id()];
/* 현재 훅의 시작 규칙으로 점프 */
e = get_entry(table_base,
private->hook_entry[state->hook]);
do {
const struct xt_entry_match *ematch;
/* 표준 매칭: src/dst IP, interface, protocol */
if (!ip_packet_match(ip, state->in, state->out,
&e->ip, e->comefrom))
goto no_match;
/* 확장 match 체인 평가 */
xt_ematch_foreach(ematch, e) {
if (!ematch->u.kernel.match->match(
skb, &acpar))
goto no_match;
}
/* 모든 match 통과 → target 실행 */
t = ipt_get_target(e);
verdict = t->u.kernel.target->target(
skb, &acpar);
if (verdict == XT_CONTINUE)
e = ipt_next_entry(e);
else if (verdict == XT_RETURN) {
/* jump 스택에서 복귀 */
e = jumpstack[--stackidx];
} else
break;
continue;
no_match:
e = ipt_next_entry(e);
} while (e != NULL);
return verdict;
}
코드 설명
- xt_table_info — per-CPU entries규칙 블롭은 per-CPU로 복제되어 있어 락 없이 접근 가능합니다.
iptables -R등으로 규칙이 변경되면 새 블롭을 할당하고table->private를 원자적으로 교체합니다. 이 구조는 nftables의 RCU 기반 체인 교체와 유사한 목적입니다. - ip_packet_match()규칙의 표준 필드(출발지/목적지 IP, 인터페이스, 프로토콜)를 빠르게 검사합니다. 확장 match를 호출하기 전에 이 단계에서 대부분의 불일치가 걸러지므로 성능에 중요합니다.
- jumpstack
-j CHAIN으로 사용자 정의 체인에 점프할 때 복귀 지점을 저장하는 per-CPU 스택입니다.XT_RETURNverdict 시 스택에서 이전 체인의 다음 규칙으로 돌아갑니다. 스택 깊이는XT_JUMP_STACK_SIZE(16)로 제한됩니다. - xt_ematch_foreach()규칙에 연결된 확장 match 체인을 순회하는 매크로입니다.
-m conntrack -m limit처럼 여러 match가 AND 조건으로 결합되며, 하나라도 실패하면 즉시 다음 규칙으로 건너뜁니다.
주요 match 모듈
| match 모듈 | 커널 소스 | 용도 | 사용 예 |
|---|---|---|---|
conntrack | xt_conntrack.c | 연결 상태 매칭 | -m conntrack --ctstate ESTABLISHED,RELATED |
multiport | xt_multiport.c | 여러 포트 동시 매칭 | -m multiport --dports 22,80,443 |
iprange | xt_iprange.c | IP 주소 범위 | -m iprange --src-range 10.0.0.1-10.0.0.100 |
string | xt_string.c | 페이로드(Payload) 문자열 검색 | -m string --string "malware" --algo bm |
limit | xt_limit.c | 속도 제한 (token bucket) | -m limit --limit 5/min --limit-burst 10 |
hashlimit | xt_hashlimit.c | per-IP/per-port 속도 제한 | -m hashlimit --hashlimit-above 10/sec --hashlimit-mode srcip |
recent | xt_recent.c | 최근 접속 IP 추적 | -m recent --name ssh --rcheck --seconds 60 --hitcount 3 |
owner | xt_owner.c | 소켓 소유자 (UID/GID) | -m owner --uid-owner 1000 |
mark | xt_mark.c | 패킷 마크 매칭 | -m mark --mark 0x1 |
connmark | xt_connmark.c | 연결 마크 매칭 | -m connmark --mark 0x2/0xff |
mac | xt_mac.c | MAC 주소 매칭 | -m mac --mac-source AA:BB:CC:DD:EE:FF |
physdev | xt_physdev.c | 브릿지 물리 디바이스 | -m physdev --physdev-in eth0 |
tcp | xt_tcpudp.c | TCP 플래그/옵션 | -p tcp --tcp-flags SYN,ACK SYN |
addrtype | xt_addrtype.c | 주소 타입 (LOCAL, BROADCAST 등) | -m addrtype --dst-type LOCAL |
comment | xt_comment.c | 규칙에 주석 추가 | -m comment --comment "Allow SSH" |
주요 target 모듈
| target | 유형 | 커널 소스 | 설명 |
|---|---|---|---|
ACCEPT | terminating | 빌트인 | 패킷 허용 |
DROP | terminating | 빌트인 | 패킷 무시 (무응답) |
REJECT | terminating | ipt_REJECT.c | 패킷 거부 + ICMP/TCP RST 응답 |
RETURN | terminating | 빌트인 | 현재 체인에서 호출자로 복귀 |
LOG | non-term | xt_LOG.c | 커널 로그에 패킷 정보 기록 |
NFLOG | non-term | xt_NFLOG.c | 유저스페이스 로깅 (ulogd2) |
NFQUEUE | terminating | xt_NFQUEUE.c | 유저스페이스 큐로 전달 |
MARK | non-term | xt_MARK.c | 패킷 마크(skb->mark) 설정 |
CONNMARK | non-term | xt_CONNMARK.c | 연결 마크(ct->mark) 설정/복사 |
DNAT | terminating | nf_nat | 목적지 주소/포트 변환 |
SNAT | terminating | nf_nat | 소스 주소/포트 변환 |
MASQUERADE | terminating | nf_nat | 동적 IP SNAT (인터페이스 IP 사용) |
REDIRECT | terminating | nf_nat | 로컬 포트로 리다이렉트 (투명 프록시) |
TPROXY | terminating | xt_TPROXY.c | 투명 프록시 (NAT 없이) |
TOS | non-term | xt_DSCP.c | IP TOS/DSCP 필드 수정 |
TTL | non-term | ipt_TTL.c | IP TTL 값 수정 |
TCPMSS | non-term | xt_TCPMSS.c | TCP MSS 클램핑 (MTU 문제 해결) |
CT | non-term | xt_CT.c | conntrack helper/zone/timeout 지정 |
NOTRACK | non-term | xt_CT.c | conntrack 추적 비활성화 |
SECMARK | non-term | xt_SECMARK.c | SELinux 보안 컨텍스트 설정 |
AUDIT | non-term | xt_AUDIT.c | 감사 로그 기록 |
iptables 명령어 구문
# 기본 구문
# iptables [-t 테이블] 명령 체인 [매치조건...] -j 타겟 [타겟옵션...]
# ── 체인 관리 ──
iptables -N MY_CHAIN # 커스텀 체인 생성
iptables -X MY_CHAIN # 커스텀 체인 삭제 (비어있어야 함)
iptables -P INPUT DROP # 빌트인 체인 기본 정책 설정
iptables -E OLD_CHAIN NEW_CHAIN # 체인 이름 변경
iptables -F INPUT # 체인의 모든 규칙 삭제 (flush)
iptables -Z INPUT # 카운터 초기화 (zero)
# ── 규칙 추가/삽입/삭제 ──
iptables -A INPUT ... # 체인 끝에 규칙 추가 (append)
iptables -I INPUT 1 ... # 체인 N번째에 삽입 (insert)
iptables -R INPUT 3 ... # 체인 3번 규칙 교체 (replace)
iptables -D INPUT 3 # 체인 3번 규칙 삭제 (by number)
iptables -D INPUT -p tcp --dport 22 -j ACCEPT # 규칙 명세로 삭제
# ── 조회 ──
iptables -L -n -v # 전체 규칙 조회 (숫자 표시, 카운터 포함)
iptables -L INPUT -n --line-numbers # 규칙 번호와 함께 조회
iptables -S # iptables-save 형식으로 출력
실전 규칙 예제
# ━━━ 기본 서버 방화벽 ━━━
# 기본 정책: 인바운드 차단, 아웃바운드 허용
iptables -P INPUT DROP
iptables -P FORWARD DROP
iptables -P OUTPUT ACCEPT
# 루프백 허용
iptables -A INPUT -i lo -j ACCEPT
# established/related 연결 허용 (stateful)
iptables -A INPUT -m conntrack --ctstate ESTABLISHED,RELATED -j ACCEPT
# invalid 패킷 차단
iptables -A INPUT -m conntrack --ctstate INVALID -j DROP
# ICMP (ping) 허용 (속도 제한)
iptables -A INPUT -p icmp --icmp-type echo-request \
-m limit --limit 1/s --limit-burst 4 -j ACCEPT
# SSH 허용 (brute force 방어: 60초 내 3회 초과 시 차단)
iptables -A INPUT -p tcp --dport 22 -m conntrack --ctstate NEW \
-m recent --name ssh --set
iptables -A INPUT -p tcp --dport 22 -m conntrack --ctstate NEW \
-m recent --name ssh --rcheck --seconds 60 --hitcount 4 -j DROP
iptables -A INPUT -p tcp --dport 22 -j ACCEPT
# HTTP/HTTPS 허용
iptables -A INPUT -p tcp -m multiport --dports 80,443 \
-m conntrack --ctstate NEW -j ACCEPT
# ━━━ SYN Flood 방어 ━━━
iptables -A INPUT -p tcp --syn -m hashlimit \
--hashlimit-above 30/sec \
--hashlimit-burst 50 \
--hashlimit-mode srcip \
--hashlimit-name syn_flood \
-j DROP
# ━━━ NAT (게이트웨이 서버) ━━━
# MASQUERADE (동적 IP 환경)
iptables -t nat -A POSTROUTING -o eth0 -j MASQUERADE
# SNAT (고정 IP 환경, MASQUERADE보다 빠름)
iptables -t nat -A POSTROUTING -o eth0 \
-j SNAT --to-source 203.0.113.1
# DNAT (포트 포워딩: 외부 8080 → 내부 10.0.0.5:80)
iptables -t nat -A PREROUTING -p tcp --dport 8080 \
-j DNAT --to-destination 10.0.0.5:80
iptables -A FORWARD -p tcp -d 10.0.0.5 --dport 80 \
-m conntrack --ctstate NEW -j ACCEPT
# REDIRECT (투명 프록시: 로컬 80 → 로컬 3128)
iptables -t nat -A PREROUTING -p tcp --dport 80 \
-j REDIRECT --to-ports 3128
# ━━━ 패킷 마킹 (정책 라우팅 연동) ━━━
iptables -t mangle -A PREROUTING -p tcp --dport 80 \
-j MARK --set-mark 0x1
# → ip rule add fwmark 0x1 table 100
# ━━━ TCP MSS 클램핑 (VPN/터널 MTU 문제 해결) ━━━
iptables -A FORWARD -p tcp --tcp-flags SYN,RST SYN \
-j TCPMSS --clamp-mss-to-pmtu
# ━━━ CONNMARK: 연결 단위 마킹 ━━━
# 새 연결에 마크 설정 → 연결 마크로 저장
iptables -t mangle -A PREROUTING -m conntrack --ctstate NEW \
-p tcp --dport 443 -j MARK --set-mark 0x2
iptables -t mangle -A PREROUTING -j CONNMARK --save-mark
# 이후 패킷에서 연결 마크 복원
iptables -t mangle -A PREROUTING -j CONNMARK --restore-mark
iptables-save / iptables-restore
규칙의 영속화와 원자적 교체에 사용됩니다. iptables-restore는 iptables 명령어를 반복 실행하는 것보다 훨씬 빠르며, 규칙을 원자적으로 교체하여 교체 중 패킷 누락을 방지합니다.
# 현재 규칙 저장
iptables-save > /etc/iptables/rules.v4
ip6tables-save > /etc/iptables/rules.v6
# 규칙 복원 (원자적 교체)
iptables-restore < /etc/iptables/rules.v4
ip6tables-restore < /etc/iptables/rules.v6
# 카운터 포함 저장 (-c)
iptables-save -c > /etc/iptables/rules.v4
# 특정 테이블만 저장
iptables-save -t filter > /etc/iptables/filter.rules
# iptables-save 출력 형식:
# *filter
# :INPUT DROP [0:0] ← 체인:정책 [패킷:바이트]
# :FORWARD DROP [0:0]
# :OUTPUT ACCEPT [0:0]
# -A INPUT -i lo -j ACCEPT
# -A INPUT -m conntrack --ctstate ESTABLISHED,RELATED -j ACCEPT
# -A INPUT -p tcp -m multiport --dports 22,80,443 -j ACCEPT
# COMMIT ← 원자적 커밋
# 부팅 시 자동 복원 (systemd)
# Debian/Ubuntu: apt install iptables-persistent
# → /etc/iptables/rules.v4, /etc/iptables/rules.v6 자동 로드
# RHEL/CentOS: iptables-services
# systemctl enable iptables
# → /etc/sysconfig/iptables 자동 로드
ip6tables (IPv6)
ip6tables는 IPv6 전용 iptables로, 구문은 iptables와 동일하지만 IPv6 고유 기능을 추가로 지원합니다.
# IPv6 기본 방화벽
ip6tables -P INPUT DROP
ip6tables -P FORWARD DROP
ip6tables -P OUTPUT ACCEPT
ip6tables -A INPUT -i lo -j ACCEPT
ip6tables -A INPUT -m conntrack --ctstate ESTABLISHED,RELATED -j ACCEPT
# ICMPv6는 IPv6 동작에 필수 — 전면 차단 금지!
# NDP(Neighbor Discovery), RA, Path MTU Discovery 등이 ICMPv6 사용
ip6tables -A INPUT -p icmpv6 --icmpv6-type router-advertisement -j ACCEPT
ip6tables -A INPUT -p icmpv6 --icmpv6-type router-solicitation -j ACCEPT
ip6tables -A INPUT -p icmpv6 --icmpv6-type neighbour-solicitation -j ACCEPT
ip6tables -A INPUT -p icmpv6 --icmpv6-type neighbour-advertisement -j ACCEPT
ip6tables -A INPUT -p icmpv6 --icmpv6-type echo-request \
-m limit --limit 1/s -j ACCEPT
# 서비스 허용
ip6tables -A INPUT -p tcp -m multiport --dports 22,80,443 -j ACCEPT
IPv6 ICMPv6 주의: IPv4와 달리 IPv6에서는 ICMPv6를 전면 차단하면 NDP(Neighbor Discovery Protocol)가 동작하지 않아 네트워크 통신이 완전히 단절됩니다. router-solicitation, router-advertisement, neighbour-solicitation, neighbour-advertisement 타입은 반드시 허용해야 합니다.
iptables 호환 레이어 아키텍처
현대 배포판에서 iptables 명령어는 두 가지 경로로 커널과 통신합니다:
혼용 금지: iptables-nft와 iptables-legacy를 같은 시스템에서 혼용하면 규칙이 별도 데이터 경로에 저장되어 예상치 못한 동작이 발생합니다. update-alternatives로 하나만 선택하거나, 순수 nft로 통일하세요.
iptables → nftables 마이그레이션
# 개별 규칙 변환
iptables-translate -A INPUT -p tcp --dport 22 -j ACCEPT
# 출력: nft add rule ip filter INPUT tcp dport 22 counter accept
iptables-translate -t nat -A POSTROUTING -o eth0 -j MASQUERADE
# 출력: nft add rule ip nat POSTROUTING oifname "eth0" counter masquerade
# 전체 규칙셋 변환
iptables-save | iptables-restore-translate > nftables-rules.nft
ip6tables-save | ip6tables-restore-translate >> nftables-rules.nft
# 변환된 nftables 규칙 적용
nft -f nftables-rules.nft
# 커널의 iptables 백엔드 확인
iptables -V
# iptables v1.8.x (nf_tables) ← nft 백엔드 (권장)
# iptables v1.8.x (legacy) ← 레거시 x_tables
# 백엔드 전환 (Debian/Ubuntu)
update-alternatives --config iptables
# 1. /usr/sbin/iptables-nft (nf_tables 백엔드)
# 2. /usr/sbin/iptables-legacy (x_tables 백엔드)
Connection Tracking
Connection tracking(conntrack)은 Netfilter의 상태 추적 엔진으로, 모든 네트워크 연결의 상태를 해시 테이블(Hash Table)에 기록합니다. 방화벽의 stateful 규칙, NAT, 프로토콜 helper 모두 conntrack 위에 동작합니다.
struct nf_conn
/* include/net/netfilter/nf_conntrack.h */
struct nf_conn {
struct nf_conntrack ct_general; /* 참조 카운트 */
/* 양방향 튜플 (원본 + 응답) */
struct nf_conntrack_tuple_hash tuplehash[IP_CT_DIR_MAX];
unsigned long status; /* IPS_* 플래그 */
u32 mark; /* CONNMARK */
/* 타임아웃 (jiffies 기반) */
struct timer_list timeout;
/* 프로토콜별 상태 (TCP window tracking 등) */
union nf_conntrack_proto proto;
/* 확장 데이터 (helper, nat, selinux 등) */
struct nf_ct_ext *ext;
/* conntrack 존 (멀티테넌트 격리) */
struct nf_conntrack_zone zone;
};
/* 연결 상태 (ct state) */
enum ip_conntrack_info {
IP_CT_ESTABLISHED, /* 양방향 패킷 확인 */
IP_CT_RELATED, /* 기존 연결과 관련 (ICMP error, FTP data 등) */
IP_CT_NEW, /* 새 연결 */
IP_CT_IS_REPLY, /* 응답 방향 */
IP_CT_ESTABLISHED_REPLY = IP_CT_ESTABLISHED + IP_CT_IS_REPLY,
IP_CT_RELATED_REPLY = IP_CT_RELATED + IP_CT_IS_REPLY,
};
코드 설명
- ct_general
struct nf_conntrack는 단순히 atomic 참조 카운터(refcnt)를 감싸는 구조체입니다.nf_ct_get(skb, &ctinfo)로 sk_buff에서 nf_conn 포인터를 가져올 때 이 카운터를 기반으로 안전한 참조가 이루어집니다. - tuplehash[IP_CT_DIR_MAX]원본(ORIGINAL) 방향과 응답(REPLY) 방향 두 개의 튜플 해시를 저장합니다. 각 튜플은 (src_ip, dst_ip, src_port, dst_port, proto)로 구성되며, conntrack 해시 테이블에 양방향으로 삽입됩니다.
- status연결 상태 플래그 비트맵입니다. 주요 플래그:
IPS_CONFIRMED(POSTROUTING 통과로 확정),IPS_NAT_MASK(NAT 방향),IPS_DYING(만료 예정),IPS_FIXED_TIMEOUT(타임아웃 고정). atomic long 타입으로 lock 없이 비트 조작이 가능합니다. - markconntrack 레벨 마킹 값(CONNMARK)입니다.
ct mark set <값>규칙으로 설정하고 이후 패킷에서ct mark로 읽어 정책 라우팅, QoS 등에 활용합니다. sk_buff의skb->mark(패킷 마크)와 별도입니다. - timeout커널 타이머로 일정 시간 트래픽이 없으면 연결을 만료시킵니다. 프로토콜별 타임아웃 정책(
nf_conntrack_tcp_timeout_established등)이 적용됩니다. 패킷이 올 때마다mod_timer()로 갱신됩니다. - proto (union nf_conntrack_proto)프로토콜별 상태를 저장하는 공용체입니다. TCP는 시퀀스 번호 추적을 위한
struct ip_ct_tcp(양방향 window 상태), UDP/ICMP는 간단한 구조체를 사용합니다. TCP 시퀀스 검증은CONFIG_NF_CT_PROTO_TCP_LIBERAL옵션으로 엄격도를 조절합니다. - ext (struct nf_ct_ext*)확장 데이터 슬롯입니다. NAT 변환 정보(
struct nf_conn_nat), conntrack helper, SELinux 레이블, acct(바이트/패킷 카운터), timestamp 등이 동적으로 연결됩니다.nf_ct_ext_find(ct, id)로 각 확장 슬롯에 접근합니다. - zone (struct nf_conntrack_zone)멀티테넌트 격리를 위한 존 ID를 담습니다. 동일 5-튜플이 다른 존에 속하면 별도의 conntrack 엔트리로 관리됩니다. 컨테이너 오버레이 네트워크에서 IP 주소 충돌을 허용할 때 사용합니다.
nf_conntrack_in() 소스 분석
nf_conntrack_in()은 conntrack의 핵심 진입 함수로, PREROUTING과 OUTPUT 훅에서 호출됩니다. 패킷의 5-튜플로 기존 연결을 조회하거나 신규 연결 엔트리를 생성합니다.
/* net/netfilter/nf_conntrack_core.c — conntrack 훅 진입점 (요약) */
unsigned int
nf_conntrack_in(struct sk_buff *skb,
const struct nf_hook_state *state)
{
const struct nf_conntrack_l4proto *l4proto;
struct nf_conn *ct;
enum ip_conntrack_info ctinfo;
int dataoff, ret;
u8 protonum;
ret = get_l4proto(skb, skb_network_offset(skb),
state->pf, &protonum, &dataoff); /* L4 프로토콜 판별 */
if (ret <= 0)
return NF_ACCEPT; /* 조각 패킷 등 추적 불가 → 통과 */
l4proto = nf_ct_l4proto_find(protonum);
ct = resolve_normal_ct(skb, dataoff, state->pf,
protonum, l4proto, &ctinfo); /* 해시 조회 or 신규 생성 */
if (!ct) {
skb->_nfct = 0;
return NF_ACCEPT;
}
if (IS_ERR(ct)) {
NF_CT_STAT_INC_ATOMIC(state->net, error); /* conntrack 에러 통계 증가 */
return NF_DROP;
}
ret = l4proto->packet(ct, skb, dataoff, ctinfo, state); /* L4 상태 기계 갱신 */
if (ret <= 0) {
nf_ct_put(ct);
skb->_nfct = 0;
return -ret;
}
return NF_ACCEPT;
}
코드 설명
- get_l4proto()IP 헤더를 분석하여 L4 프로토콜 번호와 페이로드 오프셋을 구합니다. IPv6 확장 헤더 체인도 순회합니다. 조각(fragment) 패킷은 재조립 전까지 conntrack에서 처리할 수 없어 NF_ACCEPT로 통과시킵니다.
- resolve_normal_ct()5-튜플 해시로
nf_conntrack_hash테이블을 탐색합니다. 기존 엔트리가 있으면 반환하고, 없으면__nf_conntrack_alloc()으로 새nf_conn을 할당합니다. 이때 상태는 아직 UNCONFIRMED이며, POSTROUTING을 통과해야IPS_CONFIRMED가 설정됩니다. - skb->_nfctsk_buff와 nf_conn을 연결하는 포인터+ctinfo 필드입니다. 하위 3비트에
ip_conntrack_info열거값이, 나머지 비트에 nf_conn 포인터가 저장됩니다.nf_ct_get(skb, &ctinfo)로 원자적으로 분리합니다. - l4proto->packet()프로토콜별 상태 기계를 갱신합니다. TCP의 경우 시퀀스 번호 윈도우를 검사하고, 유효하지 않은 패킷은
NF_DROP을 반환합니다. UDP/ICMP는 단순히 타임아웃만 갱신합니다. - NF_CT_STAT_INC_ATOMIC()per-CPU conntrack 통계를 원자적으로 증가합니다.
/proc/net/stat/nf_conntrack또는conntrack -S에서 확인할 수 있으며, drop/insert_failed/error 등 운영 디버깅에 중요한 지표입니다.
conntrack 튜플과 해시 조회
conntrack의 핵심 자료구조는 5-튜플(출발지/목적지 IP, 출발지/목적지 포트, 프로토콜)을 표현하는 nf_conntrack_tuple입니다. 각 연결은 원본 방향(ORIGINAL)과 응답 방향(REPLY) 두 개의 튜플로 식별되며, 해시 테이블에서 빠르게 조회됩니다.
/* include/net/netfilter/nf_conntrack_tuple.h */
struct nf_conntrack_tuple {
struct nf_conntrack_man {
union nf_inet_addr u3; /* src IP (v4/v6) */
union nf_conntrack_man_proto u;
/* src port / ICMP id / GRE key 등 */
u_int16_t l3num; /* AF_INET / AF_INET6 */
} src;
struct {
union nf_inet_addr u3; /* dst IP */
union {
__be16 all;
struct { __be16 port; } tcp;
struct { __be16 port; } udp;
struct { u_int8_t type, code; } icmp;
} u;
u_int8_t protonum; /* IPPROTO_* */
u_int8_t dir; /* IP_CT_DIR_* */
} dst;
};
/* 해시 테이블 노드 */
struct nf_conntrack_tuple_hash {
struct hlist_nulls_node hnnode;
struct nf_conntrack_tuple tuple;
};
/* net/netfilter/nf_conntrack_core.c — 해시 계산 */
static u32
hash_conntrack_raw(const struct nf_conntrack_tuple *tuple,
unsigned int zoneid,
const struct net *net)
{
u64 a, b, c;
get_random_once(&nf_conntrack_hash_rnd,
sizeof(nf_conntrack_hash_rnd));
a = (u64)tuple->src.u3.all[0] << 32 |
tuple->src.u3.all[1];
b = (u64)tuple->dst.u3.all[0] << 32 |
tuple->dst.u3.all[1];
c = (u64)tuple->src.u.all << 16 |
tuple->dst.u.all |
(u64)tuple->dst.protonum << 32 |
(u64)zoneid << 40;
return (u32)siphash(&a, &b, &c,
&nf_conntrack_hash_rnd);
}
/* 해시 조회 */
struct nf_conntrack_tuple_hash *
nf_conntrack_find_get(struct net *net,
const struct nf_conntrack_zone *zone,
const struct nf_conntrack_tuple *tuple)
{
struct nf_conntrack_tuple_hash *h;
unsigned int hash, bucket;
hash = hash_conntrack_raw(tuple, zone->id, net);
bucket = reciprocal_scale(hash,
net->ct.htable_size);
/* RCU 보호 하에 hlist_nulls 순회 */
hlist_nulls_for_each_entry_rcu(h,
&nf_conntrack_hash[bucket], hnnode) {
if (nf_ct_tuple_equal(tuple, &h->tuple) &&
nf_ct_zone_equal_any(h->tuple, zone)) {
if (atomic_inc_not_zero(
&nf_ct_tuplehash_to_ctrack(h)
->ct_general.use))
return h;
}
}
return NULL;
}
코드 설명
- nf_conntrack_tuplesrc 방향은
nf_conntrack_man으로 NAT 변환 대상(manip) 필드를 포함합니다. dst의dir필드가 ORIGINAL/REPLY 방향을 구분합니다. 하나의nf_conn에는tuplehash[IP_CT_DIR_ORIGINAL]과tuplehash[IP_CT_DIR_REPLY]두 해시 노드가 포함됩니다. - hash_conntrack_raw()SipHash를 사용하여 튜플의 5-tuple + zone ID로 해시값을 계산합니다.
nf_conntrack_hash_rnd는 부트 시 한 번 초기화되는 시크릿으로, 해시 충돌 공격(HashDoS)을 방지합니다. IPv6의 경우 128비트 주소 전체가 해시 입력에 포함됩니다. - nf_conntrack_find_get()해시 버킷을 RCU로 보호하며 순회합니다.
hlist_nulls자료구조를 사용하여 false negative 없이 lockless 조회가 가능합니다. 참조 카운트를atomic_inc_not_zero()로 안전하게 증가시켜, 동시에 삭제 중인 엔트리를 반환하지 않습니다.
conntrack 생명주기: 할당 → 확인 → 삭제
conntrack 엔트리는 PREROUTING에서 할당되어 UNCONFIRMED 상태로 패킷과 함께 스택을 통과하고, POSTROUTING에서 해시 테이블에 삽입(confirm)됩니다. 이 2단계 설계는 패킷이 중간에 DROP되더라도 해시 테이블이 오염되지 않도록 보장합니다.
/* net/netfilter/nf_conntrack_core.c — 엔트리 할당 */
struct nf_conn *
__nf_conntrack_alloc(struct net *net,
const struct nf_conntrack_zone *zone,
const struct nf_conntrack_tuple *orig,
const struct nf_conntrack_tuple *repl,
gfp_t gfp)
{
struct nf_conn *ct;
/* 전역 한도 검사 */
if (nf_conntrack_max &&
atomic_read(&net->ct.count) >= nf_conntrack_max) {
if (!early_drop(net, hash))
return ERR_PTR(-ENOMEM);
}
ct = kmem_cache_alloc(nf_conntrack_cachep, gfp);
if (!ct)
return ERR_PTR(-ENOMEM);
memset(&ct->__nfct_init_offset, 0,
offsetofend(struct nf_conn, proto) -
offsetof(struct nf_conn, __nfct_init_offset));
ct->tuplehash[IP_CT_DIR_ORIGINAL].tuple = *orig;
ct->tuplehash[IP_CT_DIR_REPLY].tuple = *repl;
ct->status = 0; /* UNCONFIRMED */
atomic_set(&ct->ct_general.use, 1);
return ct;
}
/* POSTROUTING에서 호출 — 해시 테이블 삽입 */
int __nf_conntrack_confirm(struct sk_buff *skb)
{
struct nf_conn *ct;
enum ip_conntrack_info ctinfo;
unsigned int hash, reply_hash;
ct = nf_ct_get(skb, &ctinfo);
/* 이미 confirmed이면 건너뜀 */
if (CTINFO2DIR(ctinfo) != IP_CT_DIR_ORIGINAL ||
test_bit(IPS_CONFIRMED_BIT, &ct->status))
return NF_ACCEPT;
hash = hash_conntrack_raw(
&ct->tuplehash[IP_CT_DIR_ORIGINAL].tuple,
nf_ct_zone_id(nf_ct_zone(ct), IP_CT_DIR_ORIGINAL),
nf_ct_net(ct));
reply_hash = hash_conntrack_raw(
&ct->tuplehash[IP_CT_DIR_REPLY].tuple,
nf_ct_zone_id(nf_ct_zone(ct), IP_CT_DIR_REPLY),
nf_ct_net(ct));
spin_lock(&nf_conntrack_locks[hash % CONNTRACK_LOCKS]);
/* 충돌 검사: 동일 튜플이 이미 있으면 실패 */
__set_bit(IPS_CONFIRMED_BIT, &ct->status);
__nf_conntrack_hash_insert(ct, hash, reply_hash);
spin_unlock(&nf_conntrack_locks[hash % CONNTRACK_LOCKS]);
return NF_ACCEPT;
}
/* conntrack 삭제 */
void nf_ct_delete(struct nf_conn *ct,
u32 portid, int report)
{
set_bit(IPS_DYING_BIT, &ct->status);
nf_ct_delete_from_lists(ct);
nf_conntrack_ecache_work(
nf_ct_net(ct)); /* 이벤트 통지 */
nf_conntrack_put(&ct->ct_general);
}
/* skb ↔ ct 연결 헬퍼 */
static inline struct nf_conn *
nf_ct_get(const struct sk_buff *skb,
enum ip_conntrack_info *ctinfo)
{
unsigned long nfct = skb->_nfct;
*ctinfo = nfct & NFCT_INFOMASK;
return (struct nf_conn *)(nfct & NFCT_PTRMASK);
}
static inline void
nf_ct_set(struct sk_buff *skb,
struct nf_conn *ct,
enum ip_conntrack_info info)
{
skb->_nfct = (unsigned long)ct | info;
}
코드 설명
- __nf_conntrack_alloc()slab 캐시(
nf_conntrack_cachep)에서nf_conn을 할당합니다.nf_conntrack_max한도를 초과하면early_drop()으로 가장 오래된 비보장(non-ASSURED) 엔트리를 강제 삭제합니다. 할당 시 원본/응답 튜플을 복사하고, status를 0(UNCONFIRMED)으로 초기화합니다. - __nf_conntrack_confirm()POSTROUTING 훅에서 호출되어 UNCONFIRMED 엔트리를 전역 해시 테이블에 삽입합니다. 원본 방향과 응답 방향 두 해시 버킷에 동시에 삽입하며, 이미 동일 튜플이 존재하면 충돌로 실패합니다. 세분화된 해시 버킷 락(
CONNTRACK_LOCKS= 1024)으로 동시성을 확보합니다. - nf_ct_delete()
IPS_DYING플래그를 설정하고 해시 테이블에서 제거합니다. 이벤트 캐시(ecache)를 통해 유저스페이스(conntrack -E)에 삭제 이벤트를 통지합니다. 참조 카운트가 0이 되면 RCU 콜백으로 메모리가 해제됩니다. - nf_ct_get() / nf_ct_set()
skb->_nfct의 하위 3비트에ip_conntrack_info(방향 + 상태)를, 상위 비트에nf_conn포인터를 비트 패킹합니다. 이 방식으로 skb 하나에 별도 필드 추가 없이 conntrack 정보를 저장합니다.
IPS_* 상태 플래그 상세
conntrack 엔트리의 status 필드는 비트 플래그로 연결의 현재 상태를 나타냅니다. 각 플래그는 include/uapi/linux/netfilter/nf_conntrack_common.h에 정의되어 있습니다.
| 플래그 | 비트 | 설명 |
|---|---|---|
IPS_EXPECTED | 0 | expectation에 의해 생성된 연결 (예: FTP 데이터 채널) |
IPS_SEEN_REPLY | 1 | 양방향 트래픽이 관측됨. 이 플래그 없으면 단방향(half-open) 상태 |
IPS_ASSURED | 2 | 연결이 확립됨. early_drop() 대상에서 제외 |
IPS_CONFIRMED | 3 | 해시 테이블에 삽입 완료. __nf_conntrack_confirm()이 설정 |
IPS_SRC_NAT | 4 | 출발지 NAT(SNAT/MASQUERADE) 적용됨 |
IPS_DST_NAT | 5 | 목적지 NAT(DNAT/REDIRECT) 적용됨 |
IPS_SEQ_ADJUST | 6 | TCP 시퀀스 번호 조정 필요 (NAT helper가 페이로드를 변경한 경우) |
IPS_SRC_NAT_DONE | 7 | SNAT 훅을 이미 통과함 (중복 NAT 방지) |
IPS_DST_NAT_DONE | 8 | DNAT 훅을 이미 통과함 |
IPS_DYING | 9 | 삭제 진행 중. nf_ct_delete()가 설정 |
IPS_FIXED_TIMEOUT | 10 | 사용자가 타임아웃을 고정함 (conntrack -U). 프로토콜 핸들러의 타임아웃 갱신을 무시 |
IPS_TEMPLATE | 11 | 실제 연결이 아닌 CT 템플릿 (규칙의 -j CT 타겟용) |
IPS_OFFLOAD | 12 | flowtable/하드웨어 오프로드 중. conntrack 통계에서 제외됨 |
IPS_HW_OFFLOAD | 13 | 하드웨어 플로우 오프로드 활성 (NIC TC 오프로드) |
nft_do_chain() 소스 분석
nftables 체인 평가 핵심 함수입니다. nftables 훅 콜백에서 호출되며, 체인에 등록된 규칙 바이트코드를 순차 실행하여 verdict를 산출합니다.
/* net/netfilter/nf_tables_core.c — nftables 체인 평가 엔진 (요약) */
unsigned int
nft_do_chain(struct nft_pktinfo *pkt,
void *priv)
{
const struct nft_chain *chain = priv, *basechain = chain;
const struct nft_rule_dp *rule;
const struct nft_expr *expr, *last;
struct nft_regs regs; /* 가상 레지스터 파일 (식 간 값 전달) */
unsigned int stackptr = 0;
struct nft_jumpstack jumpstack[NFT_JUMP_STACK_SIZE]; /* 점프 복귀 스택 (최대 16단계) */
next_rule:
regs.verdict.code = NFT_CONTINUE; /* 규칙마다 verdict 초기화 */
rule = (struct nft_rule_dp *)rcu_dereference(
chain->blob_gen_0->data); /* RCU로 규칙 바이트코드 접근 */
nft_rule_dp_for_each_expr(expr, last, rule) {
expr->ops->eval(expr, ®s, pkt); /* 표현식 평가 (payload/cmp/lookup 등) */
if (regs.verdict.code != NFT_CONTINUE)
break; /* verdict 확정 시 표현식 루프 탈출 */
}
switch (regs.verdict.code) {
case NFT_BREAK:
regs.verdict.code = NFT_CONTINUE;
continue; /* 현재 규칙 건너뛰고 다음으로 */
case NFT_CONTINUE:
rule = nft_rule_next(rule);
goto next_rule; /* 다음 규칙으로 이동 */
case NFT_JUMP:
case NFT_GOTO:
if (regs.verdict.code == NFT_JUMP) {
jumpstack[stackptr].chain = chain; /* JUMP: 복귀 주소 저장 */
jumpstack[stackptr].rule = nft_rule_next(rule);
stackptr++;
}
chain = regs.verdict.chain; /* 대상 체인으로 교체 */
goto next_rule;
case NFT_RETURN:
if (stackptr > 0) {
stackptr--; /* RETURN: 스택에서 복귀 체인 꺼냄 */
chain = jumpstack[stackptr].chain;
rule = jumpstack[stackptr].rule;
goto next_rule;
}
break;
}
return nft_verdict_to_nf(regs.verdict.code); /* NFT_ACCEPT/DROP → NF_ACCEPT/DROP 변환 */
}
코드 설명
- nft_regsnftables 가상 머신의 레지스터 파일입니다. 표현식(payload, meta, ct 등)이 읽어낸 값을 레지스터에 저장하고, 이후 cmp/lookup 표현식이 레지스터 값을 비교합니다. 레지스터 0은 verdict 코드에 예약됩니다.
- jumpstacknftables JUMP verdict 시 호출 체인을 최대 16단계까지 중첩할 수 있는 스택입니다. GOTO와 달리 JUMP는 복귀 주소를 스택에 저장하여 하위 체인 평가 후 원래 규칙 다음으로 돌아옵니다.
- blob_gen_0 (RCU)규칙 바이트코드를 담은 blob 포인터는 두 세대(gen_0, gen_1)로 관리됩니다.
nft commit시 새 규칙 세트를 gen_1에 쓰고, RCU grace period 후 gen_0와 교체합니다. 이로써 패킷 처리 도중 무잠금(lock-free) 원자적 규칙 갱신이 가능합니다. - expr->ops->eval()각 표현식 타입의 평가 함수를 간접 호출합니다. payload 표현식은 패킷 헤더에서 데이터를 읽고, cmp는 레지스터와 비교하며, lookup은 set을 조회하고, counter는 통계를 갱신합니다. verdict 표현식은 regs.verdict.code를 설정합니다.
- NFT_BREAK현재 규칙의 남은 표현식을 건너뛰되 체인은 계속 평가합니다. nftables 내부 흐름 제어에 사용됩니다.
- NFT_JUMP / NFT_GOTOJUMP는 현재 위치를 스택에 저장 후 대상 체인으로 이동하고, GOTO는 복귀 없이 대상 체인으로 이동합니다. iptables의 -j CHAIN(JUMP)과 -j RETURN 패턴과 유사합니다.
- nft_verdict_to_nf()nftables 내부 verdict 코드(NFT_ACCEPT=0, NFT_DROP=0x…)를 Netfilter 공통 verdict(NF_ACCEPT=1, NF_DROP=0)로 변환합니다. 이 변환 결과가
nf_hook_slow()의 switch 분기로 전달됩니다.
conntrack 해시 테이블
conntrack 엔트리는 소스/목적지 IP, 포트, 프로토콜로 구성된 튜플(tuple)의 해시(Hash)값으로 해시 테이블에 저장됩니다. 기본 해시 크기는 nf_conntrack_buckets 파라미터로 조정합니다.
# conntrack 해시 테이블 크기 확인/조정
sysctl net.netfilter.nf_conntrack_buckets
sysctl -w net.netfilter.nf_conntrack_buckets=65536
# 최대 추적 연결 수
sysctl net.netfilter.nf_conntrack_max
sysctl -w net.netfilter.nf_conntrack_max=262144
# 현재 추적 중인 연결 수
conntrack -C
# 또는
cat /proc/sys/net/netfilter/nf_conntrack_count
conntrack 존 (Zone)
conntrack 존은 동일한 튜플을 가진 연결을 격리합니다. 멀티테넌트 환경이나 컨테이너에서 겹치는 IP 주소 공간(Address Space)을 처리할 때 사용합니다.
# nftables에서 conntrack 존 할당
nft add rule inet raw prerouting iifname "veth-tenant1" ct zone set 1
nft add rule inet raw prerouting iifname "veth-tenant2" ct zone set 2
nft add rule inet raw output oifname "veth-tenant1" ct zone set 1
nft add rule inet raw output oifname "veth-tenant2" ct zone set 2
conntrack Helper 프레임워크
conntrack helper는 FTP, SIP, TFTP 등 데이터 연결을 별도로 여는 프로토콜의 관련(related) 연결을 자동으로 추적합니다.
/* include/net/netfilter/nf_conntrack_helper.h */
struct nf_conntrack_helper {
struct hlist_node hnode;
char name[NF_CT_HELPER_NAME_LEN];
struct module *me;
struct nf_conntrack_expect_policy *expect_policy;
unsigned int expect_class_max;
/* 패킷 검사 콜백 */
int (*help)(struct sk_buff *skb,
unsigned int protoff,
struct nf_conn *ct,
enum ip_conntrack_info conntrackinfo);
struct nf_conntrack_tuple tuple;
};
int nf_conntrack_helper_register(struct nf_conntrack_helper *helper);
void nf_conntrack_helper_unregister(struct nf_conntrack_helper *helper);
코드 설명
- nf_conntrack_helperconntrack helper 프레임워크의 핵심 구조체입니다(
include/net/netfilter/nf_conntrack_helper.h). FTP, SIP, TFTP 등 제어 채널에서 데이터 채널 정보를 파싱하여 관련(RELATED) 연결을 자동 추적합니다.tuple필드가 어떤 프로토콜/포트에 이 helper를 매칭할지 결정합니다. - help() 콜백conntrack이 추적 중인 연결의 패킷이 도착할 때마다 호출됩니다. 페이로드를 검사하여 데이터 연결의 IP/포트를 추출하고,
nf_ct_expect_alloc()+nf_ct_expect_related()로 expectation을 생성합니다. 이후 해당 튜플에 매칭되는 새 연결은 자동으로IP_CT_RELATED상태로 분류됩니다. - expect_policyhelper가 생성할 수 있는 expectation의 정책을 정의합니다.
max_expected는 동시 활성 expectation 최대 수이고,timeout은 expectation 만료 시간(초)입니다. FTP의 경우 동시 데이터 연결이 제한적이므로max_expected를 작게 설정합니다. - nf_conntrack_helper_register()helper를 전역 해시 테이블에 등록합니다. 이후 conntrack이 새 연결을 추적할 때
tuple필드와 매칭되면 해당 helper가 연결에 자동 할당됩니다. 보안상 Linux 4.7부터는 자동 할당이 기본 비활성화되어, nftablesct helper문으로 명시적 할당이 권장됩니다.
conntrack 이벤트와 ctnetlink
# 실시간 conntrack 이벤트 모니터링
conntrack -E
# [NEW] tcp 6 120 SYN_SENT src=10.0.0.1 dst=93.184.216.34 ...
# [UPDATE] tcp 6 60 SYN_RECV ...
# [UPDATE] tcp 6 432000 ESTABLISHED ...
# conntrack 내용 조회
conntrack -L
conntrack -L -p tcp --state ESTABLISHED
# 특정 연결 삭제
conntrack -D -s 10.0.0.1 -d 93.184.216.34
TCP Window Tracking
conntrack은 TCP 연결에 대해 시퀀스 번호, 윈도우 크기, 스케일 팩터를 추적하여 유효하지 않은 패킷을 탐지합니다.
/* net/netfilter/nf_conntrack_proto_tcp.c */
struct ip_ct_tcp {
struct ip_ct_tcp_state seen[2]; /* 양방향 TCP 상태 */
u8 state; /* TCP_CONNTRACK_* */
u8 last_dir;
u8 retrans;
u8 last_index;
u32 last_seq;
u32 last_ack;
u32 last_end;
u16 last_win;
};
struct ip_ct_tcp_state {
u32 td_end; /* 시퀀스 윈도우 끝 */
u32 td_maxend; /* 최대 끝 */
u32 td_maxwin; /* 최대 윈도우 크기 */
u32 td_maxack; /* 최대 ACK */
u8 td_scale; /* 윈도우 스케일 팩터 */
u8 flags;
};
코드 설명
- ip_ct_tcp — seen[2]양방향(ORIGINAL, REPLY) TCP 윈도우 상태를 각각 추적합니다(
net/netfilter/nf_conntrack_proto_tcp.c). 각 방향의 시퀀스 번호 범위, 최대 윈도우 크기, 스케일 팩터를 기록하여 유효하지 않은 패킷(윈도우 바깥 시퀀스, 비정상 ACK)을 탐지합니다. - state (TCP_CONNTRACK_*)conntrack이 추적하는 TCP 연결의 현재 상태입니다.
TCP_CONNTRACK_SYN_SENT,TCP_CONNTRACK_ESTABLISHED,TCP_CONNTRACK_TIME_WAIT등의 값을 가집니다. 이 상태에 따라 적용되는 타임아웃이 달라지며,nf_conntrack_tcp_timeout_*sysctl로 조정합니다. - ip_ct_tcp_state — td_end/td_maxend
td_end는 해당 방향에서 다음에 올 것으로 예상되는 시퀀스 번호이고,td_maxend는 윈도우를 고려한 최대 허용 시퀀스입니다. 이 범위를 벗어나는 패킷은nf_ct_tcp_be_liberalsysctl이 비활성화된 경우 INVALID로 판정되어 DROP됩니다. 이 엄격한 검증이 시퀀스 번호 기반 공격을 방어합니다. - td_scaleTCP Window Scale 옵션(RFC 7323)의 스케일 팩터입니다. SYN/SYN-ACK에서 한 번만 교환되며, conntrack은 이 값을 기억하여 이후 패킷의 실제 윈도우 크기를 올바르게 계산합니다. NAT가 SYN 패킷을 수정할 경우에도 스케일 팩터를 일관되게 유지해야 합니다.
- retrans/last_seq/last_ack재전송 탐지 및 시퀀스 추적에 사용되는 필드들입니다.
retrans는 연속 재전송 횟수로, 임계값 초과 시 연결 상태를 재설정할 수 있습니다.last_seq와last_ack는 마지막으로 관찰한 시퀀스/ACK 번호로, 중복 패킷과 재전송을 구분하는 데 사용됩니다.
conntrack 타임아웃 정책
# 프로토콜별 기본 타임아웃 확인
sysctl net.netfilter.nf_conntrack_tcp_timeout_established
# 432000 (5일)
sysctl net.netfilter.nf_conntrack_tcp_timeout_time_wait
# 120 (2분)
sysctl net.netfilter.nf_conntrack_udp_timeout
# 30 (30초)
sysctl net.netfilter.nf_conntrack_udp_timeout_stream
# 120 (2분)
sysctl net.netfilter.nf_conntrack_icmp_timeout
# 30 (30초)
# nftables에서 커스텀 타임아웃 정책
nft add ct timeout inet my_filter web_timeout \
'{ protocol tcp; l3proto ip; \
policy = { established: 3600, close_wait: 10, close: 10 }; }'
nft add rule inet my_filter input tcp dport 80 \
ct timeout set "web_timeout"
- Netfilter Flowtable — SW/HW 오프로드로 conntrack bypass하여 성능 극대화
- conntrack 헬퍼 & ALG — FTP/SIP/H.323 등 프로토콜별 conntrack 헬퍼 구현
- NAT — nf_nat + conntrack 튜플 변환, CGNAT, NAT64
NAT 코어 함수와 conntrack 통합
NAT는 conntrack 위에 구축되어 있으며, 연결의 첫 패킷에서 튜플 변환 규칙을 설정하고 이후 패킷은 conntrack 매핑을 참조하여 자동 변환됩니다. NAT 훅은 DNAT(PREROUTING, priority -100)과 SNAT(POSTROUTING, priority 100)에 등록되어 conntrack 훅(priority -200/300) 사이에 위치합니다.
/* net/netfilter/nf_nat_core.c — NAT 바인딩 설정 */
unsigned int
nf_nat_setup_info(struct nf_conn *ct,
const struct nf_nat_range2 *range,
enum nf_nat_manip_type maniptype)
{
struct nf_conntrack_tuple curr_tuple, new_tuple;
/* 현재 응답 방향 튜플 가져오기 */
nf_ct_invert_tuple(&curr_tuple,
&ct->tuplehash[IP_CT_DIR_REPLY].tuple);
/* 범위 내에서 유일한 새 튜플 선택 */
get_unique_tuple(&new_tuple, &curr_tuple,
range, ct, maniptype);
if (!nf_ct_tuple_equal(&new_tuple, &curr_tuple)) {
struct nf_conntrack_tuple reply;
nf_ct_invert_tuple(&reply, &new_tuple);
nf_conntrack_alter_reply(ct, &reply);
if (maniptype == NF_NAT_MANIP_SRC)
ct->status |= IPS_SRC_NAT;
else
ct->status |= IPS_DST_NAT;
}
if (maniptype == NF_NAT_MANIP_SRC)
ct->status |= IPS_SRC_NAT_DONE;
else
ct->status |= IPS_DST_NAT_DONE;
return NF_ACCEPT;
}
/* 패킷별 NAT 변환 실행 */
unsigned int
nf_nat_packet(struct nf_conn *ct,
enum ip_conntrack_info ctinfo,
unsigned int hooknum,
struct sk_buff *skb)
{
enum nf_nat_manip_type mtype =
HOOK2MANIP(hooknum);
enum ip_conntrack_dir dir = CTINFO2DIR(ctinfo);
unsigned int verdict = NF_ACCEPT;
/* 방향에 따라 변환할 튜플 결정 */
if (!nf_nat_manip_pkt(skb, ct, mtype, dir))
verdict = NF_DROP;
return verdict;
}
/* 실제 헤더 재작성 */
static bool
nf_nat_manip_pkt(struct sk_buff *skb,
struct nf_conn *ct,
enum nf_nat_manip_type mtype,
enum ip_conntrack_dir dir)
{
struct nf_conntrack_tuple target;
/* conntrack의 reply 튜플에서 변환 대상 추출 */
if (mtype == NF_NAT_MANIP_SRC)
target = ct->tuplehash[!dir].tuple;
else
target = ct->tuplehash[dir].tuple;
/* L3 프로토콜별 핸들러로 IP/포트 재작성 */
return l3proto_manip_pkt(skb, &target, mtype);
}
/* 명시적 NAT 규칙이 없을 때의 null 바인딩 */
unsigned int
nf_nat_alloc_null_binding(struct nf_conn *ct,
unsigned int hooknum)
{
struct nf_nat_range2 range = {
.flags = NF_NAT_RANGE_MAP_IPS,
.min_addr = ct->tuplehash[IP_CT_DIR_REPLY]
.tuple.dst.u3,
.max_addr = ct->tuplehash[IP_CT_DIR_REPLY]
.tuple.dst.u3,
};
return nf_nat_setup_info(ct, &range,
HOOK2MANIP(hooknum));
}
코드 설명
- nf_nat_setup_info()연결의 첫 패킷에서 호출되어 NAT 바인딩을 설정합니다.
get_unique_tuple()은 지정된 범위 내에서 다른 연결과 충돌하지 않는 유일한 튜플을 선택합니다. 선택된 튜플이 원래와 다르면 conntrack의 reply 튜플을 변경하고IPS_SRC_NAT또는IPS_DST_NAT플래그를 설정합니다. - nf_nat_packet()연결의 모든 패킷에 대해 호출됩니다. 훅 번호로 SNAT/DNAT 유형을 결정하고(
HOOK2MANIP()),nf_nat_manip_pkt()로 실제 헤더 재작성을 수행합니다. conntrack이 기억하는 튜플 매핑을 참조하므로 첫 패킷 이후에는 별도의 규칙 평가 없이 자동 변환됩니다. - nf_nat_manip_pkt()conntrack 튜플에서 변환 대상 주소/포트를 추출하여 L3 프로토콜별 핸들러(
l3proto_manip_pkt())에 전달합니다. IPv4의 경우 IP 헤더 체크섬과 L4(TCP/UDP) 체크섬을 함께 갱신합니다. - nf_nat_alloc_null_binding()명시적 NAT 규칙과 매칭되지 않았지만 conntrack이 추적 중인 연결에 대해 호출됩니다. 원래 주소 그대로의 "null" 바인딩을 생성하여, 이후 동일 튜플의 새 연결이 NAT 포트 충돌을 일으키지 않도록 방지합니다.
NFQUEUE (유저스페이스 패킷 처리)
NFQUEUE는 커널에서 패킷을 유저스페이스로 전달하여 사용자 프로그램이 패킷을 검사하고 verdict(수락/폐기/수정)를 내릴 수 있게 합니다. IDS/IPS, DPI(Deep Packet Inspection), 커스텀 방화벽 구현에 사용됩니다.
# nftables에서 NFQUEUE로 패킷 전달
nft add rule inet my_filter input tcp dport 80 queue num 0
# 다수 큐 + CPU 바인딩 (로드 밸런싱)
nft add rule inet my_filter input tcp dport 80 queue num 0-3 fanout
# iptables에서 NFQUEUE
iptables -A INPUT -p tcp --dport 80 -j NFQUEUE --queue-num 0
iptables -A INPUT -p tcp --dport 80 -j NFQUEUE --queue-balance 0:3
/* libnetfilter_queue 기본 사용 패턴 */
#include <libnetfilter_queue/libnetfilter_queue.h>
static int cb(struct nfq_q_handle *qh,
struct nfgenmsg *nfmsg,
struct nfq_data *nfa, void *data)
{
struct nfqnl_msg_packet_hdr *ph = nfq_get_msg_packet_hdr(nfa);
u_int32_t id = ntohl(ph->packet_id);
unsigned char *payload;
int len = nfq_get_payload(nfa, &payload);
/* 패킷 분석 로직 ... */
/* verdict 판정: NF_ACCEPT 또는 NF_DROP */
return nfq_set_verdict(qh, id, NF_ACCEPT, 0, NULL);
}
int main(void)
{
struct nfq_handle *h = nfq_open();
nfq_unbind_pf(h, AF_INET);
nfq_bind_pf(h, AF_INET);
struct nfq_q_handle *qh = nfq_create_queue(h, 0, &cb, NULL);
nfq_set_mode(qh, NFQNL_COPY_PACKET, 0xffff);
int fd = nfq_fd(h);
char buf[4096];
while (1) {
int rv = recv(fd, buf, sizeof(buf), 0);
nfq_handle_packet(h, buf, rv);
}
}
nf_queue()와 nf_reinject() 내부
NFQUEUE verdict가 반환되면 커널은 nf_queue()를 통해 패킷을 유저스페이스 큐로 전달합니다. 유저스페이스가 verdict를 결정하면 nf_reinject()가 패킷을 원래 훅 체인의 중단 지점부터 재개합니다. 이 메커니즘은 비동기 패킷 처리의 핵심입니다.
/* net/netfilter/nf_queue.c — 패킷을 큐로 전달 */
static int
nf_queue(struct sk_buff *skb,
struct nf_hook_state *state,
unsigned int index,
unsigned int verdict)
{
struct nf_queue_entry *entry;
const struct nf_queue_handler *qh;
int ret;
qh = rcu_dereference(nf_queue_handler);
if (!qh)
return -ESRCH;
entry = kmalloc(sizeof(*entry), GFP_ATOMIC);
if (!entry)
return -ENOMEM;
/* 훅 체인 재개에 필요한 컨텍스트 보존 */
*entry = (struct nf_queue_entry) {
.skb = skb,
.state = *state,
.hook_index = index, /* 현재 훅 배열 인덱스 */
.size = sizeof(*entry),
};
nf_queue_entry_get_refs(entry);
/* queue 번호 추출 후 핸들러에 전달 */
ret = qh->outfn(entry,
nfqueue_num(verdict));
if (ret < 0) {
nf_queue_entry_free(entry);
return ret;
}
return 0;
}
/* 유저스페이스 verdict 후 패킷 재주입 */
void nf_reinject(struct nf_queue_entry *entry,
unsigned int verdict)
{
struct sk_buff *skb = entry->skb;
const struct nf_hook_state *state = &entry->state;
const struct nf_hook_entries *hooks;
hooks = rcu_dereference(
state->net->nf.hooks_ipv4[state->hook]);
switch (verdict & NF_VERDICT_MASK) {
case NF_ACCEPT:
case NF_STOP:
/* 중단 지점의 다음 훅부터 재개 */
verdict = nf_hook_slow(skb,
(struct nf_hook_state *)state,
hooks, entry->hook_index + 1);
break;
case NF_DROP:
kfree_skb(skb);
break;
case NF_REPEAT:
/* 동일 훅부터 재실행 */
verdict = nf_hook_slow(skb,
(struct nf_hook_state *)state,
hooks, entry->hook_index);
break;
}
if (verdict == NF_ACCEPT)
state->okfn(state->net, state->sk, skb);
nf_queue_entry_free(entry);
}
코드 설명
- nf_queue()
nf_hook_slow()에서 NF_QUEUE verdict를 받으면 호출됩니다.nf_queue_entry에 skb, 훅 상태, 현재 훅 인덱스를 보존하여, 나중에 정확한 위치에서 훅 체인을 재개할 수 있게 합니다.qh->outfn()은nfnetlink_queue모듈이 등록한 함수로, netlink 소켓을 통해 유저스페이스로 패킷을 전송합니다. - nf_queue_entry_get_refs()entry에 참조된 네트워크 장치(
in/out), 소켓, conntrack 등의 참조 카운트를 증가시킵니다. 유저스페이스 처리 중에 이들 객체가 해제되는 것을 방지합니다. - nf_reinject()유저스페이스의 verdict에 따라 패킷 처리를 재개합니다. NF_ACCEPT인 경우
entry->hook_index + 1부터nf_hook_slow()를 재호출하여 나머지 훅을 순회합니다. NF_REPEAT는 같은 훅을 다시 실행하며, 모든 훅을 통과하면 원래의okfn()이 호출됩니다.
NFLOG (패킷 로깅)
NFLOG는 패킷을 유저스페이스의 로깅 데몬(ulogd2)으로 전달합니다. 커널 내 LOG 타겟과 달리 syslog를 거치지 않으므로 고성능 로깅이 가능합니다.
| 항목 | LOG 타겟 | NFLOG 타겟 |
|---|---|---|
| 출력 경로 | 커널 printk → syslog | netlink → ulogd2 (유저스페이스) |
| 성능 | syslog 병목(Bottleneck) | 고성능 (비동기) |
| 유연성 | syslog 형식 고정 | DB, pcap, 파일 등 다양한 출력 |
| 그룹 | 미지원 | 그룹 번호로 분류 |
| 속도 제한 | --limit 필요 | qthreshold, 타이머(Timer) 기반 |
# nftables에서 NFLOG 사용
nft add rule inet my_filter input tcp dport 22 \
log prefix "SSH: " group 1 queue-threshold 10
nft add rule inet my_filter forward ct state invalid \
log prefix "INVALID: " group 2 drop
# ulogd2 설정 (/etc/ulogd.conf)
# [global]
# logfile="/var/log/ulogd.log"
# stack=log1:NFLOG,base1:BASE,pcap1:PCAP
# [log1]
# group=1
# [pcap1]
# file="/var/log/nflog.pcap"
# sync=1
Netfilter Netlink (nfnetlink)
nfnetlink은 Netfilter 서브시스템의 커널-유저스페이스 통신 채널입니다. netlink 프로토콜 패밀리 NETLINK_NETFILTER를 사용하며, 각 서브시스템이 별도의 메시지 타입을 등록합니다.
| 서브시스템 상수 | 용도 |
|---|---|
NFNL_SUBSYS_CTNETLINK | conntrack 항목 조회/생성/삭제/이벤트 |
NFNL_SUBSYS_CTNETLINK_EXP | conntrack expect (helper 관련) |
NFNL_SUBSYS_QUEUE | NFQUEUE 패킷 전달/verdict |
NFNL_SUBSYS_ULOG | NFLOG 패킷 로그 |
NFNL_SUBSYS_NFTABLES | nftables 규칙 관리 (트랜잭션 지원) |
NFNL_SUBSYS_CTNETLINK_TIMEOUT | conntrack 타임아웃 정책 |
NFNL_SUBSYS_CTTIMEOUT | conntrack 타임아웃 오브젝트 |
NFNL_SUBSYS_IPSET | ipset 관리 |
nftables의 규칙 업데이트는 nfnetlink 트랜잭션으로 처리됩니다. 여러 규칙 변경을 하나의 원자적 배치로 커밋할 수 있어, 규칙 교체 중 패킷이 누락되지 않습니다.
# nft의 원자적 규칙 교체 (-f 파일 로드)
nft -f /etc/nftables.conf
# 전체 ruleset을 원자적으로 교체
nft flush ruleset
nft -f /etc/nftables.conf
테이블/체인 아키텍처 상세
패킷이 각 훅 포인트를 통과할 때, 등록된 테이블들이 우선순위 순서로 처리됩니다. 전체 패킷 통과 순서를 이해하는 것이 방화벽 규칙 설계의 핵심입니다.
PREROUTING 훅의 테이블 처리 순서
| 단계 | 체인/우선순위 | 주요 처리 |
|---|---|---|
| 1 | PREROUTING, -450 | raw: NOTRACK 결정 |
| 2 | PREROUTING, -400 | conntrack defrag: IP 조각 재조립 |
| 3 | PREROUTING, -200 | conntrack: 연결 상태 추적 시작 |
| 4 | PREROUTING, -150 | mangle: MARK/TOS 수정 |
| 5 | PREROUTING, -100 | DNAT: 목적지 주소 변환(Address Translation) |
| 6 | 라우팅 결정 | INPUT(로컬) 또는 FORWARD(포워딩) 분기 |
| 7 | INPUT/FORWARD | mangle → filter → security |
| 8 | POSTROUTING | mangle → SNAT/MASQUERADE → conntrack confirm |
raw 테이블
raw 테이블은 conntrack 이전에 실행되어, 특정 패킷의 연결 추적을 건너뛸 수 있습니다. 대량 트래픽에서 conntrack 오버헤드(Overhead)를 줄이는 데 유용합니다.
# nftables에서 NOTRACK 설정
nft add table inet raw
nft add chain inet raw prerouting \
'{ type filter hook prerouting priority -300; }'
nft add rule inet raw prerouting tcp dport 8080 notrack
nft add rule inet raw prerouting udp dport 53 notrack
mangle 테이블
# 패킷 마킹 (정책 라우팅, QoS 연동)
nft add rule inet mangle_tbl forward ip saddr 10.0.1.0/24 \
meta mark set 0x1
nft add rule inet mangle_tbl forward ip saddr 10.0.2.0/24 \
meta mark set 0x2
# TTL 수정
nft add rule inet mangle_tbl forward ip ttl set 64
# DSCP/TOS 수정
nft add rule inet mangle_tbl forward tcp dport 22 \
ip dscp set cs4
Netfilter 성능 최적화
규칙 최적화 전략
핵심 원칙: 선형 규칙 리스트(O(n)) 대신 set/map(O(1))을 사용하세요. 1,000개의 IP 차단 규칙보다 하나의 set + lookup이 훨씬 빠릅니다.
# 나쁜 예: 선형 규칙 (O(n))
nft add rule inet filter input ip saddr 10.0.0.1 drop
nft add rule inet filter input ip saddr 10.0.0.2 drop
# ... 수백 개 반복
# 좋은 예: set 활용 (O(1) 해시 조회)
nft add set inet filter blocklist '{ type ipv4_addr; flags interval; }'
nft add element inet filter blocklist '{ 10.0.0.0/24, 192.168.1.0/24 }'
nft add rule inet filter input ip saddr @blocklist drop
# verdict map으로 다중 분기 최적화
nft add map inet filter svc_map '{ type inet_service : verdict; }'
nft add element inet filter svc_map \
'{ 22 : accept, 80 : accept, 443 : accept }'
nft add rule inet filter input tcp dport vmap @svc_map
conntrack 튜닝
# 해시 테이블 크기 (버킷 수, 2의 거듭제곱 권장)
# 메모리: 약 buckets × 16 bytes
sysctl -w net.netfilter.nf_conntrack_buckets=131072
# 최대 추적 연결 수 (buckets × 4 ~ buckets × 8 권장)
sysctl -w net.netfilter.nf_conntrack_max=524288
# TCP established 타임아웃 단축 (서버 환경)
sysctl -w net.netfilter.nf_conntrack_tcp_timeout_established=86400
# 불필요한 트래픽 NOTRACK (conntrack 부하 감소)
nft add rule inet raw prerouting udp dport 53 notrack
nft add rule inet raw output udp sport 53 notrack
Flowtable Fast Path
Flowtable은 이미 확립된(ESTABLISHED) 연결의 패킷을 Netfilter 훅 체인을 우회하여 직접 전달합니다. 포워딩 성능을 크게 향상시킵니다. 상세 내용은 NAT 페이지(Page)를 참조하세요.
# flowtable 설정
nft add flowtable inet filter ft \
'{ hook ingress priority 0; devices = { eth0, eth1 }; }'
# established 연결을 flowtable으로 오프로드
nft add rule inet filter forward ct state established \
flow add @ft
nft add rule inet filter forward ct state related accept
nft add rule inet filter forward ct state new accept
Flowtable 내부 구조
Flowtable은 conntrack이 확립(ESTABLISHED)된 연결의 패킷을 전체 Netfilter 훅 체인을 우회하여 빠르게 전달(forwarding)하는 메커니즘입니다. ingress 훅에서 해시 테이블 조회만으로 출력 장치를 결정하여, conntrack/NAT/nftables 규칙 평가를 모두 건너뜁니다. 하드웨어 오프로드 시 NIC가 직접 포워딩하여 CPU 개입을 최소화합니다.
/* include/net/netfilter/nf_flow_table.h */
struct nf_flowtable {
struct list_head list;
struct rhashtable rhashtable; /* 플로우 해시 */
int priority;
struct nf_flowtable_type *type;
struct delayed_work gc_work; /* GC 워커 */
unsigned int flags;
/* NF_FLOWTABLE_HW_OFFLOAD 등 */
struct flow_block flow_block;
possible_net_t net;
};
/* 개별 플로우 엔트리 */
struct flow_offload {
struct flow_offload_tuple_rhash tuplehash[2];
struct nf_conn *ct;
unsigned long flags;
/* FLOW_OFFLOAD_SNAT, FLOW_OFFLOAD_DNAT,
* FLOW_OFFLOAD_DYING, FLOW_OFFLOAD_HW */
u16 type;
u32 timeout;
};
/* 플로우 방향별 튜플 */
struct flow_offload_tuple {
union {
struct in_addr src_v4;
struct in6_addr src_v6;
};
union {
struct in_addr dst_v4;
struct in6_addr dst_v6;
};
struct {
__be16 src_port;
__be16 dst_port;
};
u8 l3proto;
u8 l4proto;
struct dst_entry *dst_cache;
struct net_device *iifn; /* 입력 장치 */
};
/* net/netfilter/nf_flow_table_ip.c — fast path 처리 */
unsigned int
nf_flow_offload_ip_hook(void *priv,
struct sk_buff *skb,
const struct nf_hook_state *state)
{
struct flow_offload_tuple_rhash *tuplehash;
struct nf_flowtable *flow_table = priv;
struct flow_offload_tuple tuple = {};
struct flow_offload *flow;
enum flow_offload_tuple_dir dir;
/* 패킷에서 5-tuple 추출 */
if (nf_flow_tuple_ip(skb, state->in, &tuple) < 0)
return NF_ACCEPT;
/* rhashtable에서 플로우 조회 */
tuplehash = flow_offload_lookup(flow_table, &tuple);
if (!tuplehash)
return NF_ACCEPT; /* slow path 진행 */
flow = container_of(tuplehash, struct flow_offload,
tuplehash[dir]);
/* NAT 변환 (필요한 경우) */
if (flow->flags & FLOW_OFFLOAD_SNAT)
nf_flow_snat_ip(state, skb, flow, dir);
if (flow->flags & FLOW_OFFLOAD_DNAT)
nf_flow_dnat_ip(state, skb, flow, dir);
/* TTL 감소 + dst_cache로 직접 전송 */
ip_decrease_ttl(ip_hdr(skb));
skb->dev = flow_dst(tuplehash)->dev;
neigh_xmit(NEIGH_ARP_TABLE, skb->dev,
&flow_dst(tuplehash)->addr, skb);
return NF_STOLEN; /* 패킷 전송 완료 */
}
코드 설명
- nf_flowtableflowtable의 코어 구조체입니다.
rhashtable에 활성 플로우가 저장되며,gc_work가 주기적으로 만료된 플로우를 정리합니다.NF_FLOWTABLE_HW_OFFLOAD플래그가 설정되면flow_block을 통해 NIC 드라이버에 하드웨어 오프로드를 요청합니다. - flow_offloadconntrack과 유사하게 양방향(ORIGINAL/REPLY) 튜플을 보유합니다.
ct포인터로 원본 conntrack 엔트리를 참조하며, NAT 플래그가 설정되어 있으면 fast path에서도 NAT 변환을 수행합니다. - flow_offload_tuple — dst_cache라우팅 조회 결과를 캐시합니다. 일반 경로에서는 패킷마다
ip_route_output()을 호출하지만, flowtable은 캐시된dst_entry로 바로 전송하여 라우팅 테이블 조회를 건너뜁니다. - nf_flow_offload_ip_hook()ingress 훅(priority -1)에 등록되어 가장 먼저 실행됩니다. rhashtable 조회로 플로우를 찾으면 NAT 변환, TTL 감소, 이웃(ARP) 캐시를 통한 직접 전송만 수행하고 NF_STOLEN을 반환합니다. 전체 Netfilter 스택(conntrack, nftables, mangle 등)을 완전히 우회하므로 극적인 성능 향상이 가능합니다.
XDP vs Netfilter 트레이드오프
| 항목 | Netfilter | XDP |
|---|---|---|
| 실행 위치 | 네트워크 스택 내부 (L3/L4) | 드라이버 수준 (L2 이전) |
| 성능 | ~수 Mpps | ~24+ Mpps (native) |
| 기능 | conntrack, NAT, 완전한 상태 추적 | 상태 없음, BPF map으로 구현 |
| 프로토콜 접근 | 파싱 완료된 sk_buff | raw 패킷 데이터 직접 파싱 |
| 적합 용도 | 일반 방화벽, NAT, stateful 필터링 | DDoS 방어, 고속 패킷 드롭/리다이렉트 |
nftables 하드웨어 오프로드
# flowtable 하드웨어 오프로드 (SmartNIC 필요)
nft add flowtable inet filter ft_hw \
'{ hook ingress priority 0; devices = { eth0 }; flags offload; }'
# 오프로드 상태 확인
nft list flowtable inet filter ft_hw
# table inet filter {
# flowtable ft_hw {
# hook ingress priority filter
# devices = { eth0 }
# flags offload
# }
# }
네트워크 네임스페이스(Namespace)와 Netfilter
각 네트워크 네임스페이스(netns)는 독립적인 Netfilter 규칙셋과 conntrack 테이블을 갖습니다. 컨테이너 환경에서 이는 격리와 동시에 복잡한 패킷 흐름 문제를 야기합니다.
Per-netns conntrack 테이블
# 네임스페이스별 독립적인 conntrack
ip netns exec ns1 conntrack -L # ns1의 conntrack 테이블
ip netns exec ns2 conntrack -L # ns2의 conntrack 테이블
# 각 네임스페이스에서 독립적인 방화벽 규칙
ip netns exec ns1 nft list ruleset
ip netns exec ns2 nft list ruleset
브릿지와 네임스페이스 간 패킷 흐름
컨테이너 환경 주의사항: Docker/Kubernetes는 호스트 netns에 iptables/nftables 규칙을 추가합니다. br_netfilter 모듈이 로드되면 브릿지된 L2 트래픽도 iptables 규칙을 통과하여 예상치 못한 패킷 드롭이 발생할 수 있습니다. sysctl net.bridge.bridge-nf-call-iptables=0으로 비활성화할 수 있으나 컨테이너 네트워킹에 영향을 줄 수 있습니다.
# veth 쌍을 통한 네임스페이스 간 통신에서 Netfilter 경로
# Host netns:
# veth-host → PREROUTING → 라우팅 → FORWARD → POSTROUTING → veth-ns
# Container netns:
# eth0(=veth-ns) → PREROUTING → INPUT → 로컬 프로세스
# br_netfilter 관련 설정
lsmod | grep br_netfilter
sysctl net.bridge.bridge-nf-call-iptables
sysctl net.bridge.bridge-nf-call-ip6tables
sysctl net.bridge.bridge-nf-call-arptables
Netfilter 디버깅
nftables trace
# 특정 패킷에 대한 nftables 규칙 추적
# 1단계: 추적 대상 마킹
nft add rule inet raw prerouting ip saddr 10.0.0.1 meta nftrace set 1
# 2단계: trace 모니터링
nft monitor trace
# trace id abcd1234 inet raw prerouting
# packet: iif "eth0" ip saddr 10.0.0.1 ip daddr 192.168.1.1 ...
# rule: ip saddr 10.0.0.1 meta nftrace set 1 (verdict continue)
# trace id abcd1234 inet filter input
# rule: tcp dport 22 accept (verdict accept)
conntrack 디버깅
# 실시간 conntrack 이벤트 모니터링
conntrack -E
conntrack -E -e NEW,DESTROY
# 특정 연결 상세 조회
conntrack -L -s 10.0.0.1 -d 93.184.216.34 -p tcp
conntrack -L --status ASSURED
# conntrack 통계
conntrack -S
# cpu=0 found=... invalid=... insert=... insert_failed=... drop=...
# ↑ drop/insert_failed 증가 시 nf_conntrack_max 초과 의심
# /proc 인터페이스
cat /proc/net/nf_conntrack # 전체 conntrack 테이블
cat /proc/sys/net/netfilter/nf_conntrack_count # 현재 엔트리 수
cat /proc/sys/net/netfilter/nf_conntrack_max # 최대 엔트리 수
iptables LOG / NFLOG 활용
# nftables log로 패킷 내용 기록
nft add rule inet filter input tcp dport 80 \
log prefix "HTTP_IN: " level info
nft add rule inet filter forward ct state invalid \
log prefix "INVALID_FWD: " group 3 drop
# 로그 확인
dmesg | grep "HTTP_IN:"
journalctl -k | grep "INVALID_FWD:"
BPF 기반 Netfilter 추적
# bpftrace로 nf_hook_slow 추적
bpftrace -e 'kprobe:nf_hook_slow {
printf("hook=%d pf=%d in=%s\n",
arg2, arg1, str(((struct net_device *)arg4)->name));
}'
# nft 규칙 평가 추적
bpftrace -e 'kprobe:nft_do_chain {
printf("chain eval: %s\n", str(((struct nft_chain *)arg1)->name));
}'
# conntrack 새 연결 추적
bpftrace -e 'kprobe:__nf_conntrack_confirm {
printf("conntrack confirm: skb=%p\n", arg0);
}'
주요 /proc, /sys 파일
| 경로 | 설명 |
|---|---|
/proc/net/nf_conntrack | conntrack 엔트리 전체 목록 |
/proc/sys/net/netfilter/nf_conntrack_max | 최대 연결 추적 수 |
/proc/sys/net/netfilter/nf_conntrack_count | 현재 추적 중인 연결 수 |
/proc/sys/net/netfilter/nf_conntrack_buckets | 해시 테이블 버킷 수 |
/proc/sys/net/netfilter/nf_conntrack_tcp_* | TCP 프로토콜 타임아웃 |
/proc/sys/net/netfilter/nf_conntrack_udp_* | UDP 프로토콜 타임아웃 |
/proc/net/stat/nf_conntrack | per-CPU conntrack 통계 |
/proc/sys/net/bridge/bridge-nf-call-iptables | 브릿지 트래픽의 iptables 통과 여부 |
Netfilter 커널 모듈 작성
커스텀 훅 모듈 예제
/* nf_example.c — 커스텀 Netfilter 훅 모듈 */
#include <linux/module.h>
#include <linux/netfilter.h>
#include <linux/netfilter_ipv4.h>
#include <linux/ip.h>
#include <linux/tcp.h>
MODULE_LICENSE("GPL");
MODULE_AUTHOR("Example");
MODULE_DESCRIPTION("Netfilter hook example");
static unsigned int my_hook_fn(
void *priv,
struct sk_buff *skb,
const struct nf_hook_state *state)
{
struct iphdr *iph;
struct tcphdr *tcph;
if (!skb)
return NF_ACCEPT;
iph = ip_hdr(skb);
if (iph->protocol != IPPROTO_TCP)
return NF_ACCEPT;
tcph = tcp_hdr(skb);
/* 포트 8888로의 SYN 패킷 차단 */
if (ntohs(tcph->dest) == 8888 && tcph->syn && !tcph->ack) {
pr_info("Blocked SYN to port 8888 from %pI4\\n",
&iph->saddr);
return NF_DROP;
}
return NF_ACCEPT;
}
static struct nf_hook_ops my_nfho = {
.hook = my_hook_fn,
.pf = NFPROTO_IPV4,
.hooknum = NF_INET_PRE_ROUTING,
.priority = NF_IP_PRI_FIRST,
};
static int __init my_init(void)
{
int ret = nf_register_net_hook(&init_net, &my_nfho);
if (ret < 0) {
pr_err("nf_register_net_hook failed: %d\\n", ret);
return ret;
}
pr_info("Netfilter hook registered\\n");
return 0;
}
static void __exit my_exit(void)
{
nf_unregister_net_hook(&init_net, &my_nfho);
pr_info("Netfilter hook unregistered\\n");
}
module_init(my_init);
module_exit(my_exit);
코드 설명
- my_hook_fn()
nf_hookfn시그니처를 따르는 훅 콜백 함수입니다.priv는nf_hook_ops.priv에 설정한 값이고,skb는 현재 패킷,state는 훅 컨텍스트(입출력 디바이스, 네트워크 네임스페이스 등)입니다. 반환값NF_ACCEPT/NF_DROP이 패킷의 운명을 결정합니다. - ip_hdr()/tcp_hdr()
sk_buff에서 IP/TCP 헤더 포인터를 추출하는 인라인 함수입니다. 선형화(linearize)되지 않은skb에서는 헤더가 분산될 수 있으므로, 운영 코드에서는skb_header_pointer()를 사용하는 것이 안전합니다. 이 예제는 간결함을 위해 직접 캐스팅합니다. - nf_hook_ops 초기화
.pf = NFPROTO_IPV4로 IPv4 패킷에만,.hooknum = NF_INET_PRE_ROUTING으로 라우팅 전에 실행됩니다..priority = NF_IP_PRI_FIRST(-2147483648)는 다른 모든 훅(conntrack, nftables 등)보다 먼저 실행되며, 운영 환경에서는 conntrack 이후에 실행되도록NF_IP_PRI_FILTER(0) 이상을 권장합니다. - nf_register_net_hook(&init_net, ...)초기 네트워크 네임스페이스(
init_net)에 훅을 등록합니다. 컨테이너 환경에서 모든 netns에 적용하려면register_pernet_subsys()로 netns 콜백을 등록하고, 각 netns 생성 시nf_register_net_hook()을 호출해야 합니다. - __init/__exit커널 모듈의 초기화/정리 함수입니다.
__init섹션은 모듈 로드 후 메모리에서 해제되고,__exit는 모듈 언로드 시 호출됩니다.nf_unregister_net_hook()을 반드시__exit에서 호출하여 훅을 해제해야 합니다.
nftables 커스텀 표현식 모듈
/* nft_example_expr.c — nftables 커스텀 표현식 모듈 (골격) */
#include <linux/module.h>
#include <net/netfilter/nf_tables.h>
struct nft_example_priv {
u32 threshold;
atomic_t counter;
};
static void nft_example_eval(
const struct nft_expr *expr,
struct nft_regs *regs,
const struct nft_pktinfo *pkt)
{
struct nft_example_priv *priv = nft_expr_priv(expr);
if (atomic_inc_return(&priv->counter) > priv->threshold) {
/* 임계값 초과 시 DROP verdict */
regs->verdict.code = NF_DROP;
}
}
static const struct nft_expr_ops nft_example_ops = {
.eval = nft_example_eval,
.size = NFT_EXPR_SIZE(sizeof(struct nft_example_priv)),
/* .init, .destroy, .dump 등 구현 필요 */
};
static struct nft_expr_type nft_example_type = {
.name = "example",
.ops = &nft_example_ops,
.owner = THIS_MODULE,
};
static int __init nft_example_init(void)
{
return nft_register_expr(&nft_example_type);
}
static void __exit nft_example_exit(void)
{
nft_unregister_expr(&nft_example_type);
}
module_init(nft_example_init);
module_exit(nft_example_exit);
MODULE_LICENSE("GPL");
conntrack helper 모듈 작성
/* nf_conntrack_myproto.c — 커스텀 conntrack helper 골격 */
#include <linux/module.h>
#include <net/netfilter/nf_conntrack.h>
#include <net/netfilter/nf_conntrack_helper.h>
#include <net/netfilter/nf_conntrack_expect.h>
#define MYPROTO_PORT 9999
static int myproto_help(
struct sk_buff *skb,
unsigned int protoff,
struct nf_conn *ct,
enum ip_conntrack_info ctinfo)
{
/* 페이로드에서 데이터 포트 추출 */
u16 data_port;
/* ... 프로토콜 파싱 ... */
/* 관련 연결에 대한 expectation 생성 */
struct nf_conntrack_expect *exp;
exp = nf_ct_expect_alloc(ct);
if (!exp)
return NF_ACCEPT;
nf_ct_expect_init(exp, NF_CT_EXPECT_CLASS_DEFAULT,
nf_ct_l3num(ct),
&ct->tuplehash[IP_CT_DIR_ORIGINAL].tuple.src.u3,
&ct->tuplehash[IP_CT_DIR_ORIGINAL].tuple.dst.u3,
IPPROTO_TCP, NULL, &data_port);
nf_ct_expect_related(exp);
nf_ct_expect_put(exp);
return NF_ACCEPT;
}
static struct nf_conntrack_expect_policy myproto_exp_policy = {
.max_expected = 1,
.timeout = 5 * 60,
};
static struct nf_conntrack_helper myproto_helper = {
.name = "myproto",
.me = THIS_MODULE,
.help = myproto_help,
.expect_policy = &myproto_exp_policy,
.tuple.src.l3num = NFPROTO_IPV4,
.tuple.src.u.tcp.port = cpu_to_be16(MYPROTO_PORT),
.tuple.dst.protonum = IPPROTO_TCP,
};
static int __init myproto_init(void)
{
return nf_conntrack_helper_register(&myproto_helper);
}
static void __exit myproto_exit(void)
{
nf_conntrack_helper_unregister(&myproto_helper);
}
module_init(myproto_init);
module_exit(myproto_exit);
MODULE_LICENSE("GPL");
Netfilter 주요 보안 취약점(Vulnerability) 사례
Netfilter/nf_tables는 커널 네트워크 스택의 핵심 구성 요소로, 권한 상승 취약점의 주요 공격 대상이 되어왔습니다. 특히 nf_tables의 복잡한 규칙 관리와 트랜잭션 처리 과정에서 메모리 안전성 문제가 반복적으로 발견되고 있습니다.
nf_tables Use-After-Free (CVE-2024-1086)
nft_verdict_init()에서 NF_DROP verdict에 양수 errno 값을 허용하여, nf_hook_slow()가 Drop 대신 Accept로 처리합니다. 이미 해제 예정인 skb가 Accept 경로로 계속 진행하면서 이중 해제(double-free)가 발생합니다. Linux 3.15~6.8에 영향을 미치며, 비특권 user namespace에서 nftables 규칙 생성 권한만으로 트리거 가능합니다.
/* CVE-2024-1086 공격 흐름 */
/* 1. nft_verdict_init()에서 NF_DROP + 양수 errno 설정 */
/* NF_DROP(0) | (NF_ACCEPT << 16) → nf_hook_slow()에서 NF_ACCEPT로 해석 */
/* net/netfilter/nf_tables_api.c — 취약 코드 */
switch (data->verdict.code) {
case NF_DROP:
/* BUG: NF_VERDICT_MASK 비트에 양수 값(NF_ACCEPT)을 허용
* → nf_hook_slow()가 verdict를 NF_ACCEPT로 오인 */
break;
}
/* net/netfilter/core.c — nf_hook_slow() */
int nf_hook_slow(...) {
verdict = nf_hook_entry_hookfn(&state, e);
switch (verdict & NF_VERDICT_MASK) {
case NF_ACCEPT:
/* skb가 이미 NF_DROP으로 처리(해제 예정)되었지만
* Accept 경로로 진행 → 이후 다시 해제 시도 → UAF/double-free */
break;
}
}
/* 수정: NF_DROP의 errno를 음수 값만 허용 (commit f342de4...) */
case NF_DROP:
if (data->verdict.code & NF_VERDICT_MASK)
return -EINVAL; /* 양수 errno 거부 */
break;
nf_tables Batch Request UAF (CVE-2023-32233)
nf_tables의 batch 요청 처리에서 익명(anonymous) set을 같은 batch 내에서 생성하고 삭제할 때, 트랜잭션 커밋 과정에서 이미 해제된 set에 대한 참조가 남아 Use-After-Free가 발생합니다. Linux 3.18~6.3에 영향을 미칩니다.
- CVE-2023-32233: 익명 set 생명주기 결함 */
- nf_tables batch 처리에서의 경쟁:
- NFT_MSG_NEWSET (익명 set 생성) — set이 트랜잭션 목록에 추가
- NFT_MSG_DELSET (같은 batch에서 삭제) — set이 삭제 표시
- 커밋 시: 삭제가 먼저 처리되어 set 해제
- 생성 측에서 해제된 set을 규칙에 바인딩 시도 → UAF
- 수정: 익명 set의 batch 내 생명주기 추적을 엄격하게 관리
- 같은 batch에서 생성/삭제 시 양쪽 모두 no-op으로 처리
Conntrack 관련 버그 패턴
conntrack은 멀티코어 환경에서 해시 테이블을 동시에 접근하므로, 경쟁 조건에 취약합니다:
RCU 보호 누락: conntrack 엔트리 순회 중 다른 CPU에서 삭제 시 UAF 발생. nf_conntrack_find_get()에서 RCU read lock과 refcount 확인이 원자적이지 않으면 해제된 엔트리에 접근 가능
Helper 등록/해제 경쟁: conntrack helper 모듈 언로드 시 활성 연결의 helper 참조가 무효화(Invalidation). nf_conntrack_helper_unregister()에서 모든 활성 conntrack을 순회하여 helper 참조를 제거해야 함
해시 테이블 리사이즈: nf_conntrack_max 변경 시 해시 테이블 교체 과정에서의 동시 접근 → RCU 기반 교체로 완화
/* conntrack 해시 테이블 경쟁 조건 방어 패턴 */
/* 안전한 conntrack 순회 — RCU read lock 필수 */
rcu_read_lock();
for (i = 0; i < nf_conntrack_htable_size; i++) {
struct nf_conntrack_tuple_hash *h;
struct hlist_nulls_node *n;
hlist_nulls_for_each_entry_rcu(h, n,
&nf_conntrack_hash[i], hnnode) {
struct nf_conn *ct = nf_ct_tuplehash_to_ctrack(h);
/* refcount를 먼저 증가시켜 해제 방지 */
if (!refcount_inc_not_zero(&ct->ct_general.use))
continue; /* 이미 해제 중 — 건너뜀 */
/* 안전하게 ct 사용 */
...
nf_ct_put(ct); /* refcount 감소 */
}
}
rcu_read_unlock();
/* conntrack 최대 항목 수 조정 시 주의사항 */
/* nf_conntrack_max 변경은 해시 테이블 리사이즈를 유발
* RCU grace period 대기 후 이전 테이블 해제 */
sysctl -w net.netfilter.nf_conntrack_max=262144
1. nf_tables 권한 제한: net.core.bpf_jit_harden=2 및 user namespace 제한으로 비특권 사용자의 nf_tables 접근 차단
2. conntrack 모니터링: conntrack -E로 실시간(Real-time) 이벤트 감시, 비정상적 conntrack 생성 패턴 탐지
3. 커널 업데이트: nf_tables 취약점은 2023~2024년에 집중 발견되었으며, 6.8 이후 커널에서 주요 수정이 적용됨
4. seccomp 프로파일: 컨테이너 환경에서 NFT_* 관련 시스템 콜(System Call)을 차단하여 공격 면적 축소
ipset과 nftables Sets/Maps
대규모 방화벽 규칙셋에서 개별 규칙을 반복 나열하면 O(n) 선형 탐색이 발생합니다. ipset(레거시)과 nftables sets/maps는 해시 또는 트리 기반 자료구조로 O(1)~O(log n) 조회를 제공하여, 수만 개 IP/포트를 효율적으로 관리합니다.
ipset 개요와 내부 구조
ipset은 iptables와 함께 사용하는 커널 모듈(ip_set)로, 다양한 데이터 타입의 집합을 커널 메모리에 유지합니다. iptables 규칙에서 -m set --match-set으로 참조합니다.
/* include/linux/netfilter/ipset/ip_set.h */
struct ip_set {
char name[IPSET_MAXNAMELEN];
spinlock_t lock;
u32 ref; /* 참조 카운트 */
u32 ref_netlink;
struct ip_set_type_variant *variant; /* hash/bitmap/list 구현 */
struct ip_set_type *type;
u8 family; /* NFPROTO_IPV4/IPV6 */
u8 revision;
void *data; /* 타입별 데이터 */
struct net *net;
struct list_head list;
size_t ext_size;
unsigned long timeout; /* 기본 타임아웃 */
};
/* ipset 타입별 연산 인터페이스 */
struct ip_set_type_variant {
int (*kadt)(struct ip_set *set, const struct sk_buff *skb,
const struct xt_action_param *par,
enum ipset_adt adt, struct ip_set_adt_opt *opt);
int (*uadt)(struct ip_set *set, struct nlattr *tb[],
enum ipset_adt adt, u32 *lineno, u32 flags, bool retried);
bool (*test)(struct ip_set *set, void *value,
const struct ip_set_ext *ext, struct ip_set_ext *mext,
u32 flags);
};
| ipset 타입 | 저장 구조 | 용도 | 조회 복잡도 |
|---|---|---|---|
hash:ip | 해시 테이블 | IP 주소 집합 | O(1) |
hash:net | 해시 + CIDR | 서브넷 집합 | O(1) |
hash:ip,port | 해시 테이블 | IP+포트 조합 | O(1) |
hash:ip,port,net | 해시 테이블 | 소스IP+포트+목적지넷 | O(1) |
hash:net,iface | 해시 테이블 | 서브넷+인터페이스 | O(1) |
bitmap:ip | 비트맵(Bitmap) | 연속 IP 범위 (/16 이하) | O(1) |
bitmap:port | 비트맵 | 포트 범위 | O(1) |
list:set | 링크드 리스트 | set의 set (메타 세트) | O(n) |
# ipset 생성 및 사용 예제
# IP 차단 리스트 생성 (자동 만료 포함)
ipset create blacklist hash:ip hashsize 65536 maxelem 1000000 timeout 3600
# 엔트리 추가
ipset add blacklist 192.168.1.100
ipset add blacklist 10.0.0.0/8 timeout 7200 # 개별 타임아웃
# iptables에서 참조
iptables -A INPUT -m set --match-set blacklist src -j DROP
# IP+포트 조합 set
ipset create allowed_services hash:ip,port
ipset add allowed_services 10.0.1.0/24,tcp:80
ipset add allowed_services 10.0.1.0/24,tcp:443
iptables -A INPUT -m set --match-set allowed_services src,dst -j ACCEPT
# 카운터 및 코멘트 확장
ipset create monitored hash:ip counters comment
ipset add monitored 10.0.0.1 comment "web-server"
ipset list monitored # 패킷/바이트 카운터 포함 출력
# set 저장/복원 (재부팅 시 보존)
ipset save > /etc/ipset.conf
ipset restore < /etc/ipset.conf
nftables Sets
nftables는 set을 규칙셋의 일급 객체로 취급합니다. ipset과 달리 별도 도구 없이 nft 명령어로 직접 관리하며, 다양한 데이터 타입과 정책을 지원합니다.
/* net/netfilter/nf_tables_api.c — nft set 내부 구조 */
struct nft_set {
struct list_head list;
struct list_head bindings; /* 이 set을 참조하는 규칙들 */
struct nft_table *table;
const struct nft_set_ops *ops; /* hash/rbtree/bitmap 구현 */
u16 flags; /* NFT_SET_* 플래그 */
u64 timeout; /* 기본 타임아웃 (ns) */
u32 gc_int; /* GC 주기 (ms) */
u32 size; /* 최대 엘리먼트 수 */
char *name;
/* ... */
};
/* set 구현체 선택 — 플래그에 따라 자동 결정 */
/* NFT_SET_INTERVAL → rbtree, 그 외 → hash
* NFT_SET_CONCAT → pipapo (Pile Packet Policies) 알고리즘 */
# === 명명된(named) set ===
# IPv4 주소 set (자동 GC 타임아웃)
nft add set inet filter blacklist \
'{ type ipv4_addr; flags timeout; timeout 1h; gc-interval 5m; size 100000; }'
# 엘리먼트 추가 (개별 타임아웃 가능)
nft add element inet filter blacklist '{ 10.0.0.1 timeout 30m, 10.0.0.2 }'
# 규칙에서 참조
nft add rule inet filter input ip saddr @blacklist counter drop
# === 인터벌 set (CIDR/범위 지원) ===
nft add set inet filter internal_nets \
'{ type ipv4_addr; flags interval; }'
nft add element inet filter internal_nets \
'{ 10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16 }'
# === 연결(concatenation) set ===
# IP + 포트 조합 매칭 (pipapo 알고리즘 사용)
nft add set inet filter svc_whitelist \
'{ type ipv4_addr . inet_service; flags interval; }'
nft add element inet filter svc_whitelist \
'{ 10.0.0.0/24 . 80, 10.0.0.0/24 . 443, 172.16.0.0/16 . 22 }'
nft add rule inet filter input ip saddr . tcp dport @svc_whitelist accept
# === 익명(anonymous) set — 인라인 ===
nft add rule inet filter input tcp dport { 22, 80, 443, 8080 } accept
# set 통계 확인
nft list set inet filter blacklist
nft set 구현체와 알고리즘
nftables set은 nft_set_ops 인터페이스를 통해 다양한 백엔드 알고리즘을 플러그인 방식으로 지원합니다. 커널은 set의 키 타입, 크기, 기능 요구사항(interval, timeout, concatenation 등)에 따라 최적의 구현체를 자동 선택합니다.
/* include/net/netfilter/nf_tables.h — set 연산 인터페이스 */
struct nft_set_ops {
bool (*lookup)(const struct net *net,
const struct nft_set *set,
const u32 *key,
const struct nft_set_ext **ext);
bool (*update)(struct nft_set *set,
const u32 *key,
void *(*new)(struct nft_set *,
const struct nft_expr *,
struct nft_regs *));
int (*insert)(const struct net *net,
const struct nft_set *set,
const struct nft_set_elem *elem,
struct nft_set_ext **ext);
void (*remove)(const struct net *net,
const struct nft_set *set,
const struct nft_set_elem *elem);
int (*init)(const struct nft_set *set,
const struct nft_set_desc *desc,
const struct nlattr * const nla[]);
void (*destroy)(const struct nft_set *set);
unsigned int elemsize;
};
/* net/netfilter/nft_set_hash.c — jhash 기반 해시 set */
static bool
nft_rhash_lookup(const struct net *net,
const struct nft_set *set,
const u32 *key,
const struct nft_set_ext **ext)
{
struct nft_rhash *priv = nft_set_priv(set);
struct nft_rhash_elem *he;
struct nft_rhash_cmp_arg arg = {
.genmask = nft_genmask_cur(net),
.set = set,
.key = key,
};
he = rhashtable_lookup(&priv->ht, &arg,
nft_rhash_params);
if (!he)
return false;
*ext = &he->ext;
return true;
}
/* net/netfilter/nft_set_pipapo.c — 연결(concatenated) set
* PIPAPO: PIle PAcket POlicies — 다중 필드 매칭 알고리즘
* 예: { ip saddr . tcp dport } 같은 복합 키를 효율적으로 검색 */
static bool
nft_pipapo_lookup(const struct net *net,
const struct nft_set *set,
const u32 *key,
const struct nft_set_ext **ext)
{
/* 각 필드에 대해 비트맵 기반 매칭 수행 후
* 결과 비트맵을 AND 연산으로 교차 → O(n*fields) */
struct nft_pipapo *priv = nft_set_priv(set);
struct nft_pipapo_match *m;
unsigned long *res_map, *fill_map;
int i;
m = rcu_dereference(priv->match);
res_map = *raw_cpu_ptr(m->scratch);
for (i = 0; i < m->field_count; i++) {
nft_pipapo_avx2_lookup(/* 또는 일반 lookup */
&m->f[i], key, res_map, fill_map);
}
/* res_map에서 첫 번째 설정된 비트 = 매칭 요소 */
return pipapo_get_match(m, res_map, ext);
}
코드 설명
- nft_set_ops모든 set 백엔드가 구현해야 하는 인터페이스입니다.
lookup()은 패킷 처리 hot path에서 호출되므로 성능이 가장 중요합니다.update()는 동적 set(예: meter)에서 원자적 삽입/갱신에 사용됩니다.elemsize는 요소당 필요한 메모리 크기입니다. - nft_rhash (rhashtable)정확한 키 매칭에 사용되는 기본 구현체입니다. 커널의
rhashtable(resizable hash table)을 사용하여 O(1) 조회를 제공합니다.nft_genmask_cur()로 현재 세대 마스크를 확인하여, 트랜잭션 중 아직 커밋되지 않은 요소를 필터링합니다. - nft_pipapo (PIPAPO)연결된 키(concatenation)와 범위(interval)를 동시 지원하는 특수 알고리즘입니다. 각 필드를 독립적으로 비트맵 매칭한 후 AND 연산으로 교차합니다. x86에서는 AVX2 명령어를 활용한 벡터화 구현(
nft_pipapo_avx2_lookup())이 제공되어, 대규모 방화벽 규칙에서도 빠른 매칭이 가능합니다. - nft_rbtree (미도시)단일 필드의 범위(interval) 매칭에 사용됩니다. Red-Black 트리로 O(log n) 조회를 제공하며,
flags interval이 설정된 단순 범위 set에서 선택됩니다.
nftables Maps과 Verdict Maps
Map은 키-값 매핑(Mapping)으로, 하나의 규칙에서 다중 분기를 처리합니다. Verdict map(vmap)은 값이 verdict(accept/drop/jump 등)인 특수 map입니다.
# === 일반 map: 데이터 변환 ===
# 포트 번호 → 마크 값 매핑
nft add map inet filter port2mark '{ type inet_service : mark; }'
nft add element inet filter port2mark '{ 80 : 0x1, 443 : 0x2, 22 : 0x3 }'
nft add rule inet filter prerouting tcp dport map @port2mark meta mark set
# === verdict map: 다중 분기 ===
nft add map inet filter port_policy '{ type inet_service : verdict; }'
nft add element inet filter port_policy \
'{ 22 : jump ssh_chain, 80 : accept, 443 : accept, 25 : drop }'
nft add rule inet filter input tcp dport vmap @port_policy
# === 인터페이스별 verdict map ===
nft add map inet filter iface_policy '{ type ifname : verdict; }'
nft add element inet filter iface_policy \
'{ "eth0" : jump wan_filter, "eth1" : jump lan_filter, "lo" : accept }'
nft add rule inet filter input iifname vmap @iface_policy
# === 동적 set (meter) — 실시간 추적 ===
# 소스 IP별 연결 속도 제한
nft add rule inet filter input \
tcp dport 22 \
meter ssh_meter '{ ip saddr limit rate 3/minute burst 5 packets }' \
accept
# meter 내용 확인
nft list meter inet filter ssh_meter
ipset vs nftables set 마이그레이션: nftables set은 ipset의 상위 호환입니다. hash:ip → type ipv4_addr, hash:net → type ipv4_addr; flags interval, hash:ip,port → type ipv4_addr . inet_service로 대응됩니다. nftables set은 별도 커널 모듈 없이 nf_tables 내장 기능으로 동작합니다.
ebtables와 브릿지 방화벽
ebtables는 이더넷 프레임 수준(L2)에서 필터링하는 Netfilter 프레임워크 확장입니다. 브릿지 인터페이스를 통과하는 트래픽을 MAC 주소, VLAN 태그, 이더넷 프로토콜 기반으로 필터링합니다. 현대 시스템에서는 nftables bridge family가 ebtables를 대체합니다.
브릿지 Netfilter 아키텍처
브릿지 환경에서 패킷은 L2 포워딩 경로를 따르지만, br_netfilter 모듈이 활성화되면 L3(iptables/nftables inet) 규칙도 브릿지 트래픽에 적용됩니다.
/* net/bridge/br_netfilter_hooks.c — 브릿지 Netfilter 훅 */
/* 브릿지 훅 포인트 (L2 레벨) */
enum nf_br_hook_priorities {
NF_BR_PRI_FIRST = INT_MIN,
NF_BR_PRI_NAT_DST_BRIDGED = -300,
NF_BR_PRI_FILTER_BRIDGED = -200,
NF_BR_PRI_BRNF = 0,
NF_BR_PRI_NAT_DST_OTHER = 100,
NF_BR_PRI_FILTER_OTHER = 200,
NF_BR_PRI_NAT_SRC = 300,
NF_BR_PRI_LAST = INT_MAX,
};
/* br_netfilter는 브릿지 프레임을 "가짜 IP 패킷"으로 위장하여
* iptables/nftables inet 규칙 체인을 통과시킴 */
static unsigned int br_nf_pre_routing(
void *priv,
struct sk_buff *skb,
const struct nf_hook_state *state)
{
/* L2 브릿지 프레임을 NF_INET_PRE_ROUTING 훅으로 라우팅 */
if (br_validate_ipv4(net, skb))
return NF_DROP;
/* nf_bridge_info에 원본 L2 정보 저장 */
nf_bridge = nf_bridge_alloc(skb);
nf_bridge->physindev = skb->dev;
return NF_HOOK(NFPROTO_IPV4, NF_INET_PRE_ROUTING,
net, sk, skb, skb->dev, NULL,
br_nf_pre_routing_finish);
}
코드 설명
- nf_br_hook_priorities브릿지 Netfilter의 훅 우선순위 열거값입니다(
net/bridge/br_netfilter_hooks.c).NF_BR_PRI_BRNF(0)가 기준점이며, NAT는 음수(-300 DNAT, +300 SNAT), 필터링은 중간(-200 bridged, +200 other)에 위치합니다. 이 우선순위는 IPv4/IPv6의NF_IP_PRI_*와 별개의 네임스페이스입니다. - br_nf_pre_routing()
br_netfilter모듈의 핵심 함수로, L2 브릿지 프레임을 L3 Netfilter 훅으로 "위장 통과(fake route)"시킵니다. 브릿지 포워딩 경로에서 호출되어, iptables/nftables inet 규칙이 브릿지 트래픽에도 적용되게 합니다. Docker/Kubernetes에서net.bridge.bridge-nf-call-iptables=1sysctl로 이 동작을 제어합니다. - br_validate_ipv4()브릿지 프레임 내 IPv4 패킷의 유효성을 검증합니다. IP 헤더 길이, 체크섬, 총 길이 등을 확인하며, 비정상 패킷은 여기서 DROP됩니다. 이 검증이 L3 훅 진입 전에 수행되어 비정상 프레임이 iptables 규칙까지 도달하는 것을 방지합니다.
- nf_bridge_alloc()/physindev
skb에nf_bridge_info구조체를 할당하여 원본 L2 정보(실제 수신 인터페이스)를 보존합니다.physindev는 실제 브릿지 포트 디바이스이며, iptables의-m physdev --physdev-in매치가 이 정보를 참조합니다. L3 훅 통과 후 원래 L2 포워딩 경로로 복귀할 때 이 정보로 디바이스를 복원합니다. - NF_HOOK(NFPROTO_IPV4, NF_INET_PRE_ROUTING, ...)브릿지 프레임을 IPv4 PREROUTING 훅 체인에 주입합니다. 이로써 conntrack, NAT, 방화벽 규칙이 브릿지 트래픽에도 적용됩니다. 완료 콜백
br_nf_pre_routing_finish가 L3 훅 통과 후 브릿지 포워딩 경로로 복귀시킵니다.
ebtables 규칙 (레거시)
# ebtables 기본 사용법
# MAC 주소 기반 필터링
ebtables -A FORWARD -s 00:11:22:33:44:55 -j DROP
ebtables -A FORWARD -d ff:ff:ff:ff:ff:ff -j DROP # 브로드캐스트 차단
# VLAN 태그 필터링
ebtables -A FORWARD -p 802_1Q --vlan-id 100 -j ACCEPT
ebtables -A FORWARD -p 802_1Q -j DROP
# ARP 스푸핑 방지
ebtables -A FORWARD -p ARP --arp-ip-src 10.0.0.1 \
--arp-mac-src ! 00:11:22:33:44:55 -j DROP
# 프로토콜 필터링 (IPv4/IPv6만 허용)
ebtables -A FORWARD -p IPv4 -j ACCEPT
ebtables -A FORWARD -p IPv6 -j ACCEPT
ebtables -A FORWARD -p ARP -j ACCEPT
ebtables -A FORWARD -j DROP
# 브릿지 인터페이스별 필터링
ebtables -A FORWARD -i eth0 -o eth1 -j ACCEPT
ebtables -A FORWARD -i eth1 -o eth0 -j ACCEPT
ebtables -A FORWARD -j DROP
nftables bridge family
nftables의 bridge family는 ebtables의 현대적 대체제로, 동일한 nft 문법으로 L2 필터링을 수행합니다.
# nftables bridge family 예제
# 브릿지 테이블 생성
nft add table bridge filter
# 체인 생성
nft add chain bridge filter forward \
'{ type filter hook forward priority filter; policy accept; }'
# MAC 주소 필터링
nft add rule bridge filter forward ether saddr 00:11:22:33:44:55 drop
# VLAN 필터링
nft add rule bridge filter forward vlan id 100 accept
nft add rule bridge filter forward vlan id != 0 drop
# ARP 제한 (ARP storm 방지)
nft add rule bridge filter forward arp operation request \
limit rate 100/second burst 50 packets accept
nft add rule bridge filter forward arp operation request drop
# STP BPDU 보호 (허가되지 않은 포트에서 BPDU 차단)
nft add rule bridge filter forward ether daddr 01:80:c2:00:00:00 \
meta ibrname "br0" meta iifname "eth2" drop
# MAC+IP 바인딩 (IP 스푸핑 방지)
nft add set bridge filter mac_ip_bind \
'{ type ether_addr . ipv4_addr; }'
nft add element bridge filter mac_ip_bind \
'{ 00:11:22:33:44:55 . 10.0.0.1, aa:bb:cc:dd:ee:ff . 10.0.0.2 }'
nft add rule bridge filter forward ether saddr . ip saddr != @mac_ip_bind drop
# 브릿지 규칙셋 확인
nft list table bridge filter
br_netfilter와 nftables bridge 충돌: br_netfilter가 활성화되면 브릿지 트래픽이 inet family 훅도 통과합니다. nftables bridge family와 inet family 규칙이 동시에 적용되어 이중 필터링이 발생할 수 있습니다. 컨테이너 환경이 아니라면 sysctl net.bridge.bridge-nf-call-iptables=0으로 비활성화하세요.
TPROXY (투명 프록시)
TPROXY는 클라이언트의 원본 목적지 주소를 변경하지 않고 패킷을 로컬 프록시 프로세스로 전달하는 Netfilter 타겟입니다. mangle 테이블의 PREROUTING 체인에서 동작하며, NAT 변환 없이 IP_TRANSPARENT 소켓과 정책 라우팅을 조합하여 투명 프록시를 구현합니다.
전용 문서: TPROXY 커널 구현, IPv4/IPv6 이중 스택, 정책 라우팅, Squid/Envoy 실전 배포는 TPROXY (투명 프록시) 전용 문서를 참조하세요.
DDoS 방어 패턴
Netfilter/nftables를 활용한 DDoS(Distributed Denial of Service) 방어는 커널 레벨에서 악성 트래픽을 조기에 차단하여 애플리케이션 부하를 줄입니다. 대규모 공격은 XDP/BPF 조합이 효과적이지만, 일반적인 규모의 공격은 nftables만으로도 효과적으로 방어할 수 있습니다.
SYN Flood 방어
# === 커널 SYN Cookie 활성화 (필수) ===
sysctl -w net.ipv4.tcp_syncookies=1
sysctl -w net.ipv4.tcp_max_syn_backlog=65536
# === nftables SYN 속도 제한 ===
nft add table inet ddos_defense
nft add chain inet ddos_defense input \
'{ type filter hook input priority filter - 10; policy accept; }'
# 소스 IP별 SYN 속도 제한
nft add rule inet ddos_defense input \
tcp flags syn \
ct state new \
meter syn_meter '{ ip saddr limit rate over 30/second burst 50 packets }' \
drop
# 전체 SYN 속도 제한 (모든 소스 합산)
nft add rule inet ddos_defense input \
tcp flags syn \
ct state new \
limit rate over 10000/second \
drop
# 비정상 TCP 플래그 조합 차단
nft add rule inet ddos_defense input tcp flags '& (fin|syn|rst|psh|ack|urg) == fin|syn' drop
nft add rule inet ddos_defense input tcp flags '& (fin|syn|rst|psh|ack|urg) == syn|rst' drop
nft add rule inet ddos_defense input tcp flags '& (fin|syn|rst|psh|ack|urg) == fin|rst' drop
nft add rule inet ddos_defense input tcp flags '& (fin|syn|rst|psh|ack|urg) == 0x0' drop
연결 수 제한
# 소스 IP당 동시 연결 수 제한
nft add rule inet ddos_defense input \
tcp dport { 80, 443 } \
ct state new \
meter conn_limit '{ ip saddr ct count over 100 }' \
reject with tcp reset
# 서브넷 단위 제한 (/24)
nft add rule inet ddos_defense input \
tcp dport { 80, 443 } \
ct state new \
meter subnet_limit '{ ip saddr & 255.255.255.0 ct count over 500 }' \
drop
UDP Flood / DNS Amplification 방어
# UDP flood 속도 제한
nft add rule inet ddos_defense input \
udp \
meter udp_meter '{ ip saddr limit rate over 100/second burst 200 packets }' \
drop
# DNS amplification 방어 (소스 포트 53 대량 유입 차단)
nft add rule inet ddos_defense input \
udp sport 53 \
limit rate over 1000/second \
drop
# NTP amplification 방어 (monlist 응답)
nft add rule inet ddos_defense input \
udp sport 123 \
udp length > 100 \
limit rate over 500/second \
drop
# 소스 포트 0 차단 (스푸핑 표시)
nft add rule inet ddos_defense input udp sport 0 drop
nft add rule inet ddos_defense input tcp sport 0 drop
ICMP Flood 방어
# ICMP echo-request 속도 제한
nft add rule inet ddos_defense input \
icmp type echo-request \
limit rate 10/second burst 20 packets \
accept
nft add rule inet ddos_defense input \
icmp type echo-request \
drop
# ICMPv6 필수 타입 허용, 나머지 제한
nft add rule inet ddos_defense input \
icmpv6 type { nd-neighbor-solicit, nd-neighbor-advert, \
nd-router-solicit, nd-router-advert } \
accept
nft add rule inet ddos_defense input \
icmpv6 type echo-request \
limit rate 10/second burst 20 packets \
accept
# Smurf 공격 방어 (브로드캐스트 ICMP 차단)
sysctl -w net.ipv4.icmp_echo_ignore_broadcasts=1
hashlimit을 이용한 정밀 제한
# iptables hashlimit — 소스 IP + 목적지 포트 조합별 제한
iptables -A INPUT -p tcp --dport 80 \
-m hashlimit \
--hashlimit-above 50/sec \
--hashlimit-burst 100 \
--hashlimit-mode srcip,dstport \
--hashlimit-name http_limit \
--hashlimit-htable-expire 60000 \
-j DROP
# nftables meter로 동일 기능
nft add rule inet ddos_defense input \
tcp dport 80 \
meter http_limit '{ ip saddr . tcp dport limit rate over 50/second burst 100 packets }' \
drop
동적 차단 (자동 블랙홀)
# nftables set + meter 조합으로 자동 블랙리스팅
# 차단 리스트 set (5분 자동 만료)
nft add set inet ddos_defense auto_blacklist \
'{ type ipv4_addr; flags timeout; timeout 5m; }'
# 블랙리스트에 있으면 즉시 차단
nft add rule inet ddos_defense input \
ip saddr @auto_blacklist drop
# SYN 임계치 초과 시 자동으로 블랙리스트에 추가
nft add rule inet ddos_defense input \
tcp flags syn \
ct state new \
meter syn_detect '{ ip saddr limit rate over 50/second burst 100 packets }' \
add @auto_blacklist '{ ip saddr }' \
drop
# 현재 블랙리스트 확인
nft list set inet ddos_defense auto_blacklist
DDoS 방어 계층화 전략: Netfilter만으로는 대규모 DDoS(수십 Gbps 이상)를 방어하기 어렵습니다. 효과적인 방어를 위해 XDP(드라이버 레벨 드롭, ~24Mpps), nftables(stateful 필터링), 애플리케이션 레벨 WAF, 외부 DDoS 방어 서비스(Cloudflare, AWS Shield 등)를 계층적으로 조합하세요. XDP에 대한 상세 내용은 BPF/XDP 페이지를 참조하세요.
conntrack 고급 기능
Connection Tracking은 단순한 연결 상태 추적을 넘어 트래픽 통계, 라벨링, 관련 연결 예측(expectation) 등 고급 기능을 제공합니다. 이 섹션에서는 성능 모니터링과 정밀한 정책 수립에 활용되는 conntrack 고급 기능을 다룹니다.
conntrack Accounting (통계)
conntrack accounting은 각 연결에 대해 양방향 패킷/바이트 카운터를 유지합니다. 네트워크 모니터링, 과금, 트래픽 분석에 활용됩니다.
# conntrack accounting 활성화
sysctl -w net.netfilter.nf_conntrack_acct=1
# 또는 부팅 시 모듈 파라미터
modprobe nf_conntrack acct=1
# 통계 확인 (패킷/바이트 카운터 포함)
conntrack -L -o extended
# 출력 예:
# tcp 6 431995 ESTABLISHED src=10.0.0.1 dst=10.0.0.2 sport=45678 dport=80
# packets=1523 bytes=125840
# src=10.0.0.2 dst=10.0.0.1 sport=80 dport=45678
# packets=2105 bytes=3145728 [ASSURED] mark=0 use=1
# nftables에서 conntrack 카운터 활용
# 특정 연결의 바이트 수 기반 규칙
nft add rule inet filter forward \
ct bytes > 1073741824 \
log prefix '"heavy-flow: "' \
counter
/* include/net/netfilter/nf_conntrack_acct.h */
struct nf_conn_acct {
struct nf_conn_counter counter[IP_CT_DIR_MAX]; /* 원본/응답 */
};
struct nf_conn_counter {
atomic64_t packets; /* 패킷 수 */
atomic64_t bytes; /* 바이트 수 */
};
/* conntrack 확장으로 등록 — nf_conn->ext에 저장 */
static struct nf_ct_ext_type acct_extend = {
.len = sizeof(struct nf_conn_acct),
.align = __alignof__(struct nf_conn_acct),
.id = NF_CT_EXT_ACCT,
};
/* 패킷 통과 시 카운터 업데이트 (nf_conntrack_core.c) */
static void nf_ct_acct_update(
struct nf_conn *ct,
enum ip_conntrack_info ctinfo,
unsigned int len)
{
struct nf_conn_acct *acct = nf_conn_acct_find(ct);
if (acct) {
struct nf_conn_counter *counter = acct->counter;
unsigned int dir = CTINFO2DIR(ctinfo);
atomic64_inc(&counter[dir].packets);
atomic64_add(len, &counter[dir].bytes);
}
}
conntrack Labels
conntrack label은 각 연결에 비트맵 라벨을 부여하여, 방화벽 규칙에서 특정 라벨이 설정된 연결을 매칭합니다. connmark(32비트 정수)보다 풍부한 메타데이터를 연결에 부여할 수 있습니다.
# connlabel 정의 파일
# /etc/xtables/connlabel.conf (또는 /etc/connlabel.conf)
# 비트 번호와 라벨명 매핑
# 0 allowed
# 1 denied
# 2 inspected
# 3 logged
# 4 rate_limited
# iptables에서 connlabel 사용
# 라벨 설정
iptables -A FORWARD -m conntrack --ctstate NEW \
-s 10.0.0.0/8 -j CONNLABEL --set allowed
# 라벨 매칭
iptables -A FORWARD -m connlabel --label allowed -j ACCEPT
# nftables에서 connlabel 사용
nft add rule inet filter forward ct state new \
ip saddr 10.0.0.0/8 ct label set "allowed"
nft add rule inet filter forward ct label "allowed" accept
# conntrack 이벤트에서 라벨 확인
conntrack -E -o label
conntrack Expectations (관련 연결 예측)
Expectation은 기존 연결(master)에서 파생될 새로운 연결(related)을 미리 예측하여, 방화벽이 동적 포트의 관련 연결을 자동으로 허용하는 메커니즘입니다. FTP, SIP, H.323 등 데이터 채널을 별도 연결로 사용하는 프로토콜에서 매우 유용합니다.
/* include/net/netfilter/nf_conntrack_expect.h */
struct nf_conntrack_expect {
struct hlist_node hnode; /* 해시 리스트 */
struct nf_conntrack_tuple tuple; /* 예상 튜플 */
struct nf_conntrack_tuple mask; /* 튜플 마스크 */
struct nf_conntrack_tuple master; /* 마스터 연결 튜플 */
struct nf_conn *master_ct; /* 마스터 nf_conn */
struct timer_list timeout; /* 만료 타이머 */
refcount_t use;
unsigned int flags; /* NF_CT_EXPECT_* */
unsigned int class; /* helper 정의 클래스 */
void (*expectfn)(struct nf_conn *new,
struct nf_conntrack_expect *this);
struct nf_conntrack_helper *helper;
};
/* FTP helper 예제: PORT 명령 파싱 후 데이터 연결 expectation 생성 */
/* net/netfilter/nf_conntrack_ftp.c */
static int help(
struct sk_buff *skb,
unsigned int protoff,
struct nf_conn *ct,
enum ip_conntrack_info ctinfo)
{
/* 1. FTP 제어 채널에서 PORT/PASV 응답 파싱 */
/* 2. 데이터 연결의 IP:포트 추출 */
/* 3. expectation 생성 */
struct nf_conntrack_expect *exp;
exp = nf_ct_expect_alloc(ct);
nf_ct_expect_init(exp, NF_CT_EXPECT_CLASS_DEFAULT,
nf_ct_l3num(ct),
&ct->tuplehash[!dir].tuple.src.u3,
&cmd.u3,
IPPROTO_TCP, NULL, &cmd.u.tcp.port);
/* expectation 등록 → 새 데이터 연결이 RELATED로 분류됨 */
nf_ct_expect_related(exp);
nf_ct_expect_put(exp);
}
# conntrack expectation 관리
# 현재 expectation 목록
conntrack -L expect
# 출력 예:
# expect: proto=6 src=10.0.0.1 dst=10.0.0.2 sport=0 dport=20
# mask-src=255.255.255.255 mask-dst=255.255.255.255 sport=0 dport=65535
# master-src=10.0.0.1 master-dst=10.0.0.2 sport=45678 dport=21
# class=0 helper=ftp
# helper 모듈 로드 (자동 할당)
modprobe nf_conntrack_ftp
modprobe nf_conntrack_sip
modprobe nf_conntrack_tftp
# nftables에서 helper 명시적 할당
nft add ct helper inet filter ftp-helper \
'{ type "ftp" protocol tcp; }'
nft add rule inet filter input \
tcp dport 21 ct state new \
ct helper set "ftp-helper"
# expectation 최대 수 조정
sysctl -w net.netfilter.nf_conntrack_expect_max=1024
# 특정 helper의 expectation 이벤트 모니터링
conntrack -E expect
ctnetlink (커널-유저스페이스 동기화)
ctnetlink는 Netlink 기반으로 conntrack 테이블을 유저스페이스와 동기화하는 인터페이스입니다. HA(High Availability) 구성에서 방화벽 페일오버 시 conntrack 상태를 동기화하는 데 핵심적입니다.
# conntrackd — conntrack 동기화 데몬
# 기본 구성 (/etc/conntrackd/conntrackd.conf)
# Primary/Backup 모드: VRRP/keepalived와 연동
# 실시간 conntrack 이벤트 스트림
conntrack -E
# [NEW] tcp 6 120 SYN_SENT src=10.0.0.1 dst=10.0.0.2 sport=45678 dport=80
# [UPDATE] tcp 6 60 SYN_RECV src=10.0.0.1 dst=10.0.0.2 sport=45678 dport=80
# [UPDATE] tcp 6 432000 ESTABLISHED src=10.0.0.1 dst=10.0.0.2 ...
# [DESTROY] tcp 6 src=10.0.0.1 dst=10.0.0.2 sport=45678 dport=80
# 특정 프로토콜/상태 이벤트만 필터링
conntrack -E -p tcp --state ESTABLISHED
conntrack -E -p udp --event-mask NEW,DESTROY
# conntrack 엔트리 수동 추가 (페일오버 동기화 등)
conntrack -I --protonum 6 --timeout 120 \
--src 10.0.0.1 --dst 10.0.0.2 \
--sport 45678 --dport 80 \
--state ESTABLISHED
# 특정 연결 삭제
conntrack -D -p tcp --src 10.0.0.1 --dst 10.0.0.2 --dport 80
/* ctnetlink 메시지 구조 (Netlink 기반) */
/* include/uapi/linux/netfilter/nfnetlink_conntrack.h */
enum cntl_msg_types {
IPCTNL_MSG_CT_NEW, /* 새 conntrack 엔트리 */
IPCTNL_MSG_CT_GET, /* conntrack 조회 */
IPCTNL_MSG_CT_DELETE, /* conntrack 삭제 */
IPCTNL_MSG_CT_GET_CTRZERO, /* 카운터 읽고 초기화 */
IPCTNL_MSG_CT_GET_STATS_CPU, /* CPU별 통계 */
IPCTNL_MSG_CT_GET_STATS, /* 전체 통계 */
IPCTNL_MSG_CT_GET_DYING, /* 소멸 중인 엔트리 */
IPCTNL_MSG_CT_GET_UNCONFIRMED, /* 미확인 엔트리 */
};
/* ctnetlink 속성 (nla_policy) */
enum ctattr_type {
CTA_TUPLE_ORIG, /* 원본 방향 튜플 */
CTA_TUPLE_REPLY, /* 응답 방향 튜플 */
CTA_STATUS, /* IPS_* 상태 플래그 */
CTA_PROTOINFO, /* 프로토콜 상태 (TCP window 등) */
CTA_TIMEOUT, /* 남은 타임아웃 */
CTA_MARK, /* connmark 값 */
CTA_COUNTERS_ORIG, /* 원본 방향 카운터 */
CTA_COUNTERS_REPLY, /* 응답 방향 카운터 */
CTA_LABELS, /* connlabel 비트맵 */
/* ... */
};
HA 방화벽 conntrack 동기화: conntrackd는 Primary/Backup 또는 Active/Active 모드를 지원합니다. VRRP(keepalived) 전환 시 conntrackd -c(커밋)으로 대기 노드의 외부 캐시(Cache)를 커널 conntrack 테이블에 주입하여, 기존 TCP 연결이 끊기지 않고 유지됩니다.
eBPF와 Netfilter 통합
Linux 6.4부터 BPF_PROG_TYPE_NETFILTER 프로그램 타입이 도입되어, eBPF 프로그램을 Netfilter 훅 포인트에 직접 연결할 수 있습니다. 기존 nftables 규칙과 공존하면서, BPF의 유연한 프로그래밍 모델과 Netfilter의 성숙한 인프라(conntrack, NAT 등)를 결합합니다.
BPF_PROG_TYPE_NETFILTER 아키텍처
/* include/uapi/linux/bpf.h (Linux 6.4+) */
/* BPF 프로그램을 Netfilter 훅에 연결하는 link 속성 */
struct { /* bpf_link_create의 netfilter 속성 */
__u32 pf; /* 프로토콜 패밀리 (NFPROTO_IPV4 등) */
__u32 hooknum; /* 훅 포인트 (NF_INET_PRE_ROUTING 등) */
__s32 priority; /* 훅 우선순위 */
__u32 flags; /* BPF_F_NETFILTER_IP_DEFRAG 등 */
} netfilter;
/* UAPI가 직접 보장하는 것은 link 속성(pf/hooknum/priority/flags)이다.
* 실제 프로그램 컨텍스트(struct bpf_nf_ctx)는 BTF/커널 내부 정의에
* 의존하므로 CO-RE 기반으로 다루는 것이 안전하다. */
pf, hooknum, priority, flags입니다.
커널 selftests 기준 현재 attach는 NFPROTO_IPV4나 NFPROTO_IPV6 같은 구체적 protocol family에 붙는 형태가 기본이며, NFPROTO_INET attach는 허용되지 않습니다. 또한 BPF_F_NETFILTER_IP_DEFRAG 플래그로 조각 재조립을 요청할 수 있습니다.
BPF Netfilter 프로그램 예제
/* bpf_netfilter_example.c — BPF Netfilter 훅 프로그램 */
#include <linux/bpf.h>
#include <bpf/bpf_helpers.h>
#include <linux/ip.h>
#include <linux/tcp.h>
/* 차단 IP 맵 */
struct {
__uint(type, BPF_MAP_TYPE_HASH);
__uint(max_entries, 10000);
__type(key, __u32); /* IPv4 주소 */
__type(value, __u64); /* 차단 카운터 */
} blocklist SEC(".maps");
/* 연결별 통계 맵 */
struct {
__uint(type, BPF_MAP_TYPE_LRU_HASH);
__uint(max_entries, 100000);
__type(key, struct flow_key);
__type(value, struct flow_stats);
} flow_table SEC(".maps");
SEC("netfilter")
int nf_block_and_count(struct bpf_nf_ctx *ctx)
{
struct sk_buff *skb = ctx->skb;
struct iphdr *iph;
__u32 saddr;
iph = bpf_skb_load_bytes(...);
if (!iph)
return NF_ACCEPT;
saddr = iph->saddr;
/* 블랙리스트 조회 */
__u64 *count = bpf_map_lookup_elem(&blocklist, &saddr);
if (count) {
__sync_fetch_and_add(count, 1);
return NF_DROP; /* 차단 */
}
/* 플로우 통계 업데이트 */
struct flow_key key = {
.saddr = iph->saddr,
.daddr = iph->daddr,
.protocol = iph->protocol,
};
struct flow_stats *stats = bpf_map_lookup_elem(&flow_table, &key);
if (stats) {
__sync_fetch_and_add(&stats->packets, 1);
__sync_fetch_and_add(&stats->bytes, skb->len);
} else {
struct flow_stats new_stats = { .packets = 1, .bytes = skb->len };
bpf_map_update_elem(&flow_table, &key, &new_stats, BPF_NOEXIST);
}
return NF_ACCEPT;
}
# BPF Netfilter 프로그램 로드 및 연결
# 컴파일
clang -O2 -target bpf -c bpf_netfilter_example.c -o bpf_nf.o
# bpftool로 로드 및 Netfilter 훅에 연결
bpftool prog load bpf_nf.o /sys/fs/bpf/nf_block
bpftool net attach netfilter \
pinned /sys/fs/bpf/nf_block \
pf 2 \
hooknum 1 \
priority -100
# libbpf로 프로그래밍 방식 연결 (C 코드)
# struct bpf_link *link = bpf_program__attach_netfilter(prog, &opts);
# 연결된 BPF 프로그램 확인
bpftool net list
# 블랙리스트 맵에 IP 추가 (유저스페이스에서)
bpftool map update pinned /sys/fs/bpf/nf_block/blocklist \
key hex 0a 00 00 01 value hex 00 00 00 00 00 00 00 00
| 항목 | nftables | BPF Netfilter | XDP |
|---|---|---|---|
| 훅 위치 | Netfilter 훅 | Netfilter 훅 | 드라이버 수준 |
| conntrack 접근 | 네이티브 | bpf_ct_* 헬퍼 | 불가 |
| 프로그래밍 | 선언적 규칙 | C (BPF 바이트코드) | C (BPF 바이트코드) |
| 동적 업데이트 | set/map 업데이트 | BPF map 업데이트 | BPF map 업데이트 |
| 성능 | 보통 | JIT 컴파일 → 고속 | 최고 (sk_buff 미생성) |
| 상태 관리 | conntrack 의존 | conntrack + BPF map | BPF map만 |
| 적합 용도 | 일반 방화벽 | 커스텀 로직 + stateful | 고속 무상태 필터링 |
BPF conntrack 헬퍼 함수
/* BPF 프로그램에서 conntrack 정보 접근 (Linux 6.4+) */
/* conntrack 조회 */
struct nf_conn *bpf_xdp_ct_lookup(
struct xdp_md *xdp,
struct bpf_sock_tuple *tuple,
u32 tuple_size,
struct bpf_ct_opts *opts,
u32 opts_size);
/* conntrack 삽입 */
struct nf_conn *bpf_skb_ct_alloc(
struct sk_buff *skb,
struct bpf_sock_tuple *tuple,
u32 tuple_size,
struct bpf_ct_opts *opts,
u32 opts_size);
/* conntrack 상태/마크 설정 */
int bpf_ct_set_status(struct nf_conn *ct, u32 status);
int bpf_ct_change_status(struct nf_conn *ct, u32 status);
int bpf_ct_set_nat_info(struct nf_conn *ct,
union nf_inet_addr *addr, int port, enum nf_nat_manip_type manip);
/* 사용 예: BPF에서 conntrack 조회 후 마크 기반 분류 */
SEC("netfilter")
int nf_classify(struct bpf_nf_ctx *ctx)
{
struct bpf_ct_opts opts = {
.netns_id = BPF_F_CURRENT_NETNS,
.l4proto = IPPROTO_TCP,
};
struct bpf_sock_tuple tuple = { ... };
struct nf_conn *ct = bpf_skb_ct_lookup(
ctx->skb, &tuple, sizeof(tuple.ipv4),
&opts, sizeof(opts));
if (ct) {
if (ct->mark == 0x42) {
bpf_ct_release(ct);
return NF_DROP;
}
bpf_ct_release(ct);
}
return NF_ACCEPT;
}
BPF Netfilter vs XDP 선택 기준: conntrack/NAT 상태가 필요하면 BPF_PROG_TYPE_NETFILTER, 순수 패킷 수준 고속 처리가 필요하면 XDP를 사용하세요. 두 타입은 공존 가능하며, XDP에서 1차 필터링 → Netfilter BPF에서 상태 기반 2차 필터링 구성이 효과적입니다.
방화벽 자동화와 관리 도구
프로덕션 환경에서 iptables/nftables 규칙을 직접 관리하는 것은 복잡하고 오류 발생 가능성이 높습니다. firewalld, ufw 등의 관리 도구와 nft JSON API를 통한 프로그래밍 방식의 규칙 관리가 효과적입니다.
firewalld
firewalld는 D-Bus 인터페이스를 제공하는 동적 방화벽 관리 데몬으로, zone 기반 정책 모델을 사용합니다. 백엔드로 nftables(기본) 또는 iptables를 사용합니다.
# === firewalld 기본 사용법 ===
# 상태 확인
firewall-cmd --state
firewall-cmd --list-all
# 존(zone) 관리
firewall-cmd --get-zones
firewall-cmd --get-default-zone
firewall-cmd --get-active-zones
# 서비스 허용 (영구 적용)
firewall-cmd --zone=public --add-service=http --permanent
firewall-cmd --zone=public --add-service=https --permanent
firewall-cmd --reload
# 포트 직접 허용
firewall-cmd --zone=public --add-port=8080/tcp --permanent
# rich rule (세밀한 규칙)
firewall-cmd --zone=public --add-rich-rule='
rule family="ipv4"
source address="10.0.0.0/8"
port protocol="tcp" port="22"
accept' --permanent
# 소스 IP 기반 존 할당
firewall-cmd --zone=trusted --add-source=192.168.1.0/24 --permanent
# 포트 포워딩
firewall-cmd --zone=public \
--add-forward-port=port=80:proto=tcp:toport=8080:toaddr=10.0.0.2 \
--permanent
# 백엔드 확인 (nftables vs iptables)
firewall-cmd --get-backend
# 실제 생성된 nftables 규칙 확인
nft list ruleset | grep firewalld
| firewalld 존 | 기본 정책 | 용도 |
|---|---|---|
drop | 모두 차단 (ICMP 응답 없음) | 최대 보안 |
block | 모두 차단 (ICMP 거부 응답) | 명시적 거부 |
public | 선택적 허용 | 외부 네트워크 (기본) |
external | 선택적 허용 + NAT | 외부 인터페이스 |
dmz | 선택적 허용 | DMZ 서버 |
work | 대부분 허용 | 업무 네트워크 |
home | 대부분 허용 | 가정 네트워크 |
internal | 대부분 허용 | 내부 네트워크 |
trusted | 모두 허용 | 신뢰 네트워크 |
ufw (Uncomplicated Firewall)
# ufw 기본 사용법 (Ubuntu/Debian)
# 활성화/비활성화
ufw enable
ufw disable
ufw status verbose
# 기본 정책 설정
ufw default deny incoming
ufw default allow outgoing
# 서비스/포트 허용
ufw allow ssh
ufw allow 80/tcp
ufw allow 443/tcp
# IP 기반 규칙
ufw allow from 10.0.0.0/8 to any port 22
ufw deny from 192.168.1.100
# 속도 제한 (SSH 브루트포스 방어)
ufw limit ssh/tcp
# 로깅 수준
ufw logging medium
# 규칙 삭제
ufw status numbered
ufw delete 3
# ufw 내부: /etc/ufw/user.rules (iptables 규칙 파일)
nft JSON API
nftables는 JSON 형식으로 규칙셋을 import/export할 수 있어, 프로그래밍 방식의 방화벽 관리에 적합합니다. 자동화 도구(Ansible, Terraform 등)와의 통합에 유용합니다.
# 현재 규칙셋을 JSON으로 내보내기
nft -j list ruleset | python3 -m json.tool
# JSON 규칙셋 예제
cat <<'EOF' > /tmp/firewall.json
{
"nftables": [
{ "flush": { "ruleset": null } },
{ "add": {
"table": {
"family": "inet",
"name": "filter"
}
}},
{ "add": {
"chain": {
"family": "inet",
"table": "filter",
"name": "input",
"type": "filter",
"hook": "input",
"prio": 0,
"policy": "drop"
}
}},
{ "add": {
"rule": {
"family": "inet",
"table": "filter",
"chain": "input",
"expr": [
{ "match": {
"left": { "ct": { "key": "state" } },
"right": ["established", "related"],
"op": "=="
}},
{ "accept": null }
]
}
}},
{ "add": {
"rule": {
"family": "inet",
"table": "filter",
"chain": "input",
"expr": [
{ "match": {
"left": { "meta": { "key": "iifname" } },
"right": "lo",
"op": "=="
}},
{ "accept": null }
]
}
}},
{ "add": {
"rule": {
"family": "inet",
"table": "filter",
"chain": "input",
"expr": [
{ "match": {
"left": { "payload": {
"protocol": "tcp",
"field": "dport"
}},
"right": { "set": [22, 80, 443] },
"op": "=="
}},
{ "accept": null }
]
}
}}
]
}
EOF
# JSON 규칙셋 적용
nft -j -f /tmp/firewall.json
# Python에서 nft JSON API 활용
# import subprocess, json
# result = subprocess.run(['nft', '-j', 'list', 'ruleset'],
# capture_output=True, text=True)
# ruleset = json.loads(result.stdout)
Ansible/자동화 통합
# Ansible nftables 관리 예제 (ansible.posix.nftables 모듈 활용 가능)
# 또는 template 모듈로 nftables.conf 배포
# /etc/nftables.conf — 부팅 시 자동 로드
# systemctl enable nftables
# nftables 규칙 영속화
nft list ruleset > /etc/nftables.conf
# 또는 include 디렉티브로 모듈화
# /etc/nftables.conf:
# #!/usr/sbin/nft -f
# flush ruleset
# include "/etc/nftables.d/*.nft"
# 규칙 검증 (적용 전 문법 확인)
nft -c -f /etc/nftables.conf
# 성공 시 출력 없음, 실패 시 에러 메시지
# 원자적 규칙셋 교체 (중단 없음)
nft -f /etc/nftables.conf
# flush + 새 규칙 로드가 하나의 트랜잭션으로 처리
프로덕션 방화벽 관리 권장사항:
- 버전 관리: nftables 규칙 파일을 Git으로 관리하여 변경 이력 추적
- 원자적 교체:
nft -f는 flush + reload를 단일 트랜잭션으로 처리하여 규칙 공백 방지 - 사전 검증:
nft -c -f로 문법 검증 후 적용 - 롤백(Rollback) 계획:
at now + 5 minutes으로 규칙 적용 전 자동 복원 스케줄링 - 모니터링:
nft monitor로 규칙 변경 실시간 감시
참고 자료
- 커널 최신 안정 버전: kernel.org (2026-03-07 기준 stable 6.19.6)
- Linux UAPI 헤더: linux/netfilter.h, linux/netfilter_ipv4.h, linux/netfilter_ipv6.h, linux/bpf.h
- 공식 커널 문서: Netfilter flowtable, Transparent proxy support, nf_conntrack sysctl
- Netfilter 프로젝트 문서: nftables man page, libnetfilter_queue, iptables project
- 커널 소스 트리:
net/netfilter/,include/linux/netfilter*.h,include/uapi/linux/netfilter*.h,tools/testing/selftests/bpf/ - 관련 페이지: 네트워크 스택 · sk_buff · NAT · BPF/XDP · 네임스페이스 · Bridge/VLAN · 커널 보안
- 커널 모듈 기초: 커널 모듈 페이지 참조
- nftables 위키: nftables wiki — nftables 설정, 문법, 예제를 포괄적으로 다루는 공식 위키입니다
- man 페이지: nft(8), iptables(8), conntrack(8)
- RFC: RFC 3022 (Traditional NAT), RFC 6241 (NETCONF), RFC 4949 (Internet Security Glossary)
- LWN 기사: nftables — a new packet filtering engine — nftables 설계 철학과 iptables 대비 개선점을 소개합니다
- 커널 소스: nf_conntrack_core.c — conntrack 핵심 구현 코드입니다
관련 문서
Netfilter와 관련된 다른 주제를 더 깊이 이해하고 싶다면 다음 문서를 참고하세요.