nf_conntrack 헬퍼 & ALG
Linux 커널 Application Layer Gateway(ALG) 내부 구조, FTP/SIP/H.323/RTSP/PPTP conntrack 헬퍼, 커스텀 헬퍼 작성, ctnetlink 이벤트, 클라우드/컨테이너 환경 ALG 이슈 종합 가이드. 커널 내부 데이터 경로, 핵심 자료구조/API, 운영 환경 튜닝 포인트와 장애 디버깅 절차까지 실무 관점으로 다룹니다.
핵심 요약
- ALG 필요성 — FTP·SIP 등 제어/데이터 채널이 분리된 프로토콜은 방화벽이 페이로드를 분석해야 데이터 채널을 자동 허용할 수 있습니다.
- conntrack expect —
nf_conntrack_expect는 "예상 연결"을 등록해두고, 실제 연결이 도착하면 기존 세션과 자동 연결합니다.
- 헬퍼 등록 —
nf_conntrack_helper 구조체의 .help 콜백이 패킷 페이로드를 파싱하고 expect를 등록합니다.
- FTP 헬퍼 — PORT/PASV 명령 파싱으로 데이터 채널 IP·포트를 추출해 expect를 등록합니다.
- SIP 헬퍼 — SIP INVITE의 SDP 바디에서 RTP 포트를 파싱해 미디어 채널 expect를 등록합니다.
- H.323/RTSP 헬퍼 — H.323은 Q.931(TCP 1720), H.245 협상, RTP 미디어 등 다중 채널을 순차적으로 협상하며 각 채널마다 expect를 등록합니다.
- PPTP/GRE 헬퍼 — PPTP는 TCP 1723 제어 채널에서 Call ID를 추출해 GRE 프로토콜 데이터 채널 expect를 등록합니다.
- 헬퍼 보안 이슈 — 자동 헬퍼 할당(
nf_conntrack_helper=1)은 CVE-2014-8160 등 보안 취약점의 원인이며, 현재 기본값은 0(비활성)입니다.
- 타임아웃 정책 —
nft ct timeout 객체로 서비스별 커스텀 타임아웃을 설정해 conntrack 테이블 효율을 최적화합니다.
- 커스텀 헬퍼 —
nf_ct_helper_init()와 nf_conntrack_helper_register()로 자체 프로토콜 헬퍼를 커널 모듈로 구현할 수 있습니다.
- ctnetlink/conntrackd — Netlink 기반 이벤트 채널로 NEW/UPDATE/DESTROY 이벤트를 수신해 conntrack 상태를 HA 장비 간에 동기화합니다.
- 클라우드 이슈 — Kubernetes·AWS·GCP 환경에서는 ALG가 오히려 SIP/FTP를 깨뜨리는 경우가 많아 비활성화가 권장됩니다.
nf_conntrack_expect는 "예상 연결"을 등록해두고, 실제 연결이 도착하면 기존 세션과 자동 연결합니다.nf_conntrack_helper 구조체의 .help 콜백이 패킷 페이로드를 파싱하고 expect를 등록합니다.nf_conntrack_helper=1)은 CVE-2014-8160 등 보안 취약점의 원인이며, 현재 기본값은 0(비활성)입니다.nft ct timeout 객체로 서비스별 커스텀 타임아웃을 설정해 conntrack 테이블 효율을 최적화합니다.nf_ct_helper_init()와 nf_conntrack_helper_register()로 자체 프로토콜 헬퍼를 커널 모듈로 구현할 수 있습니다.단계별 이해
- conntrack 기초 확인
conntrack -L로 기존 연결 추적 테이블을 확인하고,ESTABLISHED·RELATED상태를 구분합니다. - 헬퍼 모듈 로드
modprobe nf_conntrack_ftp로 FTP 헬퍼를 로드하고,lsmod | grep nf_conntrack으로 로드된 헬퍼 목록을 확인합니다. - expect 테이블 관찰
FTP PASV 연결 시conntrack -L expect로 등록된 expect 엔트리를 실시간으로 확인합니다. - iptables 헬퍼 연결
iptables -t raw -A PREROUTING -p tcp --dport 21 -j CT --helper ftp로 특정 트래픽에 헬퍼를 명시적으로 연결합니다. - SIP ALG 문제 진단
SIP 통화가 한쪽만 들리는 경우sysctl net.netfilter.nf_conntrack_helper로 ALG 활성화 여부를 확인하고 비활성화를 검토합니다. - 보안 강화
nf_conntrack_helper=0상태에서 nftables로 수동 헬퍼 할당(ct helper set "ftp")을 통해 의도한 트래픽에만 ALG를 적용합니다. - 타임아웃 최적화
nft ct timeout객체를 생성해 서비스별 TCP/UDP 타임아웃을 커스터마이즈하고,conntrack -S로 드롭 통계를 모니터링합니다. - 커스텀 헬퍼 개발
nf_conntrack_helper구조체를 구현하고,nf_conntrack_helper_register()로 등록해 자체 프로토콜의 연관 채널을 자동 추적합니다.
개요: ALG와 conntrack 헬퍼
리눅스 Netfilter의 conntrack 헬퍼(Connection Tracking Helper)는 Application Layer Gateway(ALG)를 구현하는 커널 메커니즘입니다. 일반적인 TCP/UDP 연결은 방화벽이 5-튜플(프로토콜·출발지 IP·출발지 포트·목적지 IP·목적지 포트)만으로 추적할 수 있지만, FTP·SIP·H.323처럼 제어 채널과 데이터 채널이 분리된 프로토콜은 페이로드를 파싱해야 연관 연결을 파악할 수 있습니다.
| 구분 | 단순 프로토콜 (HTTP, SSH) | 복합 프로토콜 (FTP, SIP) |
|---|---|---|
| 채널 구성 | 단일 연결 | 제어 채널 + 동적 데이터 채널 |
| 방화벽 처리 | 5-튜플만으로 추적 가능 | 페이로드 파싱 없이 데이터 채널 차단 |
| ALG 필요 여부 | 불필요 | 필수 (데이터 채널 자동 허용) |
| conntrack 상태 | ESTABLISHED | 제어: ESTABLISHED, 데이터: RELATED |
conntrack 헬퍼는 제어 채널 패킷을 가로채 페이로드를 파싱하고,
nf_conntrack_expect를 통해 예상 연결(expected connection)을 등록합니다.
이후 데이터 채널 연결이 도착하면 커널이 이를 RELATED 상태로 분류해 방화벽 규칙 없이 자동으로 허용합니다.
iptables -A FORWARD -m conntrack --ctstate RELATED,ESTABLISHED -j ACCEPT 한 줄로
ALG가 등록한 모든 데이터 채널을 자동 허용할 수 있습니다. ALG 없이는 각 데이터 포트를 개별 허용해야 합니다.
ALG(Application Layer Gateway) 개념
ALG는 OSI 7계층(애플리케이션 계층)의 프로토콜 내용을 분석해 방화벽·NAT 장비가 연관 연결을 자동으로 처리할 수 있도록 도와주는 기술입니다.
리눅스 커널에서 ALG는 nf_conntrack_helper 구조체를 통해 구현됩니다.
문제: FTP PASV 모드와 방화벽
FTP는 두 가지 모드로 데이터를 전송합니다.
| 모드 | 데이터 채널 방향 | 방화벽 문제 |
|---|---|---|
| Active (PORT) | 서버 → 클라이언트 (서버가 연결 개시) | 클라이언트 방화벽이 인바운드 연결 차단 |
| Passive (PASV) | 클라이언트 → 서버 (클라이언트가 연결 개시) | 서버 방화벽이 임의 포트 인바운드 차단 |
PASV 모드에서 서버는 제어 채널(포트 21)에 227 Entering Passive Mode (a,b,c,d,p1,p2)를 응답하며,
클라이언트는 (p1*256)+p2 포트로 새 연결을 엽니다.
방화벽은 이 포트를 알 수 없으므로 ALG 없이는 차단합니다.
conntrack expect 메커니즘
nf_conntrack_expect는 "아직 도착하지 않은 연관 연결"을 사전에 등록하는 자료구조입니다.
헬퍼가 제어 채널에서 데이터 채널 정보를 파싱하면 다음 과정이 진행됩니다.
nf_ct_expect_alloc(ct)— 현재 conntrack에 연결된 expect 객체 할당nf_ct_expect_init(exp, ...)— 예상 연결의 5-튜플·마스크 설정nf_ct_expect_related(exp)— expect 해시 테이블에 등록 (만료 타이머 포함)- 실제 연결 도착 시
nf_conntrack_in()이 expect 테이블을 조회하고, 매칭되면IP_CT_RELATED상태 부여
nf_conntrack_helper 구조체
/* include/net/netfilter/nf_conntrack_helper.h */
struct nf_conntrack_helper {
struct hlist_node hnode; /* 헬퍼 해시 테이블 노드 */
char name[NF_CT_HELPER_NAME_LEN]; /* "ftp", "sip" 등 */
refcount_t refcnt;
struct module *me; /* THIS_MODULE */
const struct nf_conntrack_expect_policy *expect_policy;
struct nf_conntrack_tuple tuple; /* 헬퍼가 처리할 트래픽 5-튜플 */
/* 핵심 콜백: 패킷마다 호출, NF_ACCEPT/NF_DROP 반환 */
int (*help)(struct sk_buff *skb,
unsigned int protoff,
struct nf_conn *ct,
enum ip_conntrack_info conntrackinfo);
/* NAT 연동: 페이로드의 주소/포트를 재작성 */
void (*destroy)(struct nf_conn *ct);
int (*from_nlattr)(struct nlattr *attr, struct nf_conn *ct);
unsigned int flags;
unsigned int expect_class_max;
};
net.netfilter.nf_conntrack_helper=0이 기본값입니다.
헬퍼를 활성화하려면 iptables -t raw -A PREROUTING -p tcp --dport 21 -j CT --helper ftp처럼
명시적으로 트래픽과 헬퍼를 연결해야 합니다.
내장 conntrack 헬퍼
리눅스 커널은 다음 프로토콜에 대한 내장 conntrack 헬퍼를 제공합니다.
각 헬퍼는 net/netfilter/ 디렉터리의 별도 파일로 구현됩니다.
| 헬퍼 모듈 | 프로토콜 | 기본 포트 | 기능 | 소스 파일 |
|---|---|---|---|---|
nf_conntrack_ftp |
FTP | TCP 21 | PORT/PASV 명령 파싱 → 데이터 채널 expect 등록 | nf_conntrack_ftp.c |
nf_conntrack_tftp |
TFTP | UDP 69 | UDP 데이터 전송 채널 expect 등록 | nf_conntrack_tftp.c |
nf_conntrack_sip |
SIP | UDP/TCP 5060 | SDP 파싱 → RTP/RTCP 미디어 채널 expect 등록 | nf_conntrack_sip.c |
nf_conntrack_h323 |
H.323 | TCP 1720 | H.245 시그널링 채널 및 RTP 채널 expect 등록 | nf_conntrack_h323.c |
nf_conntrack_rtsp |
RTSP | TCP 554 | SETUP 메시지 파싱 → RTP/RTCP 채널 expect 등록 | nf_conntrack_rtsp.c |
nf_conntrack_pptp |
PPTP | TCP 1723 | GRE 터널 연결 expect 등록 (Call ID 기반) | nf_conntrack_pptp.c |
nf_conntrack_amanda |
Amanda | UDP 10080 | 백업 소프트웨어 데이터 채널 expect 등록 | nf_conntrack_amanda.c |
nf_conntrack_irc |
IRC | TCP 6667 | DCC(Direct Client Connection) 전송 expect 등록 | nf_conntrack_irc.c |
헬퍼 확인 및 관리 명령
# 로드된 conntrack 헬퍼 모듈 확인
lsmod | grep nf_conntrack
# 등록된 헬퍼 목록 확인 (conntrack -L expect와 병행)
cat /proc/net/nf_conntrack_expect
# FTP 헬퍼 수동 로드
modprobe nf_conntrack_ftp
# 헬퍼 자동 로드 활성화 (보안 취약, 권장 안 함)
sysctl -w net.netfilter.nf_conntrack_helper=1
# 권장: iptables raw 테이블로 명시적 연결
iptables -t raw -A PREROUTING -p tcp --dport 21 -j CT --helper ftp
iptables -t raw -A OUTPUT -p tcp --dport 21 -j CT --helper ftp
# expect 테이블 실시간 확인 (FTP PASV 연결 후 빠르게 확인)
conntrack -L expect
nf_conntrack_helper=1 (자동 로드)은 커널 4.7 이전의 기본값이었지만,
임의 트래픽에 헬퍼를 자동 적용해 의도치 않은 RELATED 허용을 유발할 수 있습니다.
최신 커널에서는 기본값이 0이며, iptables CT 타깃으로 명시적 연결을 권장합니다.
FTP 헬퍼 심화
nf_conntrack_ftp는 가장 널리 사용되는 conntrack 헬퍼로, FTP 제어 채널의 PORT/PASV 명령을 파싱합니다.
소스 파일: net/netfilter/nf_conntrack_ftp.c
Active 모드 (PORT 명령) 처리
클라이언트가 서버에 데이터 채널 주소를 알리는 PORT 명령을 파싱합니다.
/* PORT a,b,c,d,p1,p2 형식 파싱 (pseudocode) */
/* FTP 제어 채널 패킷에서 PORT 명령 탐지 */
static int ftp_help(struct sk_buff *skb, unsigned int protoff,
struct nf_conn *ct, enum ip_conntrack_info ctinfo)
{
/* 패킷에서 FTP 페이로드 위치 계산 */
const unsigned char *data;
unsigned int dataoff = protoff + sizeof(struct tcphdr);
/* "PORT " 문자열 탐지 */
/* PORT 192,168,1,100,192,1 파싱:
* IP = 192.168.1.100
* port = 192*256 + 1 = 49153 */
__be32 ip = MKIPADDR(a, b, c, d);
__be16 port = htons(p1 * 256 + p2);
/* expect 할당 및 등록 */
struct nf_conntrack_expect *exp = nf_ct_expect_alloc(ct);
nf_ct_expect_init(exp, NF_CT_EXPECT_CLASS_DEFAULT,
AF_INET, NULL, &ip, IPPROTO_TCP, NULL, &port);
nf_ct_expect_related(exp); /* expect 해시 테이블에 등록 */
nf_ct_expect_put(exp);
return NF_ACCEPT;
}
Passive 모드 (PASV 응답) 처리
서버가 클라이언트에 임시 포트를 알리는 PASV 응답을 파싱합니다.
/* 서버 PASV 응답: "227 Entering Passive Mode (a,b,c,d,p1,p2)"
* 예: "227 Entering Passive Mode (203,0,113,5,192,0)"
* → 서버 IP = 203.0.113.5
* → 데이터 포트 = 192*256 + 0 = 49152
*
* expect 등록:
* tuple.src = 클라이언트 IP (임의)
* tuple.dst = 서버 IP : 49152
* 클라이언트가 이 포트로 SYN을 보내면 RELATED로 분류됨 */
/* NAT 연동: PASV 응답의 IP 주소도 재작성 */
/* nf_nat_ftp.c: NF_CT_HELPER_F_NAT_AWARE 플래그로 페이로드 수정 */
NAT 환경에서의 FTP ALG
NAT 뒤에 FTP 클라이언트가 있는 경우, 헬퍼는 페이로드의 사설 IP를 공인 IP로 재작성해야 합니다.
nf_nat_ftp.c가 이 역할을 담당하며, TCP 시퀀스 번호 조정(nf_nat_mangle_tcp_packet())도 수행합니다.
# NAT + FTP 설정 예시
# 1. FTP 헬퍼 로드
modprobe nf_conntrack_ftp
modprobe nf_nat_ftp # NAT 페이로드 재작성 모듈
# 2. 제어 채널에 헬퍼 연결
iptables -t raw -A PREROUTING -p tcp --dport 21 -j CT --helper ftp
# 3. RELATED 상태 허용 (데이터 채널 자동 허용)
iptables -A FORWARD -m conntrack --ctstate RELATED,ESTABLISHED -j ACCEPT
# 4. 마스커레이드 (SNAT)
iptables -t nat -A POSTROUTING -o eth0 -j MASQUERADE
vsftpd.conf의 pasv_min_port/pasv_max_port 옵션으로 범위를 설정하고,
해당 포트 범위를 방화벽에서 직접 허용하는 방법이 더 간단하고 안전한 경우가 많습니다.
find_pattern() 함수와 페이로드 스캔
nf_conntrack_ftp.c의 핵심은 find_pattern() 함수로, FTP 페이로드에서 명령 패턴을 탐색합니다.
/* net/netfilter/nf_conntrack_ftp.c — find_pattern() 핵심 로직 (pseudocode) */
/* FTP 패턴 테이블: 파싱할 명령 목록 */
static const struct ftp_search {
const char *pattern; /* 탐색 문자열: "PORT ", "227 " 등 */
size_t plen; /* 패턴 길이 */
char skip; /* 구분 문자 (','나 공백) */
char term; /* 종료 문자 ('\r' 또는 '\n') */
enum nf_ct_ftp_type type; /* ACTIVE_PORT, PASV_227 등 */
int (*getnum)(const char *, size_t, struct nf_conntrack_man *, char);
} search[NF_CT_FTP_MAX][2] = {
/* 클라이언트 → 서버: PORT 명령 (Active mode) */
[NF_CT_FTP_PORT][IP_CT_DIR_ORIGINAL] = {
.pattern = "PORT", .plen = 4,
.skip = ',', .term = '\r',
.type = NF_CT_FTP_PORT,
.getnum = try_number, /* a,b,c,d,p1,p2 파싱 */
},
/* 서버 → 클라이언트: PASV 응답 (Passive mode) */
[NF_CT_FTP_PASV][IP_CT_DIR_REPLY] = {
.pattern = "227 ", .plen = 4,
.skip = '(', .term = ')',
.type = NF_CT_FTP_PASV,
.getnum = try_number,
},
/* IPv6 확장: EPRT (Active), EPSV (Passive) */
[NF_CT_FTP_EPRT][IP_CT_DIR_ORIGINAL] = {
.pattern = "EPRT", .plen = 4,
.skip = '|', .term = '|',
.type = NF_CT_FTP_EPRT,
.getnum = try_eprt, /* |AF|address|port| 파싱 */
},
[NF_CT_FTP_EPSV][IP_CT_DIR_REPLY] = {
.pattern = "229 ", .plen = 4,
.skip = '(', .term = ')',
.type = NF_CT_FTP_EPSV,
.getnum = try_epsv_response, /* (|||port|) 파싱 */
},
};
Active vs Passive FTP 모드 완전 비교
| 항목 | Active 모드 (PORT/EPRT) | Passive 모드 (PASV/EPSV) |
|---|---|---|
| 데이터 채널 개시 | 서버가 클라이언트로 연결 (SYN) | 클라이언트가 서버로 연결 (SYN) |
| FTP 명령 | PORT a,b,c,d,p1,p2 |
227 Entering Passive Mode (a,b,c,d,p1,p2) |
| IPv6 명령 | EPRT |2|::1|49152| |
229 Entering Extended Passive Mode (|||49152|) |
| 헬퍼 파싱 방향 | IP_CT_DIR_ORIGINAL (클라이언트→서버) |
IP_CT_DIR_REPLY (서버→클라이언트) |
| NAT 재작성 대상 | 클라이언트 IP:PORT → 공인 IP:PORT | 서버 IP:PORT (NAT 뒤 서버일 때) |
| 방화벽 문제 | 클라이언트 방화벽이 인바운드 차단 | 서버 방화벽의 임의 포트 인바운드 차단 |
FTPS (FTP over TLS) — 헬퍼가 작동하지 않는 이유
FTPS(Explicit TLS: AUTH TLS 또는 Implicit FTPS: 포트 990)는 제어 채널이 TLS로 암호화되어
FTP 헬퍼가 PORT/PASV 명령을 파싱할 수 없습니다.
nf_conntrack_ftp가 페이로드를 읽을 수 없습니다.
FTPS 환경에서는 PASV 포트 범위를 고정(pasv_min_port/pasv_max_port)하고 방화벽 규칙에 명시하는 것이 유일한 방법입니다.
# FTPS 환경 권장 설정 (vsftpd.conf)
pasv_enable=YES
pasv_min_port=50000
pasv_max_port=51000
ssl_enable=YES
# 방화벽: PASV 포트 범위 직접 허용 (ALG 없이)
iptables -A FORWARD -p tcp --dport 50000:51000 -j ACCEPT
# 또는 nftables
nft add rule ip filter forward tcp dport 50000-51000 accept
FTP 헬퍼 디버깅
# conntrack FTP 추적 로그 활성화
sysctl -w net.netfilter.nf_log_all_netns=1
# FTP 연결 시 conntrack 상태 확인
conntrack -L | grep ftp
# FTP PASV 연결 직후 expect 엔트리 확인
conntrack -L expect
# 출력 예:
# tcp 6 29 ESTABLISHED src=192.168.1.100 dst=203.0.113.5
# sport=0 dport=49152 src=203.0.113.5 dst=192.168.1.100
# sport=49152 dport=0 mark=0 use=2
# conntrack 이벤트 실시간 모니터링
conntrack -E
# FTP 패킷 덤프와 병행 (제어 채널 패킷 확인)
tcpdump -i eth0 -A 'tcp port 21 and (((ip[2:2] - ((ip[0]&0xf)<<2)) - ((tcp[12]&0xf0)>>2)) != 0)'
SIP 헬퍼 심화
nf_conntrack_sip는 SIP(Session Initiation Protocol) VoIP 트래픽을 위한 conntrack 헬퍼입니다.
SIP는 시그널링 채널(포트 5060)과 미디어 채널(RTP, 동적 포트)이 분리되어 있어 ALG가 필수입니다.
소스 파일: net/netfilter/nf_conntrack_sip.c
SIP 세션 설정 흐름과 ALG 개입
/* SIP INVITE 처리 흐름 (pseudocode) */
/* 1. SIP INVITE 메시지 감지 */
static int sip_help(struct sk_buff *skb, ...) {
/* SIP 메서드 파싱 (INVITE, REGISTER, BYE 등) */
if (strncmp(msg, "INVITE", 6) == 0) {
/* 2. SDP 바디 파싱 */
/* SDP 예시:
* v=0
* o=- 12345 1 IN IP4 192.168.1.100
* c=IN IP4 192.168.1.100 ← RTP 목적지 IP
* m=audio 49170 RTP/AVP 0 ← RTP 포트 49170
*/
parse_sdp_connection(&rtp_ip, &rtp_port);
parse_sdp_media(&rtp_port);
/* 3. RTP expect 등록 */
nf_ct_expect_init(exp_rtp, ..., &rtp_ip, IPPROTO_UDP, NULL, &rtp_port);
nf_ct_expect_related(exp_rtp);
/* 4. RTCP expect 등록 (RTP 포트 + 1) */
rtcp_port = htons(ntohs(rtp_port) + 1);
nf_ct_expect_init(exp_rtcp, ..., &rtp_ip, IPPROTO_UDP, NULL, &rtcp_port);
nf_ct_expect_related(exp_rtcp);
}
return NF_ACCEPT;
}
SIP ALG의 주요 문제점
SIP ALG는 의도와 달리 VoIP 통화를 방해하는 경우가 많습니다.
| 문제 | 원인 | 증상 |
|---|---|---|
| Contact 헤더 변조 | ALG가 SIP 패킷의 Contact/Via 헤더를 공인 IP로 재작성 (잘못된 경우) | REGISTER 실패, 통화 연결 불가 |
| SRTP 페이로드 파싱 실패 | TLS(SIPS) 또는 암호화된 SIP를 ALG가 파싱하지 못함 | 미디어 채널 expect 미등록 → RTP 차단 |
| SIP trunk 충돌 | ALG의 Via 헤더 수정이 SIP 프록시 경로와 충돌 | 일방향 음성 (one-way audio) |
| 타이밍 문제 | expect 만료 전 RTP 채널이 시작되지 않을 경우 | 간헐적 통화 실패 |
SIP ALG 비활성화 권장 시나리오
# SIP ALG 비활성화 (권장: 전용 SIP 프록시 사용 환경)
# 방법 1: 헬퍼 모듈 언로드
rmmod nf_conntrack_sip
rmmod nf_nat_sip
# 방법 2: 특정 트래픽에만 헬퍼 미적용 (CT --helper 미설정)
# nf_conntrack_helper=0 상태에서 SIP 포트에 CT 타깃 미설정
# 방법 3: nftables로 특정 흐름에서 헬퍼 제거
nft add rule ip filter PREROUTING \
tcp dport 5060 ct helper unset
# SIP ALG 대안: STUN/TURN 서버 또는 SIP-aware 프록시 사용
# (Asterisk, Kamailio 등은 자체 NAT traversal 처리)
RTP/RTCP expect 등록 메커니즘
nf_nat_sip_expected()는 SIP expect가 실제 RTP 연결로 매칭될 때 호출되는 콜백으로,
NAT 환경에서 미디어 채널에 적절한 DNAT/SNAT 규칙을 자동 추가합니다.
/* net/netfilter/nf_nat_sip.c — nf_nat_sip_expected() 동작 흐름 */
/* SIP expect → RTP 연결 수립 시 호출 */
static void nf_nat_sip_expected(struct nf_conn *ct,
struct nf_conntrack_expect *exp)
{
struct nf_nat_range2 range;
/* exp->saved_proto: 원래 협상된 RTP 포트 */
/* exp->tuple.dst.u.udp.port: NAT 이후 실제 포트 */
/* 미디어 채널에 DNAT 규칙 적용 (NAT 뒤 SIP 단말로 전달) */
memset(&range, 0, sizeof(range));
range.flags |= NF_NAT_RANGE_PROTO_SPECIFIED;
range.min_proto = range.max_proto = exp->saved_proto;
nf_nat_setup_info(ct, &range, NF_NAT_MANIP_DST);
/* SNAT: 미디어 패킷의 출발지 IP를 공인 IP로 변경 */
range.flags = NF_NAT_RANGE_MAP_IPS;
range.min_addr = range.max_addr = ct->tuplehash[!exp->dir].tuple.dst.u3;
nf_nat_setup_info(ct, &range, NF_NAT_MANIP_SRC);
}
SIP over TCP 처리
SIP는 UDP(포트 5060)와 TCP(포트 5060/5061) 양쪽을 지원합니다. TCP 위의 SIP는 여러 SIP 메시지가 하나의 TCP 스트림에 담길 수 있어 헬퍼가 세그먼트 경계를 처리해야 합니다.
/* SIP over TCP: 메시지 경계 처리 */
/* Content-Length 헤더로 SIP 메시지 크기를 파악하고
* TCP 세그먼트 경계를 넘는 SIP 메시지를 재조합 */
/* sip_help_tcp()는 sip_help_udp()와 달리:
* 1. TCP 스트림에서 Content-Length 파싱
* 2. 불완전한 SIP 메시지는 NF_ACCEPT 후 다음 패킷 대기
* 3. 완전한 SIP 메시지 수신 후 SDP 파싱 수행
*/
/* nftables로 TCP SIP에 헬퍼 연결 */
nft add table ip sip_alg
nft add chain ip sip_alg prerouting \
'{ type filter hook prerouting priority raw; }'
# TCP SIP (5060)
nft add rule ip sip_alg prerouting \
tcp dport 5060 ct helper set "sip"
# TLS SIP (5061) — 암호화로 파싱 불가, 헬퍼 미설정 권장
# UDP SIP (5060)
nft add rule ip sip_alg prerouting \
udp dport 5060 ct helper set "sip"
SIP B2BUA 시나리오와 ALG 충돌
B2BUA(Back-to-Back User Agent)는 Asterisk, FreeSWITCH 같은 SIP 프록시로, SIP 다이얼로그를 종단 간에 완전히 재생성합니다. 이 환경에서 SIP ALG는 이미 B2BUA가 처리한 SIP 헤더를 이중으로 재작성하여 문제를 발생시킵니다.
| 시나리오 | SIP ALG 영향 | 권장 조치 |
|---|---|---|
| B2BUA (Asterisk/FreeSWITCH) | Via/Contact 헤더 이중 재작성 → 라우팅 루프 | SIP ALG 완전 비활성화 |
| SIP 등록 서버 (kamailio) | REGISTER Contact 헤더 변조 → 등록 실패 | ALG 비활성화 + SIP 프록시에서 NAT 처리 |
| SRTP/SIPS (TLS) | 암호화로 SDP 파싱 불가 → RTP expect 미등록 | STUN/TURN 사용, ALG 불필요 |
| WebRTC (SIP over WebSocket) | WebSocket 페이로드 파싱 미지원 → 무동작 | ICE/STUN/TURN으로 NAT 처리 |
커스텀 conntrack 헬퍼 작성
표준 ALG로 지원되지 않는 사내 프로토콜이나 특수 목적 프로토콜에 대해 커널 모듈로 커스텀 conntrack 헬퍼를 작성할 수 있습니다.
커스텀 헬퍼 골격 코드
/* my_proto_helper.c — 커스텀 conntrack 헬퍼 예시
*
* 가정: TCP 포트 9999를 제어 채널로 사용하는 프로토콜
* "DATAPORT <port>\r\n" 명령으로 데이터 채널 포트를 협상함
*/
#include <linux/module.h>
#include <linux/netfilter.h>
#include <net/netfilter/nf_conntrack.h>
#include <net/netfilter/nf_conntrack_helper.h>
#include <net/netfilter/nf_conntrack_expect.h>
MODULE_LICENSE("GPL");
MODULE_AUTHOR("Example");
#define MY_PROTO_PORT 9999
#define MY_PROTO_NAME "myproto"
/* expect 정책: 최대 1개 연관 연결, 타임아웃 60초 */
static const struct nf_conntrack_expect_policy my_exp_policy = {
.max_expected = 1,
.timeout = 60,
.name = "myproto-data",
};
/* 헬퍼 콜백: 제어 채널 패킷마다 호출 */
static int my_helper_cb(struct sk_buff *skb,
unsigned int protoff,
struct nf_conn *ct,
enum ip_conntrack_info ctinfo)
{
struct nf_conntrack_expect *exp;
const struct tcphdr *th;
unsigned int dataoff;
const char *payload;
unsigned int paylen;
__be16 data_port;
int port;
/* 서버 → 클라이언트 방향 패킷만 처리 */
if (CTINFO2DIR(ctinfo) == IP_CT_DIR_ORIGINAL)
return NF_ACCEPT;
/* TCP 헤더 후 페이로드 위치 계산 */
th = skb_header_pointer(skb, protoff, sizeof(*th), NULL);
if (!th)
return NF_ACCEPT;
dataoff = protoff + th->doff * 4;
paylen = skb->len - dataoff;
if (paylen < 10)
return NF_ACCEPT;
payload = skb_header_pointer(skb, dataoff, paylen, NULL);
if (!payload)
return NF_ACCEPT;
/* "DATAPORT <port>" 파싱 */
if (strncmp(payload, "DATAPORT ", 9) != 0)
return NF_ACCEPT;
if (sscanf(payload + 9, "%d", &port) != 1)
return NF_ACCEPT;
if (port <= 0 || port > 65535)
return NF_ACCEPT;
data_port = htons(port);
/* expect 할당 및 초기화 */
exp = nf_ct_expect_alloc(ct);
if (!exp)
return NF_ACCEPT;
/* 클라이언트 IP, data_port로 expect 설정 */
nf_ct_expect_init(exp,
NF_CT_EXPECT_CLASS_DEFAULT,
nf_ct_l3num(ct),
/* 출발지: 임의 (마스크 = 0) */
NULL,
/* 목적지: 서버 IP */
&ct->tuplehash[IP_CT_DIR_REPLY].tuple.src.u3,
IPPROTO_TCP,
NULL,
&data_port);
/* expect 등록 */
if (nf_ct_expect_related(exp) != 0)
pr_debug("myproto: expect_related failed\n");
nf_ct_expect_put(exp);
return NF_ACCEPT;
}
/* 헬퍼 구조체 */
static struct nf_conntrack_helper my_helper __read_mostly = {
.name = MY_PROTO_NAME,
.me = THIS_MODULE,
.tuple.src.l3num = AF_INET,
.tuple.src.u.tcp.port = cpu_to_be16(MY_PROTO_PORT),
.tuple.dst.protonum = IPPROTO_TCP,
.expect_policy = &my_exp_policy,
.help = my_helper_cb,
};
static int __init my_helper_init(void)
{
/* nf_ct_helper_init()으로 tuple 초기화 후 등록 */
return nf_conntrack_helper_register(&my_helper);
}
static void __exit my_helper_fini(void)
{
nf_conntrack_helper_unregister(&my_helper);
}
module_init(my_helper_init);
module_exit(my_helper_fini);
Makefile 예시
="macro">obj-m := my_proto_helper.o
KDIR := /lib/modules/="fn">$(shell uname -r)/build
PWD := ="fn">$(shell pwd)
all:
="fn">$(MAKE) -C ="fn">$(KDIR) M=="fn">$(PWD) modules
clean:
="fn">$(MAKE) -C ="fn">$(KDIR) M=="fn">$(PWD) clean
헬퍼 빌드 및 테스트
# 빌드
make
# 로드 (nf_conntrack 의존성 자동 처리)
insmod my_proto_helper.ko
# 특정 트래픽에 헬퍼 연결
iptables -t raw -A PREROUTING -p tcp --dport 9999 -j CT --helper myproto
# 헬퍼 등록 확인
cat /proc/net/nf_conntrack | grep myproto
# 테스트 후 정리
iptables -t raw -D PREROUTING -p tcp --dport 9999 -j CT --helper myproto
rmmod my_proto_helper
헬퍼 내부 API 상세
커스텀 헬퍼 작성 시 사용하는 핵심 API를 단계별로 살펴봅니다.
/* 1. nf_ct_helper_ext_add(): 헬퍼 전용 확장 데이터 할당
* conntrack 엔트리에 헬퍼가 사용할 사적 데이터 공간을 추가 */
struct myproto_state *state;
state = nf_ct_helper_ext_add(ct, GFP_ATOMIC);
if (!state)
return NF_DROP;
state->sequence = 0; /* 프로토콜 상태 초기화 */
/* 2. nf_ct_expect_alloc(): expect 객체 할당
* 현재 conntrack에 연결된 expect 슬롯 할당 (refcount 포함) */
struct nf_conntrack_expect *exp = nf_ct_expect_alloc(ct);
if (!exp)
return NF_ACCEPT; /* 실패 시 패킷은 통과 */
/* 3. nf_ct_expect_init(): expect 5-튜플 설정
* Parameters:
* exp — expect 객체
* class — NF_CT_EXPECT_CLASS_DEFAULT (대부분의 헬퍼)
* family — AF_INET 또는 AF_INET6
* saddr — 예상 출발지 주소 (NULL = 마스크 전체)
* daddr — 예상 목적지 주소
* proto — IPPROTO_TCP, IPPROTO_UDP, IPPROTO_GRE 등
* sport — 예상 출발지 포트 (NULL = 임의)
* dport — 예상 목적지 포트 (필수) */
nf_ct_expect_init(exp,
NF_CT_EXPECT_CLASS_DEFAULT,
AF_INET,
NULL, /* saddr: 임의 */
&ct->tuplehash[IP_CT_DIR_REPLY].tuple.src.u3,
IPPROTO_TCP,
NULL, /* sport: 임의 */
&data_port); /* dport: 협상된 포트 */
/* 4. nf_ct_expect_related(): expect 해시 테이블에 등록
* Returns: 0 = 성공, -EBUSY = 이미 등록됨, -ENOMEM = 메모리 부족 */
int ret = nf_ct_expect_related(exp);
if (ret)
pr_warn("myproto: expect 등록 실패: %d\n", ret);
/* 5. nf_ct_expect_put(): expect refcount 감소
* 등록 성공 후 반드시 호출 (헬퍼의 참조 해제) */
nf_ct_expect_put(exp);
/* 6. nf_conntrack_helper_register(): 헬퍼 등록
* 반환: 0 = 성공, 음수 = 오류 (-EEXIST: 이름 중복) */
ret = nf_conntrack_helper_register(&my_helper);
/* 7. nf_conntrack_helper_unregister(): 헬퍼 등록 해제
* 모든 연결된 conntrack가 헬퍼 참조를 해제할 때까지 대기 */
nf_conntrack_helper_unregister(&my_helper);
IPv4/IPv6 듀얼스택 헬퍼 등록
/* IPv4와 IPv6 각각 별도 헬퍼 구조체 등록 */
static struct nf_conntrack_helper my_helpers[2] __read_mostly;
static int __init my_helper_init(void)
{
int ret;
/* IPv4 헬퍼 */
nf_ct_helper_init(&my_helpers[0], AF_INET, IPPROTO_TCP,
MY_PROTO_NAME, MY_PROTO_PORT,
0, /* priv_data_len: 없음 */
1, /* expect_class_max */
&my_exp_policy,
my_helper_cb);
/* IPv6 헬퍼 */
nf_ct_helper_init(&my_helpers[1], AF_INET6, IPPROTO_TCP,
MY_PROTO_NAME, MY_PROTO_PORT,
0, 1,
&my_exp_policy,
my_helper_cb);
ret = nf_conntrack_helpers_register(my_helpers, ARRAY_SIZE(my_helpers));
if (ret)
pr_err("myproto: 헬퍼 등록 실패: %d\n", ret);
return ret;
}
static void __exit my_helper_fini(void)
{
nf_conntrack_helpers_unregister(my_helpers, ARRAY_SIZE(my_helpers));
}
NFCT_HELPER 넷링크 인터페이스를 통해
사용자공간에서 conntrack 헬퍼를 구현할 수도 있습니다(nfct-helper 도구 참조).
복잡한 프로토콜 파싱을 커널 모듈 없이 처리할 수 있어 개발·디버깅이 편리합니다.
ctnetlink 이벤트 기반 동기화
ctnetlink는 Netfilter conntrack 정보를 사용자공간으로 전달하는 Netlink 인터페이스입니다. NFCT 이벤트를 구독해 conntrack 테이블 변경을 실시간으로 처리하거나, HA(고가용성) 환경에서 장비 간 conntrack 상태를 동기화하는 데 사용합니다.
ctnetlink 이벤트 종류
| 이벤트 | 상수 | 발생 시점 |
|---|---|---|
| NEW | IPCTNL_MSG_CT_NEW |
새 conntrack 엔트리 생성 시 |
| UPDATE | IPCTNL_MSG_CT_NEW + NLM_F_REPLACE |
상태 변경 (ESTABLISHED, CLOSE 등) |
| DESTROY | IPCTNL_MSG_CT_DELETE |
conntrack 엔트리 만료·삭제 시 |
| EXP_NEW | IPCTNL_MSG_EXP_NEW |
새 expect 엔트리 등록 시 |
| EXP_DESTROY | IPCTNL_MSG_EXP_DELETE |
expect 엔트리 만료·삭제 시 |
libnetfilter_conntrack을 이용한 이벤트 수신
/* ctnetlink 이벤트 수신 예시 (C, libnetfilter_conntrack 사용) */
#include <libnetfilter_conntrack/libnetfilter_conntrack.h>
static int event_cb(enum nf_conntrack_msg_type type,
struct nf_conntrack *ct, void *data)
{
char buf[1024];
nfct_snprintf(buf, sizeof(buf), ct, type, NFCT_O_DEFAULT, NFCT_OF_SHOW_LAYER3);
switch (type) {
case NFCT_T_NEW:
printf("[NEW] %s\n", buf);
break;
case NFCT_T_UPDATE:
printf("[UPDATE] %s\n", buf);
break;
case NFCT_T_DESTROY:
printf("[DESTROY] %s\n", buf);
break;
default:
break;
}
return NFCT_CB_CONTINUE;
}
int main(void)
{
struct nfct_handle *h = nfct_open(CONNTRACK,
NF_NETLINK_CONNTRACK_NEW |
NF_NETLINK_CONNTRACK_UPDATE |
NF_NETLINK_CONNTRACK_DESTROY);
if (!h) {
perror("nfct_open");
return 1;
}
nfct_callback_register(h, NFCT_T_ALL, event_cb, NULL);
/* 이벤트 루프 (블로킹) */
nfct_catch(h);
nfct_close(h);
return 0;
}
# 컴파일
gcc -o ct_events ct_events.c -lnetfilter_conntrack
# 실행 (root 권한 필요)
./ct_events
# conntrackd를 사용한 HA 동기화 (패키지 설치 후)
apt install conntrack conntrackd
# /etc/conntrackd/conntrackd.conf 설정 후:
conntrackd -d # 데몬 시작
conntrackd -s # 통계 확인
conntrackd를 이용한 HA 동기화
conntrackd는 ctnetlink 이벤트를 구독하고 네트워크를 통해 다른 장비의 conntrackd와 상태를 동기화합니다.
장비 장애 발생 시 페일오버 장비가 기존 연결 상태를 이어받아 세션 중단 없는 페일오버를 구현합니다.
# conntrackd.conf 핵심 설정 예시
Sync {
Mode FTFW { # Fault-Tolerant Failover Watchdog
ResendQueueSize 131072;
PurgeTimeout 5;
FaultZeroDelay on;
MaxAttempts 10;
CheckSum on;
}
UDP {
IPv4_address 192.168.1.1; # 로컬 동기화 주소
IPv4_Destination_Address 192.168.1.2; # 피어 주소
Port 3780;
Interface eth1;
Checksum on;
}
}
General {
Hashsize 32768;
MaxEntries 65535;
LogFile /var/log/conntrackd.log;
LockFile /var/lock/conntrack.lock;
UNIX {
Path /var/run/conntrackd.ctl;
Backlog 20;
}
}
클라우드/컨테이너 환경 이슈
클라우드 네이티브 환경(Kubernetes, AWS, GCP)에서는 ALG가 오히려 애플리케이션을 방해하는 경우가 많습니다. 다음은 주요 이슈와 권장 대응 방법입니다.
Kubernetes에서의 SIP ALG 문제
# 문제: kube-proxy의 iptables 규칙과 SIP ALG 충돌
# kube-proxy가 Service IP를 DNAT하면, SIP ALG가 잘못된 IP로
# Contact/Via 헤더를 재작성해 SIP 등록 실패
# 확인 방법
sysctl net.netfilter.nf_conntrack_helper
# 값이 1이면 자동 ALG 적용 중
# 해결: ALG 비활성화
sysctl -w net.netfilter.nf_conntrack_helper=0
# 영구 설정 (/etc/sysctl.d/99-conntrack.conf)
echo "net.netfilter.nf_conntrack_helper=0" >> /etc/sysctl.d/99-conntrack.conf
# Kubernetes DaemonSet으로 노드 전체 적용
# (securityContext.privileged: true 필요)
AWS/GCP ALG 정책
| 클라우드 환경 | ALG 정책 | 권장 대응 |
|---|---|---|
| AWS EC2 | Security Group은 ALG 미지원. RELATED 상태 개념 없음 | FTP PASV 포트 범위를 Security Group에 직접 명시 |
| AWS NLB | L4 로드밸런서 — ALG 불가 | FTP 서버를 고정 PASV 포트 범위로 설정 |
| GCP VPC 방화벽 | Stateless 규칙 기반 — ALG 미지원 | 허용 포트를 방화벽 규칙에 명시 |
| 온프레미스 Linux | nf_conntrack_helper 활용 가능 | nf_conntrack_helper=0 + 명시적 CT 연결 권장 |
| 컨테이너 (Docker) | 호스트 nf_conntrack_helper 설정 상속 | 호스트에서 비활성화 후 컨테이너별 명시 설정 |
ALG 활성화/비활성화 비교
| 항목 | ALG 활성화 | ALG 비활성화 |
|---|---|---|
| FTP PASV | 자동 허용 (데이터 채널) | PASV 포트 범위를 방화벽에 명시해야 함 |
| SIP/VoIP | 자동 RTP 허용 (단, 헤더 변조 위험) | STUN/TURN 또는 SIP 프록시로 처리 |
| 보안 | RELATED 자동 허용 — 공격 벡터 가능 | 명시적 규칙만 허용 — 더 안전 |
| 성능 | 페이로드 파싱 오버헤드 (소규모 영향) | 페이로드 파싱 불필요 — 약간 빠름 |
| 암호화 트래픽 | TLS/SRTP 페이로드 파싱 불가 — 무용지물 | 해당 없음 (애플리케이션 레벨에서 처리) |
| 클라우드 호환성 | 클라우드 로드밸런서와 충돌 가능 | 클라우드 환경 친화적 |
컨테이너 환경 conntrack 최적화
# Kubernetes 노드 conntrack 설정
# /etc/sysctl.d/99-kubernetes-conntrack.conf
# 1. ALG 자동 로드 비활성화
net.netfilter.nf_conntrack_helper = 0
# 2. conntrack 테이블 크기 확대 (대규모 클러스터)
net.netfilter.nf_conntrack_max = 1048576
net.nf_conntrack_max = 1048576
# 3. TCP/UDP 타임아웃 조정
net.netfilter.nf_conntrack_tcp_timeout_established = 86400
net.netfilter.nf_conntrack_udp_timeout = 30
net.netfilter.nf_conntrack_udp_timeout_stream = 60
# 4. expect 타임아웃 축소 (expect 테이블 낭비 방지)
net.netfilter.nf_conntrack_expect_max = 1024
# 적용
sysctl --system
nf_conntrack: table full, dropping packet 오류와 함께 드롭됩니다.
conntrack -C로 현재 사용량을 모니터링하고 nf_conntrack_max를 충분히 설정하세요.
H.323 / RTSP / Amanda 헬퍼 심화
H.323·RTSP·Amanda는 FTP/SIP보다 더 복잡한 다중 채널 구조를 가지며, 각 헬퍼는 제어 채널에서 여러 단계의 협상을 거쳐 미디어 채널 포트를 결정합니다.
H.323: 다중 채널 협상 구조
H.323은 ITU-T 표준 화상회의 프로토콜로, 단일 연결이 아닌 여러 단계의 채널 협상을 통해 미디어를 전송합니다.
nf_conntrack_h323.c는 이 복잡한 구조를 처리하기 위해 ASN.1 PER 인코딩을 파싱합니다.
| 채널 | 프로토콜/포트 | 역할 | 헬퍼 함수 |
|---|---|---|---|
| RAS (Registration, Admission, Status) | UDP 1719 | 게이트키퍼 등록·승인·상태 관리 | expect_q931()에서 RAS expect 등록 |
| Q.931 (Call Signaling) | TCP 1720 | H.323 호 설정·해제 (TPKT 캡슐화) | h323_help() → expect_h245() |
| H.245 (Control) | TCP 동적 포트 | 미디어 능력 교환·채널 개방 협상 | h245_help() → RTP/RTCP expect |
| RTP 미디어 | UDP 동적 포트 | 오디오/비디오 스트림 전송 | H.245 OpenLogicalChannel 파싱 후 등록 |
| RTCP 제어 | UDP (RTP 포트+1) | RTP 품질 모니터링·통계 | RTP expect와 함께 자동 등록 |
/* net/netfilter/nf_conntrack_h323.c — 핵심 함수 흐름 */
/* 1단계: Q.931 제어 채널(TCP 1720) 감시 */
static int h323_help(struct sk_buff *skb, unsigned int protoff,
struct nf_conn *ct, enum ip_conntrack_info ctinfo)
{
/* TPKT 헤더 파싱 (H.323 메시지 경계 구분) */
/* TPKT: 0x03 0x00 length(2) data... */
/* H.225.0 Q.931 메시지 파싱 (ASN.1 PER 인코딩) */
/* SETUP 메시지에서 H.245 주소 추출 */
/* h245Addr: TransportAddress (IP + 포트) */
return expect_h245(skb, ct, ctinfo, protoff, &h245_addr);
}
/* 2단계: H.245 포트에 대한 expect 등록 */
static int expect_h245(struct sk_buff *skb, struct nf_conn *ct,
enum ip_conntrack_info ctinfo,
unsigned int protoff,
struct nf_conntrack_man *exp_addr)
{
struct nf_conntrack_expect *exp;
exp = nf_ct_expect_alloc(ct);
/* H.245 TCP 포트 expect 등록 */
nf_ct_expect_init(exp, NF_CT_EXPECT_CLASS_DEFAULT,
nf_ct_l3num(ct),
NULL, &exp_addr->u3,
IPPROTO_TCP, NULL, &exp_addr->u.tcp.port);
/* H.245 연결 수립 시 호출될 콜백 등록 */
exp->expectfn = nat_h245; /* NAT 환경에서 포트 재작성 */
nf_ct_expect_related(exp);
nf_ct_expect_put(exp);
return NF_ACCEPT;
}
/* 3단계: H.245 채널에서 RTP/RTCP 포트 협상 감시 */
static int h245_help(struct sk_buff *skb, unsigned int protoff,
struct nf_conn *ct, enum ip_conntrack_info ctinfo)
{
/* OpenLogicalChannel 메시지 파싱 */
/* mediaChannel: 미디어 전송 주소 (UDP 포트) */
/* mediaControlChannel: RTCP 제어 주소 (UDP 포트) */
/* RTP expect 등록 */
expect_rtp_rtcp(skb, ct, ctinfo, protoff, &rtp_addr, &rtcp_addr);
return NF_ACCEPT;
}
RTSP 헬퍼 동작
RTSP(Real Time Streaming Protocol, RFC 2326)는 미디어 스트리밍 제어 프로토콜입니다.
nf_conntrack_rtsp.c는 RTSP SETUP 메시지의 Transport 헤더를 파싱해 RTP/RTCP 채널 포트를 추출합니다.
/* RTSP SETUP 메시지 예시:
* SETUP rtsp://server/stream/track1 RTSP/1.0
* CSeq: 3
* Transport: RTP/AVP;unicast;client_port=49170-49171
* ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
* 클라이언트가 수신할 RTP(49170)/RTCP(49171) 포트
*
* 서버 응답:
* RTSP/1.0 200 OK
* Transport: RTP/AVP;unicast;client_port=49170-49171;server_port=6970-6971
* ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
* 서버가 전송할 RTP(6970)/RTCP(6971) 포트
*/
/* rtsp_parse_transport() — Transport 헤더 파싱 */
static int rtsp_parse_transport(const char *value, size_t len,
struct rtsp_session *sess)
{
/* "client_port=NNNN-MMMM" 파싱 */
/* RTP 포트: NNNN, RTCP 포트: MMMM (= NNNN+1) */
/* 서버 측 RTP expect 등록: client_port로 들어오는 패킷 허용 */
nf_ct_expect_init(exp_rtp, ..., IPPROTO_UDP, NULL, &client_rtp_port);
nf_ct_expect_related(exp_rtp);
/* 서버 측 RTCP expect 등록 */
nf_ct_expect_init(exp_rtcp, ..., IPPROTO_UDP, NULL, &client_rtcp_port);
nf_ct_expect_related(exp_rtcp);
}
Amanda 헬퍼 동작
Amanda(Advanced Maryland Automatic Network Disk Archiver)는 네트워크 백업 소프트웨어로, UDP 10080(amandaidx)으로 인덱스 서버에 연결하고, 실제 백업 데이터는 동적 TCP 포트를 협상합니다.
/* Amanda 프로토콜 협상 (단순 텍스트 기반):
* 클라이언트 → 서버 (UDP 10080):
* "amanda 2 DATA 192.168.1.100 49152 INDEX 192.168.1.100 49153"
* DATA 포트: 백업 데이터 전송
* INDEX 포트: 파일 인덱스 전송
*/
/* nf_conntrack_amanda.c — 포트 파싱 */
static int amanda_help(struct sk_buff *skb, ...)
{
/* "DATA" 토큰 뒤의 IP:PORT 파싱 */
/* "INDEX" 토큰 뒤의 IP:PORT 파싱 */
/* 각각 TCP expect 등록 */
nf_ct_expect_init(exp_data, ..., IPPROTO_TCP, NULL, &data_port);
nf_ct_expect_related(exp_data);
nf_ct_expect_init(exp_index, ..., IPPROTO_TCP, NULL, &index_port);
nf_ct_expect_related(exp_index);
}
헬퍼 특성 비교 표
| 헬퍼 | 제어 채널 | 데이터 채널 수 | 포트 개방 패턴 | 복잡도 | NAT 지원 |
|---|---|---|---|---|---|
| FTP | TCP 21 | 1 (데이터) | PORT/PASV 명령 파싱 | 낮음 | nf_nat_ftp.c |
| SIP | UDP/TCP 5060 | 2 (RTP + RTCP) | SDP m= 라인 파싱 | 중간 | nf_nat_sip.c |
| H.323 | TCP 1720 + UDP 1719 | 3+ (H.245 + RTP + RTCP) | ASN.1 PER 다단계 파싱 | 매우 높음 | nf_nat_h323.c |
| RTSP | TCP 554 | 2 (RTP + RTCP) | Transport 헤더 파싱 | 중간 | nf_nat_rtsp.c |
| Amanda | UDP 10080 | 2 (DATA + INDEX) | 텍스트 토큰 파싱 | 낮음 | 없음 |
| PPTP | TCP 1723 | 1 (GRE 프로토콜) | Call ID 기반 GRE expect | 중간 | nf_nat_pptp.c |
PPTP/GRE 헬퍼와 VPN 통과
PPTP(Point-to-Point Tunneling Protocol, RFC 2637)는 구형 VPN 프로토콜로,
TCP 1723의 제어 채널과 GRE(프로토콜 번호 47) 데이터 채널의 두 채널로 구성됩니다.
nf_conntrack_pptp.c는 PPTP 제어 채널을 파싱해 GRE 채널에 대한 expect를 등록합니다.
PPTP 제어 채널 파싱
/* PPTP 메시지 구조 (RFC 2637)
* 모든 PPTP 메시지는 TCP 1723으로 전송
*
* PPTP Start-Control-Connection-Request (SCCRQ)
* PPTP Start-Control-Connection-Reply (SCCRP)
* PPTP Outgoing-Call-Request (OCRQ): 클라이언트 Call ID
* PPTP Outgoing-Call-Reply (OCRP): 서버 Peer Call ID
* PPTP Incoming-Call-Request (ICRQ): 서버 Call ID
*/
/* pptp_outbound_pkt(): 클라이언트→서버 패킷 처리 */
static int pptp_outbound_pkt(struct sk_buff *skb, struct nf_conn *ct,
enum ip_conntrack_info ctinfo,
struct PptpControlHeader *ctlh,
union pptp_ctrl_union *pptpReq)
{
struct nf_ct_pptp_master *info = nfct_help_data(ct);
__be16 msg_type = ctlh->messageType;
switch (msg_type) {
case PPTP_OUT_CALL_REQUEST:
/* 클라이언트 Call ID 저장 */
info->pns_call_id = pptpReq->ocreq.callID;
/* GRE 데이터 채널 expect 등록 준비 */
break;
}
return NF_ACCEPT;
}
/* pptp_inbound_pkt(): 서버→클라이언트 패킷 처리 */
static int pptp_inbound_pkt(struct sk_buff *skb, struct nf_conn *ct,
enum ip_conntrack_info ctinfo,
struct PptpControlHeader *ctlh,
union pptp_ctrl_union *pptpReq)
{
struct nf_ct_pptp_master *info = nfct_help_data(ct);
switch (ctlh->messageType) {
case PPTP_OUT_CALL_REPLY:
/* 서버 Peer Call ID 획득 */
info->pac_call_id = pptpReq->ocrep.peersCallID;
/* GRE 데이터 채널 expect 등록:
* GRE 프로토콜 (47), Call ID 기반 식별 */
exp_orig = nf_ct_expect_alloc(ct);
nf_ct_expect_init(exp_orig, NF_CT_EXPECT_CLASS_DEFAULT,
nf_ct_l3num(ct),
&ct->tuplehash[IP_CT_DIR_REPLY].tuple.src.u3,
&ct->tuplehash[IP_CT_DIR_REPLY].tuple.dst.u3,
IPPROTO_GRE,
&info->pac_call_id, /* GRE key = Call ID */
&info->pns_call_id);
nf_ct_expect_related(exp_orig);
/* 역방향 GRE expect (서버→클라이언트) */
exp_reply = nf_ct_expect_alloc(ct);
/* ... 역방향 설정 ... */
nf_ct_expect_related(exp_reply);
break;
}
return NF_ACCEPT;
}
GRE 특수 처리: nf_conntrack_gre.c
GRE는 포트 번호가 없는 프로토콜이므로, nf_conntrack_gre.c는 GRE 헤더의 Key 필드(Call ID)를
포트 번호처럼 사용해 연결을 구분합니다.
/* net/netfilter/nf_conntrack_proto_gre.c */
/* GRE 4-튜플: (src_ip, dst_ip, src_callid, dst_callid) */
static bool gre_pkt_to_tuple(const struct sk_buff *skb, unsigned int dataoff,
struct net *net, struct nf_conntrack_tuple *tuple)
{
const struct gre_hdr_pptp *pgrehdr;
pgrehdr = skb_header_pointer(skb, dataoff, sizeof(*pgrehdr), &_pgrehdr);
/* GRE Key 필드 = PPTP Call ID */
tuple->src.u.gre.key = pgrehdr->callid; /* 클라이언트 Call ID */
tuple->dst.u.gre.key = 0; /* 초기에는 0, expect에서 설정 */
return true;
}
/* GRE conntrack 타임아웃 (기본값) */
static const unsigned int gre_timeouts[GRE_CT_MAX] = {
[GRE_CT_UNREPLIED] = 30 * HZ, /* 30초: 응답 없는 GRE */
[GRE_CT_REPLIED] = 180 * HZ, /* 3분: 응답 있는 GRE 세션 */
};
NAT + PPTP: nf_nat_pptp.c의 Call ID 재작성
NAT 환경에서 여러 클라이언트가 동일한 PPTP 서버에 연결할 때, Call ID가 충돌하지 않도록
nf_nat_pptp.c가 Call ID를 재작성합니다.
/* nf_nat_pptp.c — Call ID 재작성 */
static void pptp_nat_expected(struct nf_conn *ct,
struct nf_conntrack_expect *exp)
{
/* 새 GRE 연결의 Call ID를 NAT 후 값으로 수정 */
/* PPTP 제어 채널의 페이로드(Outgoing-Call-Request)도 수정 */
/* GRE Key 필드 = 재할당된 Call ID로 SNAT */
range.flags = NF_NAT_RANGE_PROTO_SPECIFIED;
range.min_proto.gre.key = range.max_proto.gre.key = new_callid;
nf_nat_setup_info(ct, &range, NF_NAT_MANIP_SRC);
}
PPTP 보안 취약성과 대안
| VPN 프로토콜 | 보안 강도 | conntrack 헬퍼 | 현재 권고 |
|---|---|---|---|
| PPTP | 취약 (MS-CHAPv2 크랙 가능, GRE 평문) | nf_conntrack_pptp + nf_conntrack_gre | 사용 금지 (RFC 2637 구형) |
| L2TP/IPsec | 양호 (IPsec ESP 암호화) | 헬퍼 불필요 (UDP 500/4500 고정) | 레거시 환경에서 사용 가능 |
| WireGuard | 우수 (ChaCha20-Poly1305, Curve25519) | 불필요 (UDP 51820 단일 포트) | 현대 환경 권장 |
| OpenVPN | 양호 (TLS 1.3) | 불필요 (UDP/TCP 1194 단일 포트) | 광범위 호환성 필요 시 사용 |
| IPsec/XFRM | 우수 (AES-GCM, PFS) | 불필요 (ESP 프로토콜 47, UDP 500/4500) | 기업 환경 표준 |
# PPTP 헬퍼 비활성화 (클라우드/보안 환경 권장)
# PPTP 헬퍼 모듈 차단
echo "install nf_conntrack_pptp /bin/true" >> /etc/modprobe.d/disable-pptp.conf
echo "install nf_nat_pptp /bin/true" >> /etc/modprobe.d/disable-pptp.conf
echo "install nf_conntrack_gre /bin/true" >> /etc/modprobe.d/disable-pptp.conf
# 이미 로드된 경우 언로드
rmmod nf_nat_pptp
rmmod nf_conntrack_pptp
rmmod nf_conntrack_gre
# PPTP 대신 WireGuard 설정 (헬퍼 불필요, UDP 51820만 허용)
iptables -A INPUT -p udp --dport 51820 -j ACCEPT
iptables -A OUTPUT -p udp --sport 51820 -j ACCEPT
# RELATED 규칙 불필요 — WireGuard는 단일 UDP 포트 사용
nf_conntrack_pptp 모듈은 클라우드·기업 환경에서 로드하지 않는 것을 강력히 권장합니다.
ALG/헬퍼 보안 이슈
conntrack 헬퍼는 강력한 기능을 제공하지만, 페이로드를 파싱해 방화벽 규칙을 자동으로 우회시키는 특성상
다수의 보안 취약점이 발견되었습니다. 자동 헬퍼 할당(nf_conntrack_helper=1)은 특히 위험합니다.
주요 CVE 취약점
| CVE | 모듈 | 취약점 유형 | 영향 | 수정 버전 |
|---|---|---|---|---|
| CVE-2014-8160 | nf_conntrack_sip | 자동 헬퍼 할당으로 임의 포트 개방 | 방화벽 규칙 우회 — 공격자가 임의 포트를 RELATED로 열 수 있음 | 3.18.x |
| CVE-2020-14305 | nf_conntrack_h323 | out-of-bounds write (ASN.1 파싱) | 커널 패닉 또는 권한 상승 가능 | 5.10.x |
| CVE-2021-3773 | nf_conntrack_ftp | FTP 페이로드 조작으로 RELATED 허용 우회 | 방화벽 규칙을 우회해 임의 연결을 RELATED로 위장 | 5.15.x |
| CVE-2019-19922 | nf_conntrack_sip | SIP 메시지 조작 → 잘못된 RTP expect 등록 | 공격자 제어 포트가 RELATED로 허용 | 5.4.x |
자동 헬퍼 할당 보안 문제
/* 취약 시나리오: nf_conntrack_helper=1 (구형 기본값)
*
* 공격자가 방화벽 내부 호스트와 통신하면서
* SIP 포트(5060)로 조작된 SDP 메시지를 전송:
*
* 조작된 SDP:
* m=audio 22 RTP/AVP 0 ← 포트 22 (SSH)를 RTP 포트로 위장
*
* 결과: 방화벽이 포트 22를 RELATED로 자동 허용
* → SSH 방화벽 규칙 우회 가능
*/
# 위험한 설정 (사용 금지)
sysctl -w net.netfilter.nf_conntrack_helper=1 # 자동 헬퍼 할당
# 안전한 설정 (커널 4.7+ 기본값)
sysctl -w net.netfilter.nf_conntrack_helper=0 # 수동 헬퍼 할당만 허용
수동 헬퍼 할당: nftables 방식 (권장)
#!/usr/sbin/nft -f
# /etc/nftables.d/alg-helpers.conf
# 안전한 수동 헬퍼 할당 (특정 IP/포트에만 적용)
table ip alg_helpers {
# 헬퍼 객체 정의
ct helper ftp_helper {
type "ftp" protocol tcp;
l3proto ip;
}
ct helper sip_helper {
type "sip" protocol udp;
l3proto ip;
}
chain prerouting {
type filter hook prerouting priority raw; policy accept;
# FTP 서버 트래픽에만 FTP 헬퍼 적용 (특정 서버 IP 지정 권장)
ip daddr 10.0.0.10 tcp dport 21 ct helper set "ftp_helper"
# SIP 트래픽에만 SIP 헬퍼 적용 (신뢰할 수 있는 SIP 프록시만)
ip saddr 10.0.0.20 udp dport 5060 ct helper set "sip_helper"
# 나머지는 헬퍼 미적용 (자동 허용 없음)
}
}
헬퍼별 최대 expect 수 제한
/* nf_conntrack_sip.c — 동시 대화 최대 수 제한 */
static unsigned int sip_max_pending_dialogs __read_mostly = 8;
module_param(sip_max_pending_dialogs, uint, 0644);
/* 각 헬퍼의 expect_policy.max_expected:
* FTP: max_expected = 1 (데이터 채널 1개)
* SIP: max_expected = 8 (동시 통화 수, sip_max_pending_dialogs)
* H.323: max_expected = 4 (H.245 + RTP + RTCP × 2)
* RTSP: max_expected = 2 (RTP + RTCP)
*/
/* 제한 이유: expect 테이블 고갈 방지 + DoS 완화 */
/* 공격자가 대량의 SIP INVITE를 전송해 expect 테이블을 가득 채우는 공격 방어 */
# SIP 동시 대화 수 제한 (모듈 로드 시)
modprobe nf_conntrack_sip sip_max_pending_dialogs=4
NGFW에서 ALG 비활성화 전략
# 단계적 ALG 비활성화 전략
# 1단계: ALG 자동 로드 비활성화 (즉시 적용)
sysctl -w net.netfilter.nf_conntrack_helper=0
# 2단계: 불필요한 헬퍼 모듈 블랙리스트 등록
cat >> /etc/modprobe.d/conntrack-security.conf << 'EOF'
# PPTP — 보안 취약, 사용 금지
install nf_conntrack_pptp /bin/true
install nf_nat_pptp /bin/true
install nf_conntrack_gre /bin/true
# H.323 — CVE-2020-14305 이전 커널에서 특히 위험
# install nf_conntrack_h323 /bin/true # 필요 시 활성화
# Amanda — 백업 트래픽이 없는 환경
install nf_conntrack_amanda /bin/true
# IRC DCC — 레거시, 보안 환경에서 불필요
install nf_conntrack_irc /bin/true
EOF
# 3단계: 필요한 헬퍼만 명시적 할당 (위 nftables 설정 적용)
# 4단계: 헬퍼 없이 PASV 포트 범위 직접 허용 (FTP 대안)
nft add rule ip filter forward tcp dport 50000-51000 accept
SELinux/AppArmor와 헬퍼 연동
# SELinux에서 conntrack 헬퍼 관련 정책 확인
# conntrack 헬퍼는 커널 모듈이므로 SELinux module_t 도메인
# AVC 거부 로그 확인 (conntrack 관련)
ausearch -m AVC -ts recent | grep conntrack
# conntrack 관련 SELinux 부울 확인
getsebool -a | grep nf_
# nf_conntrack_ftp_ports -- on/off
# FTP ALG 특정 포트 허용 (SELinux 정책)
semanage port -a -t ftp_data_port_t -p tcp 50000-51000
# AppArmor: nf_conntrack 모듈 로드 권한
# /etc/apparmor.d/abstractions/base 에서
# capability net_admin 필요 (modprobe 실행 시)
타임아웃 정책 심화
conntrack 엔트리의 타임아웃은 연결 추적 테이블의 메모리 사용량과 세션 지속성을 결정하는 핵심 파라미터입니다.
프로토콜과 상태별로 다른 기본값이 설정되어 있으며, nftables의 ct timeout 객체로 세밀하게 커스터마이즈할 수 있습니다.
프로토콜별 기본 타임아웃
| 프로토콜 | 상태 | 기본 타임아웃 | sysctl 키 |
|---|---|---|---|
| TCP | SYN_SENT | 120초 | nf_conntrack_tcp_timeout_syn_sent |
| SYN_RECV | 60초 | nf_conntrack_tcp_timeout_syn_recv |
|
| ESTABLISHED | 432000초 (5일) | nf_conntrack_tcp_timeout_established |
|
| FIN_WAIT | 120초 | nf_conntrack_tcp_timeout_fin_wait |
|
| CLOSE_WAIT | 60초 | nf_conntrack_tcp_timeout_close_wait |
|
| LAST_ACK | 30초 | nf_conntrack_tcp_timeout_last_ack |
|
| TIME_WAIT | 120초 | nf_conntrack_tcp_timeout_time_wait |
|
| CLOSE | 10초 | nf_conntrack_tcp_timeout_close |
|
| UDP | UNREPLIED | 30초 | nf_conntrack_udp_timeout |
| ASSURED (양방향) | 180초 | nf_conntrack_udp_timeout_stream |
|
| ICMP | 기본 | 30초 | nf_conntrack_icmp_timeout |
| ICMPv6 | 30초 | nf_conntrack_icmpv6_timeout |
|
| GRE | UNREPLIED | 30초 | nf_conntrack_gre_timeout |
| REPLIED | 180초 | nf_conntrack_gre_timeout_stream |
nftables ct timeout 객체로 커스텀 정책 설정
#!/usr/sbin/nft -f
# /etc/nftables.d/ct-timeout.conf
# 서비스별 conntrack 타임아웃 커스터마이즈
table ip ct_timeouts {
# 1. 일반 웹 서비스용 타임아웃 (기본보다 짧게)
ct timeout web_timeout {
protocol tcp;
l3proto ip;
policy = {
established: 1800, # 30분 (기본 5일 → 30분으로 단축)
fin_wait: 30,
close_wait: 15,
last_ack: 15,
time_wait: 30,
close: 5,
}
}
# 2. 대용량 파일 전송(FTP/SFTP)용 타임아웃 (길게)
ct timeout bulk_transfer_timeout {
protocol tcp;
l3proto ip;
policy = {
established: 86400, # 24시간 (대용량 전송 중단 방지)
syn_sent: 300, # 5분
syn_recv: 120,
fin_wait: 300,
close_wait: 120,
last_ack: 60,
time_wait: 240,
}
}
# 3. 실시간 스트리밍용 UDP 타임아웃
ct timeout streaming_udp_timeout {
protocol udp;
l3proto ip;
policy = {
unreplied: 10, # 10초 (응답 없는 UDP 빠르게 제거)
replied: 30, # 30초 (RTP 스트림: 짧게 설정)
}
}
# 4. DNS 쿼리용 초단시간 타임아웃
ct timeout dns_timeout {
protocol udp;
l3proto ip;
policy = {
unreplied: 5,
replied: 10,
}
}
chain prerouting {
type filter hook prerouting priority raw; policy accept;
# 웹 트래픽에 web_timeout 적용
tcp dport { 80, 443 } ct timeout set "web_timeout"
# FTP 제어/데이터 채널에 bulk_transfer_timeout 적용
tcp dport { 20, 21 } ct timeout set "bulk_transfer_timeout"
tcp sport { 20, 21 } ct timeout set "bulk_transfer_timeout"
# RTP 포트 범위에 streaming 타임아웃 적용
udp dport 10000-20000 ct timeout set "streaming_udp_timeout"
# DNS 타임아웃
udp dport 53 ct timeout set "dns_timeout"
}
}
헬퍼 연결의 expect 타임아웃
nf_ct_expect_set_timeout()으로 각 expect 엔트리의 만료 시간을 개별 설정할 수 있습니다.
기본 타임아웃은 헬퍼의 expect_policy.timeout 필드에서 결정됩니다.
/* expect 타임아웃 기본값 (각 헬퍼의 expect_policy.timeout 필드) */
/* FTP: 60초 — PASV 응답 후 60초 내 데이터 연결 없으면 제거 */
/* SIP: 181초 — SIP 다이얼로그 타이머와 맞춤 */
/* H.323: 300초 — H.245 협상 완료까지 여유 시간 */
/* RTSP: 300초 — SETUP 후 PLAY까지 허용 시간 */
/* PPTP: 30초 — GRE 채널 expect 빠른 만료 */
/* 커스텀 헬퍼에서 타임아웃 동적 조정 */
static int my_helper_cb(struct sk_buff *skb, ...)
{
struct nf_conntrack_expect *exp = nf_ct_expect_alloc(ct);
/* ... expect 초기화 ... */
/* 타임아웃을 패킷 내용에 따라 동적 설정 */
unsigned int timeout = 120; /* 기본 2분 */
if (is_large_transfer)
timeout = 600; /* 대용량 전송 시 10분 */
exp->timeout.expires = jiffies + timeout * HZ;
nf_ct_expect_related(exp);
nf_ct_expect_put(exp);
return NF_ACCEPT;
}
타임아웃 만료 후 conntrack GC 동작
/* 타임아웃 만료 시 커널 내부 동작 흐름:
*
* 1. 타이머 만료: death_by_timeout() 호출
* 2. conntrack 상태를 "dying" 마크
* 3. ctnetlink DESTROY 이벤트 발생 → conntrackd에 통보
* 4. 해시 테이블에서 제거 → GC 큐에 추가
* 5. nf_ct_gc_worker() 주기적 실행으로 메모리 해제
*
* GC 설정:
*/
# GC 개입 임계치 (conntrack 테이블이 이 비율 이상 채워지면 GC 강제 실행)
cat /proc/sys/net/netfilter/nf_conntrack_buckets
# 고부하 환경 최적화 sysctl 설정
# /etc/sysctl.d/99-conntrack-tuning.conf
# TCP ESTABLISHED 타임아웃: 5일 → 1시간 (단기 연결 중심 환경)
net.netfilter.nf_conntrack_tcp_timeout_established = 3600
# TCP TIME_WAIT 단축 (TIME_WAIT 엔트리 빠른 재활용)
net.netfilter.nf_conntrack_tcp_timeout_time_wait = 30
# UDP 타임아웃 단축 (UDP 기반 서비스가 없는 환경)
net.netfilter.nf_conntrack_udp_timeout = 15
net.netfilter.nf_conntrack_udp_timeout_stream = 60
# conntrack 최대 버킷 수 증가 (해시 충돌 감소)
# 주의: nf_conntrack_buckets는 부팅 시에만 변경 가능 (또는 모듈 파라미터)
# modprobe nf_conntrack hashsize=131072
# conntrack 최대 엔트리 수 (hashsize의 8배 권장)
net.netfilter.nf_conntrack_max = 1048576
ctnetlink 기반 상태 동기화 실전
conntrackd는 ctnetlink 이벤트를 구독하고 Primary/Backup 서버 간 conntrack 상태를 동기화하는 데몬입니다.
Pacemaker 같은 클러스터 매니저와 연동해 세션 중단 없는 장애 복구(Stateful HA Failover)를 구현합니다.
conntrackd 아키텍처
/* conntrackd 내부 구조 (사용자공간 데몬):
*
* [Primary NGFW] [Backup NGFW]
* ┌─────────────────────────────┐ ┌─────────────────────────────┐
* │ conntrackd (Primary) │ │ conntrackd (Backup) │
* │ ┌────────────────────────┐ │ │ ┌────────────────────────┐ │
* │ │ ctnetlink 이벤트 수신 │ │ │ │ ctnetlink 이벤트 수신 │ │
* │ │ (NFCT_T_NEW/UPD/DEL) │ │ │ │ (로컬 상태 갱신) │ │
* │ └──────────┬─────────────┘ │ │ └──────────┬─────────────┘ │
* │ ┌──────────▼─────────────┐ │ │ ┌──────────▼─────────────┐ │
* │ │ State Sync Engine │◄───►│ │ State Sync Engine │ │
* │ │ (FTFW 프로토콜) │ │ │ │ (수신 + 로컬 CT 갱신) │ │
* │ └────────────────────────┘ │ │ └────────────────────────┘ │
* └─────────────────────────────┘ └─────────────────────────────┘
* │ UDP 멀티캐스트/유니캐스트 동기화 채널 │
* └──────────────────────────────────────────┘
*/
conntrackd.conf 완전 예제
# /etc/conntrackd/conntrackd.conf
# Primary NGFW 설정 (Backup은 IPv4_address ↔ IPv4_Destination_Address 교환)
Sync {
# FTFW: Fault-Tolerant Failover Watchdog — 패킷 손실 허용, 재전송 지원
Mode FTFW {
ResendQueueSize 131072; # 재전송 큐 크기 (패킷 단위)
PurgeTimeout 5; # 확인 안 된 엔트리 제거 대기 (초)
FaultZeroDelay on; # 장애 감지 즉시 백업으로 전환
MaxAttempts 10; # 최대 재전송 시도 횟수
CheckSum on; # UDP 동기화 패킷 체크섬 검증
}
# UNICAST 동기화 (멀티캐스트 불가 환경)
UDP {
IPv4_address 192.168.100.1; # 로컬 동기화 인터페이스 IP
IPv4_Destination_Address 192.168.100.2; # Backup 서버 IP
Port 3780; # 동기화 포트 (방화벽 허용 필요)
Interface bond0; # 동기화 전용 인터페이스
Checksum on;
SndSocketBuffer 1249280; # 송신 소켓 버퍼 크기
RcvSocketBuffer 1249280; # 수신 소켓 버퍼 크기
}
# 또는 MULTICAST 동기화 (동일 L2 세그먼트)
# Multicast {
# IPv4_address 225.0.0.50;
# Group 3780;
# IPv4_interface 192.168.100.1;
# Interface bond0;
# Checksum on;
# }
}
General {
Hashsize 32768; # 내부 해시 테이블 크기
MaxEntries 65535; # 최대 동기화 엔트리 수
LogFile /var/log/conntrackd.log;
Syslog yes;
LockFile /var/lock/conntrack.lock;
StripNAT off; # NAT 정보도 동기화 (on이면 제거)
UNIX {
Path /var/run/conntrackd.ctl;
Backlog 20;
}
# 동기화 필터: 불필요한 프로토콜 무시
Filter From Kernelspace {
# ICMP는 동기화 제외 (짧은 수명, 동기화 불필요)
Protocol Accept {
TCP;
UDP;
# GRE; # PPTP 환경에서는 포함
}
# FTP 데이터 채널(포트 20)도 동기화 제외
# (헬퍼 정보는 어차피 동기화 불가)
Address Ignore {
IPv4_address 127.0.0.1; # 루프백 제외
IPv4_address 169.254.0.0/16; # link-local 제외
}
}
}
NFCT 이벤트 처리 흐름
/* conntrackd가 처리하는 ctnetlink 이벤트 */
/* NFCT_T_NEW: 새 conntrack 엔트리 생성 */
/* → Backup에 ADD 메시지 전송 → Backup이 로컬 CT 테이블에 추가 */
/* NFCT_T_UPDATE: 상태 변경 (SYN_RECV → ESTABLISHED 등) */
/* → Backup에 UPDATE 메시지 전송 → Backup이 로컬 엔트리 업데이트 */
/* NFCT_T_DESTROY: 엔트리 만료/삭제 */
/* → Backup에 DELETE 메시지 전송 → Backup이 로컬 엔트리 제거 */
/* 헬퍼 상태 동기화 한계:
* conntrackd는 기본 conntrack 5-튜플과 상태만 동기화
* 헬퍼의 내부 상태 (expect 엔트리, 헬퍼별 사적 데이터)는 동기화 불가
*
* 예: FTP PASV expect는 Backup으로 전송되지 않음
* 장애 복구 후 진행 중인 FTP 전송은 데이터 채널이 끊길 수 있음
* → FTP는 세션 재시작 필요 (통상 수용 가능)
*/
# 동기화 상태 실시간 확인
conntrackd -s # 전체 통계
conntrackd -s network # 네트워크 동기화 통계
conntrackd -s cache # 캐시 동기화 통계
conntrackd -s runtime # 런타임 통계
conntrackd -s queue # 큐 통계
Pacemaker + conntrackd HA 구성
# Pacemaker + conntrackd 리소스 정의 (crm 형식)
primitive p_conntrackd ocf:heartbeat:conntrackd \
params ctd_config="/etc/conntrackd/conntrackd.conf" \
ctd_binary="/usr/sbin/conntrackd" \
op start timeout="30s" \
op stop timeout="30s" \
op monitor interval="10s" timeout="30s"
# VIP 리소스
primitive p_vip IPaddr2 \
params ip="203.0.113.1" cidr_netmask="24" nic="eth0" \
op monitor interval="5s"
# 동일 노드에서 VIP와 conntrackd 함께 실행
colocation co_vip_conntrackd inf: p_vip p_conntrackd
order ord_conntrackd_before_vip mandatory: p_conntrackd p_vip
# 노드 순서 (Primary 우선)
location loc_primary p_vip 100: primary-node
# conntrackd 페일오버 스크립트 (Pacemaker notify action)
# /usr/lib/ocf/resource.d/heartbeat/conntrackd
# Primary → Backup 전환 시:
conntrackd -t # 내부 캐시 → 커널 CT 테이블 커밋 (Backup 승격 시)
conntrackd -f # 커널 CT 테이블 → 내부 캐시 동기화 (Primary 복귀 시)
# 운영 명령
conntrackd -R # 커널로부터 CT 상태 다시 읽기 (리셋)
conntrackd -B # Bulk send: 전체 CT 상태를 Backup으로 일괄 전송
conntrackd -s stats # 상세 통계 출력
conntrackd는 기본 conntrack 5-튜플과 TCP/UDP 상태만 동기화합니다.
FTP PASV expect, SIP RTP expect 같은 헬퍼 내부 상태는 동기화되지 않습니다.
따라서 장애 복구 후 진행 중인 FTP 파일 전송이나 활성 SIP 통화는 미디어 채널 재협상이 필요할 수 있습니다.
장기 세션이 많은 환경에서는 이 제한을 운영 정책에 반영해야 합니다.
커널 소스 구조
conntrack 헬퍼 관련 커널 소스는 net/netfilter/와 include/net/netfilter/에 위치합니다.
| 파일 경로 | 역할 |
|---|---|
net/netfilter/nf_conntrack_core.c |
conntrack 코어: 해시 테이블, 상태 머신, 타이머 |
net/netfilter/nf_conntrack_helper.c |
헬퍼 등록/조회 API (nf_conntrack_helper_register()) |
net/netfilter/nf_conntrack_expect.c |
expect 테이블 관리 (nf_ct_expect_related()) |
net/netfilter/nf_conntrack_ftp.c |
FTP ALG 구현 (PORT/PASV 파싱) |
net/netfilter/nf_conntrack_sip.c |
SIP ALG 구현 (SDP 파싱, RTP expect) |
net/netfilter/nf_conntrack_h323.c |
H.323 ALG (복잡한 ASN.1 파싱 포함) |
net/netfilter/nf_conntrack_pptp.c |
PPTP ALG (GRE Call ID 기반 expect) |
net/netfilter/nf_nat_ftp.c |
FTP NAT 페이로드 재작성 (TCP 시퀀스 조정 포함) |
net/netfilter/nf_nat_sip.c |
SIP NAT 페이로드 재작성 (Contact/Via 헤더 수정) |
net/netfilter/nf_conntrack_netlink.c |
ctnetlink 구현 (Netlink 기반 이벤트 전달) |
include/net/netfilter/nf_conntrack_helper.h |
nf_conntrack_helper 구조체 정의 |
include/net/netfilter/nf_conntrack_expect.h |
nf_conntrack_expect 구조체 정의 |
nf_conntrack_expect 구조체
/* include/net/netfilter/nf_conntrack_expect.h */
struct nf_conntrack_expect {
/* expect 해시 테이블 노드 */
struct hlist_node hnode;
struct list_head lnode; /* master ct의 expect 리스트 */
/* 예상 연결의 5-튜플과 마스크 */
struct nf_conntrack_tuple tuple;
struct nf_conntrack_tuple mask;
/* 이 expect를 등록한 master conntrack */
struct nf_conn *master;
/* expect 만료 타이머 */
struct timer_list timeout;
/* 연결된 헬퍼 */
struct nf_conntrack_helper *helper;
/* expect 클래스 (헬퍼당 여러 클래스 가능) */
unsigned int class;
/* expect 매칭 시 호출되는 콜백 (NAT 연동 등) */
void (*expectfn)(struct nf_conn *new,
struct nf_conntrack_expect *this);
unsigned int flags;
unsigned int id;
};
헬퍼 → NAT 연동 경로
/* 헬퍼가 expect를 등록할 때 NAT 연동 콜백 설정 예 (FTP) */
exp->expectfn = nf_nat_ftp_expected;
/* nf_nat_ftp_expected()는 데이터 채널 연결 수립 시 호출되어:
* 1. 데이터 채널에 SNAT/DNAT 규칙 자동 추가
* 2. NAT IP:PORT를 데이터 채널 연결에 반영
*/
진단
conntrack 헬퍼 관련 문제를 진단하는 주요 도구와 명령을 정리합니다.
기본 진단 명령
# 1. conntrack 테이블 전체 조회 (RELATED 엔트리 확인)
conntrack -L | grep RELATED
# 2. expect 테이블 확인 (헬퍼가 등록한 예상 연결)
conntrack -L expect
# 3. conntrack 통계 확인 (드롭·insert_failed 확인)
conntrack -S
# 4. 특정 IP의 conntrack 엔트리 조회
conntrack -L --src-nat --orig-src 192.168.1.100
# 5. conntrack 테이블 현재 사용량 / 최대값 확인
conntrack -C # 현재 엔트리 수
cat /proc/sys/net/netfilter/nf_conntrack_max # 최대값
cat /proc/sys/net/netfilter/nf_conntrack_count # 현재 사용수
# 6. 헬퍼 등록 상태 확인
cat /proc/net/nf_conntrack_helper 2>/dev/null || \
nf-queue --list-helper 2>/dev/null
# 7. 실시간 conntrack 이벤트 모니터링
conntrack -E
# 8. FTP 연결 추적 (tcpdump와 병행)
tcpdump -i eth0 -A 'tcp port 21' &
conntrack -E -p tcp --dport 21
iptables 헬퍼 연결 확인
# raw 테이블에서 CT 타깃 설정 확인
iptables -t raw -L -v -n
# 헬퍼가 올바르게 연결된 경우:
# Chain PREROUTING (policy ACCEPT 0 packets, 0 bytes)
# pkts bytes target prot opt in out source destination
# 123 4567 CT tcp -- * * 0.0.0.0/0 0.0.0.0/0 tcp dpt:21 CT helper ftp
# nftables로 확인
nft list table ip raw
커널 디버그 로그 활성화
# conntrack 디버그 로그 활성화 (동적 디버그)
echo 'module nf_conntrack +p' | tee /sys/kernel/debug/dynamic_debug/control
echo 'module nf_conntrack_ftp +p' | tee /sys/kernel/debug/dynamic_debug/control
echo 'module nf_conntrack_sip +p' | tee /sys/kernel/debug/dynamic_debug/control
# 로그 확인
dmesg -w | grep -E 'conntrack|nf_ct'
# 또는 /proc/net/nf_conntrack 직접 확인
watch -n 1 'cat /proc/net/nf_conntrack | grep -i ftp'
# 다이나믹 디버그 해제
echo 'module nf_conntrack_ftp -p' | tee /sys/kernel/debug/dynamic_debug/control
bpftrace를 이용한 헬퍼 추적
# nf_ct_expect_related() 호출 추적
bpftrace -e '
kprobe:nf_ct_expect_related {
printf("[expect_related] pid=%d comm=%s\n", pid, comm);
}
kprobe:nf_conntrack_helper_register {
printf("[helper_register] pid=%d comm=%s\n", pid, comm);
}'
# conntrack 테이블 고갈 감지
bpftrace -e '
tracepoint:netfilter:nf_conntrack_alloc_overflow {
printf("[OVERFLOW] conntrack table full! time=%lu\n", nsecs);
}'
일반적인 문제와 해결책
| 증상 | 원인 | 해결책 |
|---|---|---|
| FTP 파일 전송 실패 (연결 거부) | FTP 헬퍼 미로드 또는 CT 연결 미설정 | modprobe nf_conntrack_ftp + CT 타깃 설정 |
| SIP 통화 한쪽만 들림 | SIP ALG가 Contact 헤더를 잘못 재작성 | rmmod nf_conntrack_sip + STUN/TURN 사용 |
nf_conntrack: table full |
conntrack 테이블 고갈 | nf_conntrack_max 증가 + 타임아웃 단축 |
| expect 등록됐으나 RELATED 미분류 | expect 만료(타임아웃) 또는 5-튜플 불일치 | 타임아웃 늘리기, expect 마스크 확인 |
| 헬퍼 로드됐으나 동작 안 함 | nf_conntrack_helper=0 + CT 타깃 미설정 |
iptables CT 타깃으로 명시적 연결 |
| Kubernetes Pod 간 FTP 실패 | kube-proxy DNAT + ALG 충돌 | ALG 비활성화 + PASV 포트 범위 고정 |
관련 문서
conntrack 헬퍼와 ALG를 더 깊이 이해하려면 다음 문서를 참고하세요.