1. 개요
Conntrack(connection tracking) 은 Netfilter 자리에 걸린 함수 둘과 커널 메모리의 해시 테이블 하나로, 지나가는 패킷이 어느 연결에 속하는지 기억하는 커널 모듈이다.
훅 함수는 패킷을 한 장씩 보고 앞에 뭐가 지나갔는지 모른다. “내가 시작한 연결의 답장만 받아라” 나 “첫 패킷에서 바꾼 주소를 같은 연결의 나머지 패킷에도 똑같이 적용하라” 는 누군가 연결을 기억해야 성립하고, 그 기억이 이 테이블이다.
nf_conntrack 모듈이 로드되면 네 자리의 명부에 함수를 등록한다. iptables -m state 나 NAT 규칙을 처음 넣을 때 자동으로 로드된다.
lsmod | grep nf_conntrack
sudo nft list hooks | grep -E 'conntrack|confirm'
2. 구성
2.1. 훅 함수 둘
| 함수 | 자리 | priority | 하는 일 |
|---|---|---|---|
nf_conntrack_in | PREROUTING, OUTPUT | -200 | 패킷에서 키를 뽑아 테이블 조회. 없으면 새 항목을 만든다. 결과를 skb 에 붙인다 |
nf_confirm | INPUT, POSTROUTING | INT_MAX | 새 항목을 테이블에 정식으로 넣는다 |
들어오는 패킷은 PREROUTING 에서 조회되고 INPUT(내 것) 이나 POSTROUTING(남 것) 에서 확정된다. 나가는 패킷은 OUTPUT 에서 조회, POSTROUTING 에서 확정. 입구와 출구에 맞춰 네 자리다. FORWARD 에는 없다. 조회는 이미 PREROUTING 에서 끝났고 확정은 POSTROUTING 에서 하면 되기 때문이다.
같은 자리의 다른 함수들은 테이블을 직접 뒤지지 않는다. nf_conntrack_in 이 skb->_nfct 에 붙여 둔 포인터를 읽는다.
| 읽는 쪽 | priority | 무엇을 |
|---|---|---|
| nat (DNAT -100 / SNAT 100) | conntrack 뒤 | 첫 패킷이면 규칙을 돌려 결과를 항목에 적고, 아니면 항목에 적힌 대로 주소를 바꾼다 |
filter 의 -m state / -m conntrack | 0 | 항목의 상태(NEW, ESTABLISHED …) 와 방향 |
2.2. 테이블
nf_conntrack_hash 는 버킷 배열이고, 각 버킷이 항목들의 연결 리스트다.
cat /proc/sys/net/netfilter/nf_conntrack_count # 지금 항목 수
cat /proc/sys/net/netfilter/nf_conntrack_max # 항목 수 상한
cat /proc/sys/net/netfilter/nf_conntrack_buckets # 버킷 수
평균 체인 길이는 count / buckets 이고, 조회 비용이 그만큼이다. max 는 항목 수 상한이지 버킷 수가 아니다.
3. 항목
항목 하나(struct nf_conn)가 연결 하나다. conntrack -L 의 한 줄이 그것이다.
tcp 6 86399 ESTABLISHED src=127.0.0.1 dst=127.0.0.1 sport=53424 dport=8080
src=127.0.0.1 dst=127.0.0.1 sport=8080 dport=53424 [ASSURED]
│ │ │ │ └ 첫째 줄: 키 1 (ORIGINAL, 요청 방향)
│ │ │ │ 둘째 줄: 키 2 (REPLY, 응답 방향)
│ │ │ 상태
│ │ 남은 초
│ 프로토콜 번호 (6 = TCP)
프로토콜
3.1. 키
테이블의 키는 struct nf_conntrack_tuple 한 덩어리다. 5-tuple 에 주소 체계와 zone 이 더해진다.
| 필드 | 뜻 | 위 줄의 키 1 |
|---|---|---|
src.ip | 출발 IP | 127.0.0.1 |
src.port | 출발 포트 | 53424 |
dst.ip | 목적지 IP | 127.0.0.1 |
dst.port | 목적지 포트 | 8080 |
dst.protonum | 프로토콜 | 6 (TCP) |
src.l3num | 주소 체계 | 2 (IPv4) |
| zone | 테이블을 논리적으로 나누는 번호 | 0 |
hash = jhash2(&tuple, sizeof(tuple) / 4, zone ^ hash_rnd) % buckets;
hash_rnd 는 부팅 때 뽑는 난수다. 공격자가 같은 버킷으로 몰리는 키를 계산해 체인을 길게 만드는 것을 막는다.
UDP 는 포트가 그대로 들어가고, ICMP 는 포트 자리에 type/code 와 id 가 들어가 echo 요청과 응답이 한 항목으로 묶인다.
3.2. 한 항목, 두 키
패킷 한 장은 자기 헤더에서 키 하나를 뽑아 한 번 조회한다. 그런데 한 연결의 패킷은 두 방향으로 오고, 요청과 답장의 키는 서로 뒤집힌 모양이라 다른 버킷으로 간다. 그래서 항목 하나를 두 키로 테이블에 건다.
일반 해시맵으로 쓰면 이것과 같다.
map["127.0.0.1:53424 → 127.0.0.1:8080"] = 항목 X
map["127.0.0.1:8080 → 127.0.0.1:53424"] = 항목 X # 같은 X
항목 안에는 키별로 테이블에 걸리는 고리(tuplehash[ORIGINAL], tuplehash[REPLY]) 가 있고, 조회는 버킷 체인을 따라가며 고리의 키를 비교하다 맞는 고리에서 항목으로 올라간다. 어느 고리에서 맞았느냐가 “이 패킷은 요청 방향인가 응답 방향인가” 가 된다(IP_CT_IS_REPLY). 방화벽의 ESTABLISHED 판정과 NAT 의 역적용이 이 방향 정보로 갈린다.
3.3. 항목의 나머지 칸
| 칸 | 뜻 |
|---|---|
status | NEW / ESTABLISHED 같은 상태 비트, ASSURED, 확정 여부 |
timeout | 지워지기까지 남은 초. 패킷이 오갈 때마다 다시 채워진다 |
| NAT 정보 | 어느 방향에 어떤 주소 변환을 적용했는지 |
| 프로토콜별 상태 | TCP 는 handshake 단계, 시퀀스 창 |
| 카운터 | 방향별 패킷·바이트 수 (nf_conntrack_acct=1 일 때) |
4. 패킷 한 장의 처리
들어온 패킷 한 장이 conntrack 과 그 위의 함수들을 어떻게 지나는지다.
4.1. 조회와 생성
/* nf_conntrack_in — 축약 */
tuple = extract_tuple(skb); /* 헤더에서 키 */
h = hash_lookup(tuple); /* ORIGINAL 이든 REPLY 든 */
if (!h) {
ct = new_conntrack(tuple); /* 키 2 는 키 1 을 뒤집어 미리 채움 */
ctinfo = IP_CT_NEW; /* 아직 테이블 밖 */
} else {
ct = container_of(h);
ctinfo = is_reply(h) ? IP_CT_ESTABLISHED_REPLY /* REPLY 고리에서 맞았나 */
: IP_CT_ESTABLISHED;
}
skb->_nfct = ct | ctinfo; /* 패킷에 붙인다 */
return NF_ACCEPT;
새 항목의 키 2 는 이 시점엔 키 1 을 뒤집은 것이다. 뒤의 DNAT 가 목적지를 바꾸면 그때 키 2 의 출발지가 바뀐 주소로 고쳐진다. 3.2 의 두 번째 그림이 그 결과다.
4.2. 확정이 맨 끝인 이유
새 항목을 조회 시점에 바로 테이블에 넣으면, 그 패킷이 뒤의 filter 에서 DROP 됐을 때 존재하지 않는 연결이 테이블에 남는다. 그래서 삽입은 priority INT_MAX 로 INPUT · POSTROUTING 명부의 마지막 칸에 두고, 패킷이 모든 검문을 통과해 거기까지 살아 도달했을 때만 넣는다.
conntrack -S 의 insert_failed 가 이 삽입이 실패한 횟수다. 테이블이 꽉 찼거나, 같은 키의 항목이 그 사이 다른 CPU 에서 먼저 들어갔을 때다.
5. 상태
5.1. TCP
Conntrack 의 상태는 TCP 의 상태와 이름이 같지만 기준이 다르다. TCP 는 handshake 가 끝나야 ESTABLISHED 이고, conntrack 은 양방향으로 패킷을 한 번씩 봤으면 ESTABLISHED 다.
| 상태 | 들어가는 조건 | 기본 timeout (초) | sysctl |
|---|---|---|---|
| SYN_SENT | 첫 SYN | 120 | nf_conntrack_tcp_timeout_syn_sent |
| SYN_RECV | SYN-ACK | 60 | …_syn_recv |
| ESTABLISHED | ACK | 432000 (5 일) | …_established |
| FIN_WAIT | 한쪽 FIN | 120 | …_fin_wait |
| CLOSE_WAIT | 반대쪽 FIN | 60 | …_close_wait |
| LAST_ACK | 마지막 ACK 대기 | 30 | …_last_ack |
| TIME_WAIT | 양쪽 FIN 완료 | 120 | …_time_wait |
| CLOSE | RST | 10 | …_close |
값은 /proc/sys/net/netfilter/nf_conntrack_tcp_timeout_* 에 있고, 배포판이나 클라우드 이미지가 바꿔 두기도 한다. ESTABLISHED 가 86400 으로 보이면 그 환경의 설정이다.
5.2. UDP 와 ICMP
연결이 없는 프로토콜도 항목이 생긴다. 조건이 “양방향으로 한 번씩” 이기 때문이다.
| 프로토콜 | NEW | ESTABLISHED (UNREPLIED 해제) | timeout |
|---|---|---|---|
| UDP | 첫 데이터그램 | 반대 방향 한 장 | 30 초, 양방향 반복이면 120 초 (udp_timeout_stream) |
| ICMP | echo 요청 | echo 응답 (같은 id) | 30 초 |
DNS 질의 한 장이 나가면 [NEW] [UNREPLIED], 응답 한 장이 오면 UNREPLIED 가 빠지고 방화벽의 ESTABLISHED 규칙이 성립한다.
5.3. ASSURED
양방향으로 충분히 오간 항목에 붙는 표시다. TCP 는 handshake 완료, UDP 는 양방향 반복. 테이블이 가득 찼을 때 커널은 ASSURED 가 아닌 항목부터 지워 자리를 만든다. SYN 만 던지고 가는 스캐너의 항목이 먼저 지워지는 이유다.
6. 테이블 포화
cat /proc/sys/net/netfilter/nf_conntrack_count /proc/sys/net/netfilter/nf_conntrack_max
sudo conntrack -S # cpu 별 insert_failed, drop, early_drop
sudo conntrack -L | awk '{print $4}' | sort | uniq -c # 상태별 항목 수
count 가 max 에 닿으면 새 항목을 만들 수 없어 패킷을 버린다. 커널 로그에 이렇게 남는다.
nf_conntrack: table full, dropping packet
| 지표 | 뜻 |
|---|---|
insert_failed | 확정 단계에서 삽입 실패 |
drop | 항목을 만들 수 없어 패킷 폐기 |
early_drop | 자리를 만들려고 ASSURED 아닌 항목을 강제로 지움 |
연결을 많이 만드는 서비스(DNS, 프록시, 쿠버네티스 노드의 kube-proxy) 에서 겪는다. max 를 키우기 전에 어떤 상태의 항목이 쌓이는지 먼저 본다. TIME_WAIT 가 대부분이면 timeout 을 줄이는 쪽이 맞고, SYN_SENT 가 쌓이면 밖에서 오는 스캔이다.
7. 확인 명령
| 명령 | 보는 것 |
|---|---|
conntrack -L [-p tcp] [--dport N] [-s IP] [-d IP] | 테이블의 항목. netlink 로 읽는다 |
conntrack -E [-o timestamp] | 항목이 생기고 바뀌고 지워지는 이벤트 |
conntrack -S | CPU 별 통계 |
conntrack -D ... | 항목 삭제 |
/proc/sys/net/netfilter/nf_conntrack_* | count, max, buckets, 프로토콜별 timeout |
nft list hooks | 어느 자리에 conntrack 함수가 걸려 있는지 |
conntrack 도구는 테이블을 읽는 유저 공간 프로그램이고, 패킷을 보는 것은 커널의 두 함수다.
8. 관련 문서
- Netfilter — conntrack 함수가 걸리는 자리와 명부, priority 순서
- NAT (예정) — 첫 패킷에만 규칙, 나머지는 이 테이블로
- iptables (예정) —
-m state,-m conntrack매치 - Service — Service IP 로 가는 연결마다 백엔드 Pod 가 고정되는 이유가 이 테이블이다
9. 출처
- nftables wiki — Connection Tracking System
- nftables wiki — Matching connection tracking stateful metainformation
- kernel.org — nf_conntrack sysctl
- conntrack-tools — conntrack(8)
- 커널 소스
net/netfilter/nf_conntrack_core.c,include/net/netfilter/nf_conntrack.h,include/net/netfilter/nf_conntrack_tuple.h