IPSec & xfrm
Linux xfrm/IPSec 스택을 심층 분석합니다. SPD/SAD와 selector·reqid·if_id·mark의 결합 방식, ESP/AH/IPCOMP 처리 경로, 터널/트랜스포트/BEET 모드, NAT-T와 route-based VPN, ACQUIRE/EXPIRE/NEWAE 이벤트, Crypto API 및 NIC 오프로드, strongSwan/Libreswan 운영 시 발생하는 MTU·재전송·정책 불일치 문제의 진단 포인트까지 실무 관점으로 정리합니다.
핵심 요약
- SPD — 어떤 트래픽을 보호, 우회, 차단할지 결정하는 정책 데이터베이스입니다.
- SAD / SA — SPI, 키, 알고리즘, 수명, anti-replay 윈도우를 가진 실제 변환 상태입니다.
- selector — 주소, 프로토콜, 포트, mark, if_id 같은 매칭 조건이며 정책과 상태를 연결합니다.
- reqid / bundle — 여러 변환(IPCOMP → ESP 등)을 한 묶음으로 만들고 policy tmpl과 state를 연결합니다.
- NAT-T / if_id / output-mark — 운영 환경에서는 NAT, route-based VPN, 정책 라우팅과 함께 봐야 합니다.
단계별 이해
- 정책 선택
패킷이 SPD에서allow/block/protect중 무엇으로 판정되는지부터 확인합니다. - 상태 확보
보호가 필요하지만 SA가 없으면 커널이XFRM_MSG_ACQUIRE를 올리고, IKE 데몬이 협상 후 SA/SP를 주입합니다. - 변환 적용
송신은xfrm_lookup()과 bundle 생성 후 ESP/AH/IPCOMP를 적용하고, 수신은 SPI 기반 SA 조회 후 anti-replay와 ICV 검증을 수행합니다. - 정책 재검증
복호화된 수신 패킷은 다시 수신 정책과 대조되어, "복호화는 성공했지만 정책이 틀린" 패킷을 차단합니다. - 관측/튜닝
ip xfrm,/proc/net/xfrm_stat,ip xfrm monitor, NIC 오프로드 통계로 병목과 불일치를 좁혀갑니다.
xfrm 프레임워크와 IPSec 심화
xfrm(transform)은 리눅스 커널의 IPSec 구현 프레임워크입니다. 패킷의 암호화, 인증, 압축, 캡슐화를 처리하며, SA(Security Association)와 SP(Security Policy) 데이터베이스로 관리됩니다.
RX: 수신 → ESP 복호화/검증 → SA 매칭 → SP 정책 검증 → 평문 전달
xfrm 아키텍처
selector, policy, state의 결합 모델
xfrm을 가장 빠르게 이해하는 방법은 패킷의 selector가
SPD 정책을 고르고, 정책의 tmpl이 필요한
SA(xfrm_state)를 찾거나 생성하게 만든다고 보는 것입니다.
운영 중에 자주 보는 reqid, mark, if_id,
output-mark는 모두 이 결합을 더 정밀하게 만드는 식별자입니다.
| 개념 | 핵심 필드 | 운영 관점 의미 |
|---|---|---|
| selector | 주소, 포트, L4 프로토콜, mark, ifindex, uid | 정책이 어떤 패킷에 적용되는지 결정합니다. tunnel mode에서도 내부 평문 selector가 중요합니다. |
| xfrm_policy | dir, priority, action, mark, if_id, xfrm_vec[] | 정책 우선순위 충돌과 route-based VPN 동작은 대부분 여기서 갈립니다. if_id가 다르면 동일 selector도 공존할 수 있습니다. |
| xfrm_tmpl | proto, mode, reqid, level, optional | 정책이 요구하는 SA의 "형태"를 정의합니다. level use는 IPCOMP 같은 예외 처리에서 의미가 큽니다. |
| xfrm_state | SPI, 알고리즘, 키, 수명, replay, if_id, encap | 실제 암복호화 엔진입니다. 수명 만료, SPI 충돌, replay-window, NAT-T 캡슐화 여부가 모두 state에 들어갑니다. |
| bundle / dst cache | state 체인, outer route, output-mark | 정책 검색 비용을 줄이고 후속 패킷을 빠르게 보냅니다. 라우팅 변경이나 SA 만료 시 캐시가 무효화됩니다. |
특히 reqid는 사람이 보기엔 사소한 숫자처럼 보이지만, IKE 데몬이
양방향 SA 쌍과 정책 템플릿을 안정적으로 묶는 데 핵심입니다. 반대로 mark,
output-mark, if_id는 라우팅 도메인과 터널 인터페이스를 분리할 때
중요하며, 강한 정책 분리 없이 default route 하나로 xfrmi를 붙이면 IKE/ESP 루프를 만들기 쉽습니다.
IPSec 프로토콜 비교
| 프로토콜 | IP 번호 | 기능 | 보호 범위 | 주의사항 |
|---|---|---|---|---|
| ESP | 50 | 기밀성 + 무결성 + anti-replay | 페이로드 전체 (터널: 원본 IP 헤더 포함) | 현대 배포의 기본값. AEAD(AES-GCM) 또는 ENC+AUTH 조합 사용. NAT 환경에서는 NAT-T 필요 |
| AH | 51 | 인증만 (암호화 없음) | IP 헤더 포함 전체 패킷 (변경 가능 필드 제외) | NAT와 호환 불가 (IP 헤더가 인증 범위). 현대 환경에서 거의 미사용 |
| IPCOMP | 108 | 페이로드 압축 | ESP/AH 전에 페이로드 압축 | 작아질 때만 전송. 압축 효과가 없으면 원문 전송되므로 receiver 정책에서 level use 검토가 필요할 수 있음 |
터널 모드 vs 트랜스포트 모드
# 트랜스포트 모드: 호스트-to-호스트, 원본 IP 헤더 유지
# [IP Header][ESP Header][Payload (encrypted)][ESP Trailer][ESP Auth]
ip xfrm state add src 10.0.0.1 dst 10.0.0.2 proto esp spi 0x100 mode transport enc "aes" 0x$(openssl rand -hex 16) auth "hmac(sha256)" 0x$(openssl rand -hex 32)
# 터널 모드: 게이트웨이-to-게이트웨이, 원본 패킷 전체 캡슐화
# [New IP][ESP Header][Original IP][Payload (encrypted)][ESP Trailer][Auth]
ip xfrm state add src 203.0.113.1 dst 198.51.100.1 proto esp spi 0x200 mode tunnel enc "aes" 0x$(openssl rand -hex 16) auth "hmac(sha256)" 0x$(openssl rand -hex 32)
# Security Policy (어떤 트래픽에 IPSec 적용할지)
ip xfrm policy add src 192.168.1.0/24 dst 192.168.2.0/24 dir out tmpl src 203.0.113.1 dst 198.51.100.1 proto esp mode tunnel
# 현재 SA/SP 확인
ip xfrm state list # SA 목록 (키, SPI, 알고리즘)
ip xfrm policy list # SP 목록 (셀렉터, 방향)
ip xfrm monitor # 실시간 xfrm 이벤트 모니터링
- transport — 호스트 대 호스트 보호에 적합합니다. 원래 IP 헤더를 유지하므로 디버깅은 쉽지만 주소 은닉 효과는 없습니다.
- tunnel — 게이트웨이 대 게이트웨이, route-based VPN, 다중 서브넷 보호의 기본값입니다. 내부 패킷 전체가 ESP 안으로 들어갑니다.
- BEET —
ip xfrm와 커널은 지원하지만 실제 일반 VPN 배포에서는 드뭅니다. 주소 고정 비용을 줄이려는 특수 시나리오에 가깝습니다. - level required — 정책에 맞는 SA가 없거나 필요한 변환이 빠지면 드롭합니다. 기본값으로 생각하면 됩니다.
- level use — SA가 있으면 사용하되 없어도 통과를 허용합니다. IPCOMP의 비압축 패킷 처리나 점진적 마이그레이션 같은 제한적 상황에서만 신중히 사용하세요.
BEET 모드 상세
BEET(Bound End-to-End Tunnel) 모드는 transport 모드와 tunnel 모드의 하이브리드입니다. 와이어 상에서는 터널 모드처럼 외부 IP 헤더를 추가하지만, 엔드포인트에서는 트랜스포트 모드처럼 동작하여 내부 주소와 외부 주소가 고정된 1:1 매핑을 갖습니다. HIP(Host Identity Protocol, RFC 5206)과 함께 사용되며, 일반 VPN 배포에서는 거의 사용되지 않습니다.
| 특성 | Transport | Tunnel | BEET |
|---|---|---|---|
| 외부 IP 헤더 | 없음 (원본 수정) | 추가 (+20B IPv4) | 추가 (+20B IPv4) |
| 내부 IP 헤더 | N/A | 암호화 영역에 포함 | 생략 (SA에서 재구성) |
| 오버헤드 | 최소 | 최대 | 중간 (터널 대비 -20B) |
| 주소 은닉 | 불가 | 완전 은닉 | 1:1 매핑 (부분 은닉) |
| 다중 서브넷 | 불가 | 가능 | 불가 (1:1 매핑) |
| 주요 용도 | 호스트-to-호스트 | 게이트웨이 VPN | HIP, 모빌리티 |
| 커널 지원 | 완전 | 완전 | 지원 (mode beet) |
| IKE 데몬 지원 | strongSwan, Libreswan | strongSwan, Libreswan | 제한적 (HIP 데몬) |
# BEET 모드 SA 설정 예시
# 내부 주소 10.0.0.1 ↔ 외부 주소 203.0.113.1 (1:1 매핑)
ip xfrm state add src 203.0.113.1 dst 198.51.100.1 \
proto esp spi 0xBEE7 mode beet \
sel src 10.0.0.1/32 dst 10.0.0.2/32 \
aead 'rfc4106(gcm(aes))' 0x$(openssl rand -hex 20) 128
ip xfrm policy add src 10.0.0.1/32 dst 10.0.0.2/32 dir out \
tmpl src 203.0.113.1 dst 198.51.100.1 proto esp mode beet
커널 xfrm 내부 구조
/* net/xfrm/xfrm_state.c — Security Association */
struct xfrm_state {
struct xfrm_id id; /* (daddr, spi, proto) */
struct xfrm_selector sel; /* SA에 연결된 selector */
struct xfrm_mark mark; /* fwmark 기반 분리 */
u32 if_id; /* xfrm interface 식별자 */
struct xfrm_lifetime_cfg lft; /* soft/hard lifetime */
struct {
u32 reqid; /* policy tmpl과의 결속 키 */
u8 mode; /* transport / tunnel / beet */
u8 replay_window;
xfrm_address_t saddr; /* 터널 외부 소스 주소 */
} props;
struct xfrm_algo_auth *aalg; /* HMAC-SHA2 등 */
struct xfrm_algo *ealg; /* AES-CBC 등 */
struct xfrm_algo *calg; /* deflate 등 */
struct xfrm_algo_aead *aead; /* AES-GCM, ChaCha20-Poly1305 */
struct xfrm_replay_state_esn *replay_esn;
/* H/W offload state */
struct xfrm_dev_offload xso;
};
/* net/ipv4/esp4.c — ESP 패킷 처리 */
/* esp_output(): 송신 패킷 암호화 */
/* esp_input(): 수신 패킷 복호화 */
위 코드는 핵심 필드만 발췌한 축약본입니다. 실제 커널 구조체에는 lock/refcount, state cache, 보안 컨텍스트, offload 통계, garbage-collection 연결 정보까지 들어갑니다. 중요한 점은 state에도 selector, mark, if_id, reqid가 남아 있다는 것이고, 그래서 "정책만 맞고 상태는 다른 터널에 붙는" 오동작을 막을 수 있습니다.
권장 알고리즘 조합
| 배포 기준 | 암호화 | 인증 | 비고 |
|---|---|---|---|
| 신규 기본값 | AES-GCM-128/256 | (내장) | RFC 8221에서 ENCR_AES_GCM_16이 MUST. HW 오프로드와 상호운용성이 가장 좋음 |
| 상호운용 기본값 | AES-CBC-128/256 | HMAC-SHA2-256-128 | RFC 8221 기준으로 AES-CBC는 MUST, HMAC-SHA2-256-128도 MUST. 레거시 장비와 가장 무난 |
| AES 가속이 약한 CPU | ChaCha20-Poly1305 | (내장) | RFC 8221에서 SHOULD. AES-NI가 없는 x86, 일부 ARM/가상화 환경에서 유리할 수 있음 |
| 레거시만 허용 | 3DES, ENCR_NULL | HMAC-SHA1-96 | 이론상 상호운용 때문에 남아 있지만 신규 배포 기준으로는 선택하지 마세요 |
| 사용 금지 | DES, Blowfish, 수동 키잉 + GCM/CTR/CCM/ChaCha | MD5 | RFC 8221 기준. 수동 키잉 환경에서는 nonce 재사용 위험 때문에 AEAD/CTR 계열을 쓰면 안 됩니다 |
IPSec/xfrm 주의사항
- MTU/PMTUD — ESP 캡슐화로 패킷 크기 증가 (ESP: +36~73바이트, 터널 모드: +20 추가). PMTUD 실패 시 블랙홀 발생.
ip link set dev ipsec0 mtu 1400또는 MSS clamping 필요 - NAT Traversal — ESP는 IP 프로토콜이라 NAT 통과 불가. NAT-T(UDP 4500 캡슐화)를 IKE에서 자동 감지/활성화해야 함
- Anti-replay 윈도우 — 기본 32패킷. 고대역 환경에서 패킷 재정렬로 정상 패킷이 드롭될 수 있음.
replay-window 1024이상 권장 - SA 수명 관리 — 키 재생성(rekey) 시 트래픽 순간 단절 가능. IKEv2의 CHILD_SA rekey가 seamless 하지만 구현 의존
- CPU 오버헤드 — 소프트웨어 ESP 암호화는 CPU 집약적. 10Gbps 환경에서 CPU 포화 가능. QAT/SmartNIC 오프로드 활용
- conntrack 상호작용 — ESP 패킷의 conntrack 처리. 터널 모드에서는 외부/내부 패킷 각각 conntrack 엔트리 생성
- Policy routing 충돌 — xfrm policy와 ip rule/route의 우선순위 상호작용 주의.
ip xfrm policy list로 정책 순서 확인 - VTI vs xfrm interface — VTI(가상 터널 인터페이스)는 레거시. 커널 4.19+의
xfrm interface(if_id기반)가 더 유연하고 netns 지원
MTU / PMTUD와 단편화 상세
ESP 캡슐화는 패킷 크기를 증가시킵니다. 이 오버헤드를 고려하지 않으면 PMTUD(Path MTU Discovery) 블랙홀이 발생하여 대용량 전송이 실패하는 흔한 장애가 생깁니다. 오버헤드 크기는 알고리즘, 모드, NAT-T 여부에 따라 달라집니다.
| 구성 | ESP 오버헤드 (바이트) | 1500B 경로 시 내부 MTU |
|---|---|---|
| Transport + AES-GCM-128 | 34~49 | 1451~1466 |
| Tunnel + AES-GCM-128 | 54~69 | 1431~1446 |
| Tunnel + AES-CBC + HMAC-SHA256 | 62~77 | 1423~1438 |
| Tunnel + NAT-T + AES-GCM-128 | 62~77 | 1423~1438 |
| Tunnel + NAT-T + AES-CBC + HMAC-SHA256 | 70~85 | 1415~1430 |
# MTU 설정 방법들
# 1. xfrm interface MTU 직접 설정 (가장 권장)
ip link set dev xfrm0 mtu 1400
# 2. MSS clamping (TCP만, 가장 안정적인 PMTUD 우회)
iptables -t mangle -A FORWARD -o xfrm0 -p tcp \
--tcp-flags SYN,RST SYN -j TCPMSS --clamp-mss-to-pmtu
# 3. PMTUD 동작 확인
ping -M do -s 1400 192.168.2.1 # DF bit 설정으로 단편화 금지
# "Frag needed" ICMP가 돌아와야 정상
# 4. PMTUD 블랙홀 테스트
tracepath 192.168.2.1 # PMTU 탐색 경로 표시
# 5. strongSwan에서 자동 MTU 처리
# swanctl.conf: connections.*.children.*.set_mark_out = 0x42
# → xfrm interface MTU가 자동으로 적용됨
# 6. 커널의 자동 단편화 동작
# 터널 모드에서 내부 패킷이 ESP 후 MTU를 초과하면:
# - DF=1: ICMP Frag Needed를 원본 발신자에게 전송
# - DF=0: ESP 전에 내부 패킷을 단편화한 후 각각 ESP 캡슐화
# (비효율적: 각 fragment마다 ESP 오버헤드 추가)
- 증상: 작은 패킷(ping, DNS)은 통과하지만 대용량 전송(HTTP, SCP)이 멈춤
- 원인: 경로 상의 장비가 "ICMP Frag Needed"를 차단하여 TCP가 MSS를 줄이지 못함
- 해결: MSS clamping이 가장 안정적. 또는
sysctl net.ipv4.tcp_mtu_probing=1로 TCP의 PLPMTUD 활성화 - IPv6: IPv6는 중간 라우터가 단편화하지 않으므로 PMTUD가 필수. ICMPv6 Packet Too Big이 반드시 통과해야 함
IPSec 디버깅
# xfrm 통계 — 오류 원인 파악
ip -s xfrm state # SA별 패킷/바이트 카운터
ip -s xfrm policy # SP별 매칭 카운터
cat /proc/net/xfrm_stat
# XfrmInError: 복호화/인증 실패
# XfrmInNoStates: 매칭 SA 없음
# XfrmOutPolBlock: SP에 의해 차단
# XfrmOutBundleGenError: SA 번들 생성 실패
# 패킷 캡처 (ESP 헤더 확인)
tcpdump -i eth0 esp
tcpdump -i eth0 'ip proto 50' # ESP
tcpdump -i eth0 'ip proto 51' # AH
# IKE 데몬 로그 (strongSwan)
swanctl --log --level 2
| 명령 | 무엇을 보는가 | 대표 증상 |
|---|---|---|
ip -s xfrm state |
SA별 패킷/바이트/오류 카운터 | 한쪽 방향만 bytes가 늘면 selector, 라우팅, NAT-T 대칭성이 틀어졌을 가능성이 큽니다. |
ip -s xfrm policy |
정책 hit 카운터 | state는 존재하는데 hit가 0이면 mark, if_id, direction, port selector를 의심하세요. |
ip xfrm monitor all-nsid |
ACQUIRE / EXPIRE / SA / policy / aevent 실시간 이벤트 | trap policy, rekey, netns 간 이벤트를 가장 빠르게 확인하는 방법입니다. |
cat /proc/net/xfrm_stat |
커널 XFRM private MIB | XfrmInNoStates, XfrmInNoPols, XfrmInTmplMismatch, XfrmOutBundleGenError를 분리해서 봐야 합니다. |
ip -d link show xfrm0 |
xfrmi의 if_id, master, 링크 속성 | route-based VPN에서 if_id 불일치, VRF 연결 누락을 확인하기 좋습니다. |
ethtool -k/-S eth0 |
ESP offload capability와 드라이버 통계 | HW offload가 기대대로 동작하지 않으면 capability 미지원이나 fallback 흔적이 드러납니다. |
xfrm_proc는 XfrmInNoStates를
"SPI/address/proto로 맞는 SA를 찾지 못한 경우", XfrmInStateProtoError를
"키 또는 프로토콜별 무결성 오류", XfrmInNoPols를
"SA는 맞지만 수신 정책이 없는 경우"로 구분합니다.
이 세 값을 섞어 보면 원인 분리가 잘못됩니다.
xfrm_stat 카운터 완전 해설
/proc/net/xfrm_stat은 커널 xfrm 서브시스템의 MIB(Management Information Base) 카운터를 노출합니다.
이 카운터들은 IPSec 장애의 원인을 정확하게 분류하는 핵심 도구이며,
각 카운터의 의미를 혼동하면 엉뚱한 방향으로 디버깅하게 됩니다.
| 카운터 | 방향 | 의미 | 진단 포인트 |
|---|---|---|---|
XfrmInError |
IN | ESP/AH 복호화 또는 인증 실패 (일반 오류) | 키 불일치, 알고리즘 mismatch, 패킷 손상. tcpdump로 SPI 확인 후 양쪽 SA 비교 |
XfrmInBufferError |
IN | 메모리 할당 실패 (skb 부족) | 시스템 메모리 부족. dmesg에 OOM 메시지 확인 |
XfrmInHdrError |
IN | IP 헤더 또는 ESP/AH 헤더 파싱 오류 | 단편화된 ESP 패킷, 잘린 패킷. MTU/단편화 문제 의심 |
XfrmInNoStates |
IN | (daddr, SPI, proto)로 SA를 찾지 못함 | SA 만료/삭제, SPI 불일치, 방향(in/out) 착오. ip xfrm state로 SPI 존재 여부 확인 |
XfrmInStateProtoError |
IN | 프로토콜별 무결성 검증 실패 (ICV mismatch) | 키 불일치 또는 전송 중 패킷 변조. AES-GCM 태그 검증 실패가 여기에 들어감 |
XfrmInStateModeError |
IN | SA 모드(transport/tunnel)와 패킷 형태 불일치 | 한쪽은 tunnel, 다른 쪽은 transport로 설정된 경우 |
XfrmInStateSeqError |
IN | 시퀀스 번호 오류 (ESN 상위 비트 불일치) | ESN 활성/비활성 불일치 또는 HA failover 후 시퀀스 점프 |
XfrmInStateExpired |
IN | 만료된 SA로 수신한 패킷 | rekey 지연. soft expire → hard expire 사이에 트래픽이 여전히 오래된 SA로 들어옴 |
XfrmInStateMismatch |
IN | SA가 있지만 패킷의 selector와 불일치 | 와일드카드 셀렉터 문제. 의도하지 않은 SA에 매칭된 경우 |
XfrmInStateInvalid |
IN | SA가 유효하지 않은 상태 (larval, dead) | ACQUIRE 후 아직 키가 설치되지 않은 SA에 패킷이 도착 |
XfrmInTmplMismatch |
IN | 수신 정책의 tmpl과 실제 적용된 SA가 불일치 | 정책은 ESP를 요구하는데 AH로 도착했거나, reqid/mode가 틀린 경우 |
XfrmInNoPols |
IN | SA는 매칭되었으나 수신 정책(POLICY_IN)이 없음 | 정책 누락. SA만 있고 inbound policy가 빠진 불완전한 설정 |
XfrmInPolBlock |
IN | 수신 정책이 BLOCK으로 판정 | 의도적 차단이 아니라면 정책 우선순위 검토 |
XfrmInPolError |
IN | 수신 정책 검색 중 오류 | 내부 커널 오류. 드물지만 메모리 부족 시 발생 가능 |
XfrmInSeqOutOfWindow |
IN | 시퀀스 번호가 anti-replay 윈도우 밖 | 가장 흔한 카운터. 윈도우 크기 부족, RSS 재정렬, 멀티패스. replay-window 확대 필요 |
XfrmInStateReplay |
IN | 중복 시퀀스 번호 (replay 공격 또는 중복 전송) | 실제 공격이 아니라면 네트워크 중복(bonding, ECMP 등) 의심 |
XfrmOutError |
OUT | ESP/AH 암호화 처리 중 일반 오류 | Crypto API 오류, 키 길이 불일치 |
XfrmOutBundleGenError |
OUT | SA bundle 생성 실패 | 정책은 있지만 SA가 없거나 조합이 틀린 경우. ACQUIRE 전 단계에서 발생 |
XfrmOutBundleCheckError |
OUT | 기존 bundle 검증 실패 (캐시 무효) | SA 만료 후 캐시된 bundle이 아직 참조됨. 일시적 |
XfrmOutNoStates |
OUT | 송신 시 사용할 SA 없음 | ACQUIRE가 실패했거나 타임아웃. IKE 데몬 상태 확인 |
XfrmOutStateProtoError |
OUT | 암호화 처리 중 프로토콜 오류 | 알고리즘 초기화 실패 (모듈 미로드 등) |
XfrmOutPolBlock |
OUT | 송신 정책이 BLOCK으로 판정 | 명시적 차단 정책 또는 우선순위 문제 |
XfrmOutPolDead |
OUT | 매칭된 정책이 이미 삭제/만료됨 | race condition. 정책 삭제와 패킷 처리가 겹친 경우 |
XfrmOutPolError |
OUT | 송신 정책 검색 중 오류 | 내부 커널 오류 |
XfrmFwdHdrError |
FWD | 포워딩 경로에서 헤더 오류 | 라우터 역할의 IPSec 게이트웨이에서 발생 |
XfrmOutStateInvalid |
OUT | 송신 SA가 유효하지 않은 상태 | SA dead 상태에서 아직 참조되는 경우 |
XfrmAcquireError |
OUT | ACQUIRE 메시지 전송 실패 | IKE 데몬이 연결되지 않았거나 Netlink 소켓 오류 |
# xfrm_stat 전체 카운터 확인
cat /proc/net/xfrm_stat
# 특정 카운터만 모니터링 (watch로 변화 추적)
watch -d -n1 'cat /proc/net/xfrm_stat | grep -E "InNoStates|InSeqOutOfWindow|OutPolBlock"'
# Prometheus node_exporter로 수집 (textfile collector)
# /etc/node_exporter/textfile/xfrm.prom 생성 스크립트:
awk '{print "xfrm_stat_" tolower($1) " " $2}' /proc/net/xfrm_stat \
> /etc/node_exporter/textfile/xfrm.prom
# 카운터 리셋 (네트워크 네임스페이스 재생성 외에는 불가)
# → 누적 카운터이므로 delta 방식으로 모니터링해야 함
XfrmInNoStates증가 → SA가 없음.ip xfrm state로 SPI 확인, IKE 데몬 로그 점검XfrmInSeqOutOfWindow증가 → replay-window 확대 (1024또는2048)XfrmInStateProtoError증가 → 키/알고리즘 불일치. 양쪽 SA 비교XfrmInNoPols증가 → inbound 정책 누락.ip xfrm policy에서 dir in 확인XfrmOutBundleGenError증가 → 정책은 있지만 SA 매칭 실패. reqid, if_id, mark 검토
실전 트러블슈팅 시나리오
IPSec 장애는 원인이 다양하고 증상이 모호한 경우가 많습니다. 아래는 실무에서 빈번하게 발생하는 문제와 체계적인 진단 경로를 정리한 것입니다.
| 증상 | 가능한 원인 | 진단 명령 | 해결 방법 |
|---|---|---|---|
| 터널 수립 후 트래픽 없음 (ping 실패) | 라우팅 누락, selector 불일치, 방화벽 차단 |
ip -s xfrm state (카운터 0?)ip route get 192.168.2.1tcpdump -i eth0 esp
|
트래픽이 xfrm policy에 매칭되는지 확인. 라우팅 테이블에 터널 경로 추가. iptables INPUT/FORWARD 규칙 검토 |
| 한 방향만 통신 (A→B OK, B→A 실패) | 비대칭 SA/SP, NAT-T 비대칭, reverse path filter |
ip -s xfrm state (한쪽만 bytes 증가?)sysctl net.ipv4.conf.all.rp_filter
|
양쪽 SA의 SPI/키 쌍 확인. rp_filter=2(loose) 또는 0으로 완화. NAT-T 포트 매핑 확인 |
| 대용량 파일 전송 실패 (작은 패킷 OK) | PMTUD 블랙홀, MSS 미조정 |
ping -M do -s 1400 peertcpdump -i eth0 'icmp and icmp[0]=3'
|
MTU 1400 설정 또는 MSS clamping: iptables -t mangle -A FORWARD -p tcp --tcp-flags SYN,RST SYN -j TCPMSS --clamp-mss-to-pmtu |
| 간헐적 패킷 드롭 | anti-replay 윈도우 부족, SA 만료 경쟁 |
cat /proc/net/xfrm_stat | grep SeqOutOfWindowip xfrm monitor
|
replay-window 2048 설정. rekey 마진 확대 |
| IKE 협상 실패 (NO_PROPOSAL_CHOSEN) | 알고리즘 제안 불일치 |
swanctl --log --level 2양쪽 proposals 비교 |
양쪽 IKE/ESP proposals를 동일하게 맞춤. 최소 aes256gcm128-x25519-sha256 |
| rekey 후 트래픽 단절 | old/new SA 전환 실패, DPD 타이밍 문제 |
ip xfrm monitor (EXPIRE/NEWSA 순서 확인)swanctl --list-sas
|
rekey_time과 life_time 간격 확인. make-before-break가 동작하는지 로그 확인 |
| ESP 패킷이 방화벽에서 차단 | NAT-T 미활성, ISP가 proto 50 차단 |
tcpdump -i eth0 'ip proto 50 or udp port 4500'
|
NAT-T 강제 활성화 (strongSwan: forceencaps=yes). TCP 캡슐화(ESP-in-TCP) 검토 |
| 성능 저하 (throughput 기대치 미달) | 소프트웨어 ESP 병목, GRO/GSO 비활성, CPU affinity 미설정 |
perf top -C 0-7ethtool -k eth0 | grep espmpstat -P ALL 1
|
GRO/GSO 활성화, CPU affinity 설정, HW offload 검토, AES-GCM 사용 확인 |
# 체계적 IPSec 진단 워크플로
# Step 1: SA/SP 존재 여부 확인
ip xfrm state list # SA가 있는가? SPI, 알고리즘, 수명 확인
ip xfrm policy list # SP가 있는가? selector, direction, priority 확인
# Step 2: 카운터 확인 (어느 단계에서 실패하는가?)
ip -s xfrm state # SA별 패킷/바이트 카운터
ip -s xfrm policy # 정책 hit 카운터
cat /proc/net/xfrm_stat # 전체 오류 카운터
# Step 3: 실시간 이벤트 관찰
ip xfrm monitor all # ACQUIRE, EXPIRE, NEWSA 이벤트 스트림
# Step 4: 패킷 캡처 (ESP 패킷이 나가는가? 들어오는가?)
tcpdump -i eth0 -n 'esp or udp port 4500 or udp port 500' -c 20
# Step 5: IKE 데몬 로그 (협상 실패 원인)
journalctl -u strongswan --since "5 min ago" | tail -50
# Step 6: 라우팅 확인 (트래픽이 올바른 경로로 가는가?)
ip route get 192.168.2.1 mark 0x42 # xfrm mark까지 포함한 경로
# Step 7: Netfilter 간섭 확인
iptables -L -v -n | grep -E 'esp|ipsec|xfrm'
iptables -t mangle -L -v -n
- 양방향 SA/SP 누락: IPSec은 단방향이므로 in/out 양쪽 모두 SA와 SP가 필요합니다. IKE 데몬이 자동 생성하지만, 수동 설정 시 빠뜨리기 쉽습니다.
- rp_filter 충돌: 터널 모드에서 내부 소스 주소가 수신 인터페이스의 서브넷에 없으면
rp_filter=1(strict)이 패킷을 드롭합니다. - firewall에서 ESP/IKE 미허용:
proto 50(ESP),udp 500(IKE),udp 4500(NAT-T) 모두 열어야 합니다. - MTU 미조정: ESP 오버헤드(50~73바이트)를 고려하지 않으면 PMTUD 블랙홀로 대용량 전송이 실패합니다.
- 시간 동기화 미비: 인증서 기반 IKE에서 시간이 어긋나면 인증서 검증이 실패합니다. NTP 필수.
ESP 패킷 형식 상세
ESP(Encapsulating Security Payload, IP 프로토콜 50)는 현대 IPSec의 핵심 프로토콜입니다. 트랜스포트 모드와 터널 모드에서 패킷 구조가 다르며, AEAD 알고리즘 사용 여부에 따라 내부 처리도 달라집니다.
/* ESP 헤더 (RFC 4303) — include/uapi/linux/ip.h */
struct ip_esp_hdr {
__be32 spi; /* Security Parameters Index — SA 식별 */
__be32 seq_no; /* 시퀀스 번호 (Anti-replay용, 단조 증가) */
__u8 enc_data[]; /* 가변 길이: IV + 암호화된 페이로드 */
};
/* ESP Trailer (암호화 영역 끝에 위치) */
/* [Padding (0~255 bytes)] — 블록 정렬용 */
/* [Pad Length (1 byte)] — 패딩 바이트 수 */
/* [Next Header (1 byte)] — 원본 프로토콜 (TCP=6, UDP=17 등) */
/* [ICV (8/12/16 bytes)] — Integrity Check Value (MAC) */
/* ESN (Extended Sequence Number, RFC 4304) */
/* 32비트 시퀀스 번호는 10Gbps에서 ~7분 만에 소진 */
/* ESN은 64비트로 확장: 상위 32비트는 패킷에 미포함, ICV 계산에만 사용 */
struct xfrm_replay_state_esn {
__u32 bmp_len; /* 비트맵 길이 (워드 수) */
__u32 oseq; /* 송신 시퀀스 (하위 32비트) */
__u32 seq; /* 수신 시퀀스 (하위 32비트) */
__u32 oseq_hi; /* 송신 시퀀스 (상위 32비트) */
__u32 seq_hi; /* 수신 시퀀스 (상위 32비트) */
__u32 replay_window; /* Anti-replay 윈도우 크기 */
__u32 bmp[]; /* 수신 비트맵 (가변 길이) */
};
crypto_aead API를, 개별 모드는 crypto_skcipher + crypto_ahash를 사용합니다.
AES-GCM의 IV는 Salt(4B, SA 생성 시 고정) + Nonce(8B, 패킷마다 증가)로 구성되며,
RFC 4106 기준 ICV는 8/12/16바이트가 가능하지만 일반적인 배포와 하드웨어 구현은 16바이트(128비트)를 기본값으로 씁니다.
AH 패킷 형식과 한계
AH(Authentication Header, IP 프로토콜 51)는 패킷의 무결성과 인증을 제공하지만 암호화는 하지 않습니다. IP 헤더를 포함한 전체 패킷이 인증 범위에 포함되는 것이 ESP와의 핵심 차이점이며, 이것이 NAT 환경과 호환되지 않는 근본 원인입니다.
/* AH 헤더 (RFC 4302) — include/uapi/linux/ip_auth.h */
struct ip_auth_hdr {
__u8 nexthdr; /* 다음 헤더 (TCP=6, ESP=50 등) */
__u8 hdrlen; /* 헤더 길이 (32비트 워드 단위 - 2) */
__be16 reserved; /* 예약 (0) */
__be32 spi; /* Security Parameters Index */
__be32 seq_no; /* 시퀀스 번호 */
__u8 auth_data[]; /* ICV — 가변 길이 (알고리즘에 따라) */
};
/* AH 인증 범위: IP 헤더 전체 + AH 헤더 + 페이로드 */
/* 단, 변경 가능(mutable) 필드는 0으로 치환 후 MAC 계산: */
/* - TTL (hop마다 감소) */
/* - Header Checksum (TTL 변경 시 재계산) */
/* - TOS/DSCP (라우터가 변경 가능) */
/* - Flags (Fragment offset) */
- NAT 비호환 — NAT는 IP 헤더의 src/dst 주소를 변경하는데, AH는 IP 헤더를 인증 범위에 포함. NAT 통과 시 ICV 검증 실패. NAT-T(UDP 캡슐화)도 AH에는 적용 불가
- 암호화 부재 — AH는 인증만 제공. ESP는 인증+암호화 모두 가능하므로 AH가 할 수 있는 것을 ESP가 모두 포함(ESP의 NULL 암호화 + 인증 = AH 동등)
- 성능 패널티 — mutable 필드를 0으로 치환하는 추가 처리. ESP 대비 실질적 이점 없이 복잡도만 증가
- RFC 7321 — IPSec 알고리즘 요구사항에서 AH를 MAY(선택)로 격하. IKEv2 구현에서 AH 지원은 필수가 아님
IPCOMP 프로토콜 상세
IPCOMP(IP Payload Compression, RFC 3173, IP 프로토콜 108)는 ESP/AH 암호화 전에 페이로드를 압축하여 대역폭을 절약하는 프로토콜입니다. 암호화된 데이터는 높은 엔트로피로 인해 압축이 불가능하므로, 반드시 IPCOMP → ESP 순서로 적용해야 합니다. 리눅스 커널에서는 SA 번들(bundle)로 IPCOMP와 ESP를 체이닝합니다.
level use를 설정해야 비압축 패킷도 수락합니다./* IPCOMP 헤더 (RFC 3173) — include/uapi/linux/ip_comp.h */
struct ip_comp_hdr {
__u8 nexthdr; /* 원본 프로토콜 (TCP=6, UDP=17 등) */
__u8 flags; /* 예약 (0) */
__be16 cpi; /* Compression Parameter Index */
};
/* net/ipv4/ipcomp.c — IPCOMP 송신 처리 */
static int ipcomp_output(struct xfrm_state *x, struct sk_buff *skb)
{
/* 1. 페이로드를 deflate로 압축 */
/* 2. 압축 결과가 원본보다 작은지 확인 */
if (compressed_len >= orig_len) {
/* 압축 효과 없음 → IPCOMP 헤더 없이 원본 전달 */
/* 수신 측은 ESP의 Next Header로 직접 판단 */
return 0;
}
/* 3. IPCOMP 헤더 삽입 (nexthdr, cpi) */
/* 4. 압축된 페이로드로 교체 → 다음 SA(ESP)에 전달 */
}
/* SA bundle에서 IPCOMP + ESP 체이닝 */
/* policy tmpl 예시:
* tmpl[0]: proto=comp, mode=transport, reqid=1, level=use
* tmpl[1]: proto=esp, mode=tunnel, reqid=1, level=required
* → IPCOMP 적용 시도 → ESP 암호화 (필수)
*/
# IPCOMP + ESP 터널 설정 예시
# 1. IPCOMP SA (CPI 기반)
ip xfrm state add src 203.0.113.1 dst 198.51.100.1 \
proto comp spi 0x1234 mode transport \
comp deflate reqid 100
# 2. ESP SA (같은 reqid로 번들링)
ip xfrm state add src 203.0.113.1 dst 198.51.100.1 \
proto esp spi 0x5678 mode tunnel \
aead 'rfc4106(gcm(aes))' 0x$(openssl rand -hex 20) 128 \
reqid 100
# 3. 정책: IPCOMP(use) + ESP(required) 템플릿
ip xfrm policy add src 192.168.1.0/24 dst 192.168.2.0/24 dir out \
tmpl src 203.0.113.1 dst 198.51.100.1 proto comp mode transport \
reqid 100 level use \
tmpl src 203.0.113.1 dst 198.51.100.1 proto esp mode tunnel \
reqid 100 level required
- 효과가 제한적: 이미 압축된 데이터(HTTPS, SSH, 미디어)에는 효과가 없습니다. 텍스트/로그 위주 트래픽에서만 의미 있습니다.
- CPU 오버헤드: deflate 압축은 CPU를 소모하며, 효과가 없어도 압축 시도 비용은 발생합니다.
- level use 필수: 수신 정책에서 IPCOMP tmpl의 level을
use로 설정해야 비압축 패킷도 수락합니다.required로 설정하면 비압축 패킷이 드롭됩니다. - strongSwan 기본 동작: strongSwan은 기본적으로 IPCOMP를 제안(proposal)에 포함합니다.
불필요하면
compress = no로 비활성화하세요. - HW offload 미지원: 현재 알려진 NIC에서 IPCOMP 하드웨어 오프로드를 지원하는 제품은 없습니다.
IKE 프로토콜과 SA 협상
IKE(Internet Key Exchange)는 IPSec SA의 자동 협상 프로토콜입니다. 커널의 xfrm 프레임워크는 데이터 평면(패킷 암호화/복호화)만 처리하며, SA 생성/삭제/갱신의 제어 평면은 유저스페이스 IKE 데몬(strongSwan, Libreswan)이 Netlink를 통해 커널에 주입합니다.
| 특성 | IKEv1 (RFC 2409) | IKEv2 (RFC 7296) |
|---|---|---|
| 교환 횟수 | Phase 1: 6~9 메시지 (Main/Aggressive) Phase 2: 3 메시지 (Quick Mode) |
IKE_SA_INIT: 2 메시지 IKE_AUTH: 2 메시지 총 4 메시지로 완료 |
| NAT-T 지원 | 확장(RFC 3947)으로 추가, 복잡 | 프로토콜에 내장 (NAT Detection payload) |
| 인증 방식 | PSK, RSA Signature, XAUTH(확장) | PSK, RSA/ECDSA Signature, EAP (내장) |
| DPD (Dead Peer) | 확장(RFC 3706), 선택적 구현 | 내장 (Informational Exchange) |
| MOBIKE | 미지원 | RFC 4555: IP 변경 시 SA 유지 (로밍) |
| CHILD_SA rekey | Phase 2 재협상 (일시 중단 가능) | CREATE_CHILD_SA로 무중단 rekey |
| 상태 | 레거시, 신규 배포 권장하지 않음 | 현행 표준, 모든 신규 배포 권장 |
Diffie-Hellman 그룹과 PFS: IKE_SA_INIT에서 DH 교환으로 공유 비밀 생성. PFS(Perfect Forward Secrecy)를 활성화하면 CREATE_CHILD_SA에서도 새로운 DH 교환을 수행하여, IKE SA 키가 노출되더라도 개별 CHILD SA(IPSec SA)의 트래픽 키는 보호됩니다. 주요 DH 그룹:
| 그룹 | 알고리즘 | 강도 | 권장 여부 |
|---|---|---|---|
| 14 | MODP 2048-bit | ~112비트 | 최소 권장 |
| 19 | ECP 256-bit (NIST P-256) | ~128비트 | 권장 |
| 20 | ECP 384-bit (NIST P-384) | ~192비트 | 고보안 |
| 21 | ECP 521-bit (NIST P-521) | ~256비트 | 고보안 |
| 31 | Curve25519 | ~128비트 | 권장 (고성능) |
커널과 IKE 데몬 상호작용:
IKE 데몬은 AF_NETLINK/NETLINK_XFRM 소켓을 통해 커널 xfrm 서브시스템과 통신합니다.
주요 Netlink 메시지:
/* include/uapi/linux/xfrm.h — 주요 XFRM Netlink 메시지 타입 */
/* SA (Security Association) 관리 */
XFRM_MSG_NEWSA /* IKE → 커널: SA 생성 (키, 알고리즘, SPI, 모드) */
XFRM_MSG_DELSA /* IKE → 커널: SA 삭제 */
XFRM_MSG_GETSA /* IKE → 커널: SA 조회 */
XFRM_MSG_UPDSA /* IKE → 커널: SA 갱신 (rekey) */
/* SP (Security Policy) 관리 */
XFRM_MSG_NEWPOLICY /* IKE → 커널: 정책 생성 (셀렉터, 방향, 액션) */
XFRM_MSG_DELPOLICY /* IKE → 커널: 정책 삭제 */
/* 커널 → IKE 이벤트 (비동기 알림) */
XFRM_MSG_ACQUIRE /* 커널 → IKE: 매칭 SA 없음, 새 SA 생성 요청 */
XFRM_MSG_EXPIRE /* 커널 → IKE: SA 수명 만료 (soft/hard) */
XFRM_MSG_MIGRATE /* MOBIKE: SA를 새 주소로 마이그레이션 */
XFRM_MSG_MAPPING /* NAT-T: NAT 매핑 변경 알림 */
/* 워크플로 예시:
* 1. 패킷 도착 → xfrm_policy 매칭 → 해당 SA 없음
* 2. 커널이 XFRM_MSG_ACQUIRE 전송 → IKE 데몬 수신
* 3. IKE 데몬이 피어와 IKEv2 교환 수행
* 4. IKE 데몬이 XFRM_MSG_NEWSA + XFRM_MSG_NEWPOLICY로 SA/SP 커널에 주입
* 5. 대기 중이던 패킷 처리 재개
*/
XFRM_MSG_MIGRATE로 SA의 주소를 동적으로 변경합니다.
SA 수명주기와 커널 이벤트
실제 운영에서는 "패킷이 왜 지금은 통과하고 1시간 뒤에는 안 통과하는가"가 중요합니다.
그 답은 대부분 SA 수명주기와 Netlink 이벤트에 있습니다. 커널은 패킷을 보다가 SA가
필요하면 ACQUIRE를 올리고, 수명이 다가오면 EXPIRE를 올리며,
HA 동기화가 필요한 환경에서는 replay/lifetime 값을 NEWAE로 흘려보냅니다.
| 이벤트 | 발생 시점 | 실무 해석 |
|---|---|---|
XFRM_MSG_ACQUIRE |
정책은 있는데 사용할 SA가 없을 때 | IKE 데몬이 trap policy를 받아 새 CHILD_SA를 만들어야 합니다. net.core.xfrm_acq_expires가 지나면 대기 패킷은 실패합니다. |
XFRM_MSG_EXPIRE |
soft/hard lifetime 도달 | soft는 rekey 신호, hard는 더 이상 사용 불가입니다. hard만 보고 있으면 순간 단절을 피하기 어렵습니다. |
XFRM_MSG_NEWAE |
replay/lifetime 임계값 초과 또는 타이머 만료 | HA 동기화용입니다. active 장비가 얼마나 bytes/seq를 썼는지 standby가 따라가야 failover 후 replay 오류를 줄일 수 있습니다. |
XFRM_MSG_MIGRATE |
MOBIKE나 주소 변경 시 | IP가 바뀌어도 IKE SA와 CHILD_SA를 유지하려는 시나리오입니다. NAT-T, 로밍, 멀티홈 환경에서 중요합니다. |
xfrm_sync는 replay 카운터와 lifetime byte 값을
listener에게 보내 active/standby 장비를 동기화하는 메커니즘을 설명합니다.
기본 sysctl은 xfrm_aevent_etime 1초, xfrm_aevent_rseqth 2패킷이며,
listener가 없으면 이벤트를 꺼 두는 것이 기본 동작입니다.
IPSec 고가용성 (HA) 상세
IPSec VPN 게이트웨이의 고가용성은 SA 상태 동기화가 핵심입니다. active 장비가 장애 시 standby가 동일한 SA(키, 시퀀스 번호, replay 윈도우)를 가지고 있어야 기존 터널이 재협상 없이 유지됩니다. 리눅스 커널의 NEWAE(Anti-replay Event) 메커니즘과 IKE 데몬의 HA 플러그인이 이를 지원합니다.
| HA 방식 | 동기화 대상 | 장점 | 단점 |
|---|---|---|---|
| SA 상태 동기화 | SA 키, SPI, 시퀀스 번호, replay 비트맵, lifetime 카운터 | 무중단 failover, 피어 재협상 불필요 | 동기화 지연 시 replay 윈도우 불일치 가능. 구현 복잡 |
| IKE SA만 동기화 | IKE SA 키와 상태만 동기화, CHILD_SA는 재협상 | 구현 단순. 동기화 데이터 적음 | failover 시 1~3초 단절 (CHILD_SA 재협상) |
| 재협상 방식 | 동기화 없음. standby가 인계 후 IKE부터 재협상 | 가장 단순. 동기화 인프라 불필요 | 5~10초 단절. DPD 타임아웃 대기 필요 |
# HA 관련 sysctl 파라미터
# NEWAE 이벤트 발생 조건 (replay 시퀀스 임계값)
sysctl -w net.core.xfrm_aevent_rseqth=2 # 2패킷마다 이벤트
# NEWAE 이벤트 발생 조건 (시간 임계값)
sysctl -w net.core.xfrm_aevent_etime=1000 # 1초(1000ms)마다 이벤트
# NEWAE 이벤트 모니터링
ip xfrm monitor aevent # anti-replay 이벤트만 관측
# SA 상태를 standby에 주입 (UPDSA)
# 실제로는 IKE 데몬의 HA 플러그인이 자동 처리
# strongSwan: ha 플러그인 (charon.plugins.ha)
# 설정 예:
# charon.plugins.ha.local = 10.0.0.1
# charon.plugins.ha.remote = 10.0.0.2
# charon.plugins.ha.segment_count = 2
# charon.plugins.ha.fifo_interface = yes
# Keepalived + IPSec HA 연동
# VRRP로 VIP failover → notify_master 스크립트에서 SA 활성화
- 동기화 지연: active에서 100패킷을 처리한 뒤 standby에 seq=50까지만 동기화된 상태에서 failover되면, standby가 seq=51부터 보내는데 피어는 이미 seq=100까지 수신했으므로 replay 체크를 통과하지만, 피어가 보낸 seq=51~100 패킷은 standby에서 이미 처리된 것으로 판정될 수 있습니다.
- 시퀀스 점프: 이를 완화하려면 failover 시 시퀀스를 N만큼 점프시키는 방법이 있습니다. strongSwan HA 플러그인은
segment_count를 사용하여 시퀀스 공간을 분할합니다. - 키 노출 위험: SA 키가 네트워크를 통해 동기화되므로, HA 채널 자체도 암호화(별도 IPSec 또는 MACsec)하거나 전용 네트워크로 격리해야 합니다.
xfrm과 네트워크 스택 통합
IPSec/xfrm 프레임워크는 Linux 네트워크 스택에 깊숙이 통합되어 있습니다. 다음 다이어그램은 전체 네트워크 플로우에서 xfrm이 어떻게 위치하고, Netfilter 훅 및 라우팅과 어떻게 상호작용하는지 보여줍니다.
| 처리 단계 | 송신 (TX) | 수신 (RX) |
|---|---|---|
| Netfilter 훅 위치 | OUTPUT → xfrm → POSTROUTING | ESP 복호화 → PREROUTING → xfrm policy 검증 |
| 라우팅 타이밍 | 라우팅 후 xfrm_lookup() 호출 | xfrm policy 검증 후 라우팅 결정 |
| xfrm 주요 함수 |
xfrm_lookup() → SPD 검색xfrm_output() → SA 적용esp_output() → 암호화
|
esp_input() → 복호화xfrm_input() → SA 매칭xfrm_policy_check() → SPD 검증
|
| 패킷 변환 |
평문 IP → ESP 캡슐화 터널 모드: 외부 IP 헤더 추가 |
ESP → 평문 IP 추출 터널 모드: 외부 IP 헤더 제거 |
| sk_buff 메타데이터 | skb_dst(skb)->xfrm에 SA 저장 |
skb->sp (secpath)에 처리된 SA 기록 |
| 정책 매칭 |
출력 인터페이스, 목적지 IP/포트로 SPD 검색 (셀렉터 매칭) |
복호화 후 내부 IP/포트로 SPD 검증 (inbound policy) |
| 실패 처리 |
SA 없음 → XFRM_MSG_ACQUIRE 전송IKE 데몬에게 협상 요청 |
SA 없음 → 패킷 드롭 Policy 불일치 → 드롭 |
- 독립적 처리: xfrm은 Netfilter 훅과 별도로 동작하며, ESP 암/복호화는 Netfilter 규칙보다 먼저 실행됩니다
- 수신 경로: ESP 패킷은 복호화 → PREROUTING 훅 → policy 검증 순서로 처리됩니다. PREROUTING에서 보이는 패킷은 이미 복호화된 평문입니다
- 송신 경로: OUTPUT 훅 통과 → 라우팅 → xfrm_lookup (정책 검색) → ESP 암호화 → POSTROUTING 훅 순서입니다
- 방화벽 규칙: IPSec 터널 내부 트래픽을 필터링하려면 INPUT/OUTPUT 훅을 사용하세요 (복호화 후 평문 상태)
- NAT 주의: DNAT는 PREROUTING에서, SNAT는 POSTROUTING에서 처리되므로 IPSec과 NAT를 함께 사용할 때 순서 주의 필요
IPSec과 GRO/GSO 통합
고성능 IPSec에서는 NIC의 GRO(Generic Receive Offload)와 GSO(Generic Segmentation Offload)가 ESP 처리와 어떻게 상호작용하는지가 중요합니다. 커널 4.18+에서는 ESP 전용 GRO/GSO 경로가 도입되어 대형 패킷을 세그먼트 단위로 분할하지 않고 한 번에 암복호화할 수 있습니다.
# ESP GRO/GSO 활성화 확인
ethtool -k eth0 | grep -i esp
# tx-esp-segmentation: on → ESP GSO 활성
# rx-gro-hw: on → HW GRO 활성
# esp-hw-offload: on → ESP 오프로드 활성
# GSO 최대 크기 확인/조정
ip -d link show eth0 | grep gso_max
# gso_max_size 65536 gso_max_segs 65535
# ESP GRO 통계 확인
ethtool -S eth0 | grep -i esp
# rx_esp_input_pkts, tx_esp_output_pkts 등 (드라이버 의존)
# SW GRO/GSO만으로도 성능 향상 가능 (HW 지원 불필요)
ethtool -K eth0 gro on gso on tso on
- GRO 수신:
esp4_gro_receive()는 동일 SA(SPI)의 연속 ESP 패킷을 하나의 super-packet으로 병합합니다. 병합된 패킷은esp_input()에서 한 번의 crypto 컨텍스트로 처리됩니다. - GSO 송신: TCP가 64KB super-packet을 내려보내면,
esp_xmit()에서 MSS 크기 세그먼트별로 ESP 헤더와 IV를 삽입하고 개별 암호화합니다. 시퀀스 번호는 세그먼트마다 증가합니다. - 제한 사항: UDP ESP 패킷은 GRO 병합이 제한적이며, NAT-T(UDP 캡슐화) 환경에서도 GRO가 동작하지만 효율이 다소 떨어질 수 있습니다.
- HW ESP offload + GSO: NIC가 inline crypto를 지원하면 GSO 세그먼트가 HW에서 암호화되어 CPU 사용이 거의 0에 가까워집니다.
xfrm 패킷 처리 경로 상세
xfrm의 패킷 처리는 Netfilter 훅과 밀접하게 통합되어 있습니다. 송신 경로에서는 라우팅 후 xfrm 정책 검색을 수행하고, 수신 경로에서는 ESP 복호화 후 정책 검증을 거칩니다.
/* net/xfrm/xfrm_output.c — 송신 경로 핵심 */
static int xfrm_output_one(struct sk_buff *skb, int err)
{
struct xfrm_state *x = skb_dst(skb)->xfrm;
/* 1. 시퀀스 번호 할당 (ESN 지원) */
err = x->outer_mode.output(x, skb); /* 터널: 외부 IP 헤더 추가 */
err = x->type->output(x, skb); /* ESP: esp_output() 호출 */
/* 2. skb→dst를 외부 라우팅 엔트리로 교체 */
/* 3. 중첩 SA가 있으면 다음 xfrm_state에 대해 반복 (bundle) */
}
/* net/ipv4/esp4.c — ESP 암호화 처리 */
static int esp_output(struct xfrm_state *x, struct sk_buff *skb)
{
struct crypto_aead *aead = x->data;
/* 1. ESP 헤더 (SPI + Seq#) 삽입 */
/* 2. IV 생성 (AEAD: salt + seq_no) */
/* 3. 패딩 추가 (블록 크기 정렬) */
/* 4. aead_request 생성 → crypto_aead_encrypt() */
/* → 비동기 완료: esp_output_done() 콜백 */
/* 5. ICV 첨부 */
}
/* net/xfrm/xfrm_input.c — 수신 경로 핵심 */
int xfrm_input(struct sk_buff *skb, int nexthdr,
__be32 spi, int encap_type)
{
/* 1. (daddr, spi, proto)로 SAD 해시 테이블 검색 */
x = xfrm_state_lookup(net, &daddr, spi, nexthdr, family);
/* 2. anti-replay 검사 */
xfrm_replay_check(x, skb, seq);
/* 3. ESP 복호화: x→type→input() → esp_input() */
/* 4. anti-replay 윈도우 업데이트 */
xfrm_replay_advance(x, seq);
/* 5. 정책 검증: 복호화된 패킷이 SP와 일치하는지 확인 */
/* (수신 정책 없으면 드롭 — XfrmInNoPols) */
}
/* xfrm_state 해시 테이블 검색 — O(1) 평균 */
/* 키: (daddr, spi, proto) → 해시 버킷 → 체인 순회 */
/* 대규모 SA 환경에서도 검색 성능 보장 */
xfrm_lookup()에서 정책에 매칭되면 xfrm_bundle_create()가 호출되어
SA 체인(여러 SA를 순서대로 적용: 예컨대 IPCOMP → ESP)을 생성합니다.
이 번들은 dst_entry에 캐싱되어 동일 흐름의 후속 패킷은 정책 검색 없이
바로 SA를 적용합니다. 라우팅 테이블 변경이나 SA 만료 시 캐시가 무효화됩니다.
xfrm_policy 내부 구조
/* include/net/xfrm.h — Security Policy 핵심 구조체 */
struct xfrm_policy {
struct hlist_node bydst; /* dst 주소별 해시 체인 */
struct hlist_node byidx; /* 인덱스별 해시 체인 */
struct xfrm_selector selector; /* 트래픽 셀렉터 (아래 상세) */
struct xfrm_lifetime_cfg lft; /* 수명: 바이트/패킷/시간 */
struct xfrm_lifetime_cur curlft; /* 현재 사용량 카운터 */
u8 type; /* XFRM_POLICY_TYPE_MAIN / SUB */
u8 action; /* XFRM_POLICY_ALLOW / BLOCK */
u8 flags; /* XFRM_POLICY_LOCALOK, ICMP 등 */
u8 xfrm_nr; /* tmpl 배열 크기 (최대 6) */
u16 family; /* AF_INET / AF_INET6 */
u32 priority; /* 정책 우선순위 (낮을수록 높음) */
u32 if_id; /* xfrm interface ID (4.19+) */
struct xfrm_tmpl xfrm_vec[XFRM_MAX_DEPTH]; /* SA 템플릿 */
/* tmpl: 요구하는 SA의 속성 (proto, mode, reqid, level) */
};
/* 트래픽 셀렉터 — 어떤 패킷에 정책을 적용할지 결정 */
struct xfrm_selector {
xfrm_address_t daddr; /* 목적지 주소 */
xfrm_address_t saddr; /* 소스 주소 */
__be16 dport; /* 목적지 포트 */
__be16 dport_mask; /* 포트 마스크 (0xFFFF = exact) */
__be16 sport; /* 소스 포트 */
__be16 sport_mask;
__u16 family; /* AF_INET / AF_INET6 */
__u8 prefixlen_d; /* 목적지 서브넷 길이 */
__u8 prefixlen_s; /* 소스 서브넷 길이 */
__u8 proto; /* 프로토콜 (6=TCP, 17=UDP, 0=all) */
int ifindex; /* 인터페이스 바인딩 */
__kernel_uid32_t user; /* UID 기반 정책 (Android) */
};
SPD 검색 알고리즘:
정책 검색은 3개의 방향(in/out/fwd)별로 독립된 해시 테이블에서 수행됩니다.
패킷의 (src, dst, proto, sport, dport)를 셀렉터와 매칭하며,
여러 정책이 매칭되면 priority가 가장 낮은(= 우선순위 높은) 정책이 선택됩니다.
ip xfrm policy set hthresh4 LBITS RBITS,
ip xfrm policy set hthresh6 LBITS RBITS로 해시 임계값을 조정할 수 있습니다.
prefix가 짧은 광범위 정책은 inexact chain에 남기 쉽기 때문에, 대규모 SPD에서는 broad selector를 최소화하는 편이 좋습니다.
/* net/xfrm/xfrm_policy.c — SPD 검색 핵심 */
static struct xfrm_policy *
xfrm_policy_lookup_bytype(struct net *net, u8 type,
const struct flowi *fl, u16 family, u8 dir)
{
/* 1. (dst_addr, family) 기반 해시 버킷 선택 */
/* 2. 버킷 내 정책 순회 → 셀렉터 매칭 검사 */
/* xfrm_selector_match(sel, fl, family) */
/* 3. 매칭된 정책 중 priority 최소값 반환 */
/* 4. action == BLOCK이면 패킷 드롭 (XfrmOutPolBlock) */
/* 5. action == ALLOW이면 tmpl 배열로 SA 검색 */
}
/* 정책 방향 (dir) */
XFRM_POLICY_IN 0 /* 수신: 복호화 후 정책 검증 */
XFRM_POLICY_OUT 1 /* 송신: 패킷 나가기 전 정책 검색 */
XFRM_POLICY_FWD 2 /* 포워딩: 라우터 역할 시 터널 간 전달 */
xfrm_policy_check()는 Netfilter의 NF_INET_PRE_ROUTING 이후,
ip_local_deliver() 이전에 호출됩니다.
복호화된 패킷의 셀렉터가 수신 정책(XFRM_POLICY_IN)과 일치하지 않으면
패킷이 드롭되어, 정책 우회 공격을 방지합니다.
이는 "수신 시에도 반드시 정책 검증"이라는 IPSec의 보안 원칙을 구현합니다.
PF_KEY vs NETLINK_XFRM API
커널 xfrm 서브시스템과 유저스페이스 IKE 데몬 간 통신에는 두 가지 API가 있습니다. PF_KEY(RFC 2367)는 BSD 유래의 레거시 인터페이스이고, NETLINK_XFRM은 리눅스 전용의 현대 인터페이스입니다. 현재 모든 주요 IKE 데몬은 NETLINK_XFRM을 기본으로 사용합니다.
| 특성 | PF_KEY (AF_KEY) | NETLINK_XFRM |
|---|---|---|
| 표준 | RFC 2367 (1998) | 리눅스 전용 (include/uapi/linux/xfrm.h) |
| 소켓 타입 | socket(PF_KEY, SOCK_RAW, PF_KEY_V2) |
socket(AF_NETLINK, SOCK_DGRAM, NETLINK_XFRM) |
| 기능 범위 | SA 관리 (SADB_*), 제한적 SPD | SA + SPD + MIGRATE + NEWAE + 모니터링 + offload 전체 |
| if_id 지원 | 미지원 | 지원 (xfrm interface, route-based VPN) |
| mark/output-mark | 미지원 | 지원 |
| HW offload | 미지원 | 지원 (XFRMA_OFFLOAD_DEV) |
| ESN | 제한적 (커널 확장) | 완전 지원 (XFRMA_REPLAY_ESN_VAL) |
| IKE 데몬 | Racoon (ipsec-tools, 폐기됨) | strongSwan, Libreswan, iproute2 |
| 커널 코드 | net/key/af_key.c | net/xfrm/xfrm_user.c |
| 권장 여부 | 레거시, 비권장 | 현행 표준, 모든 신규 배포 권장 |
/* NETLINK_XFRM 주요 메시지 속성 (NLA — Netlink Attribute) */
/* include/uapi/linux/xfrm.h */
/* SA 생성 시 포함되는 주요 속성 */
XFRMA_ALG_AEAD /* AEAD 알고리즘 (AES-GCM) */
XFRMA_ALG_AUTH_TRUNC /* 인증 알고리즘 + truncation 길이 */
XFRMA_ALG_CRYPT /* 암호화 알고리즘 */
XFRMA_ENCAP /* NAT-T 캡슐화 정보 */
XFRMA_REPLAY_ESN_VAL /* ESN 상태/윈도우 */
XFRMA_OFFLOAD_DEV /* H/W offload 대상 디바이스 */
XFRMA_IF_ID /* xfrm interface 식별자 */
XFRMA_SET_MARK /* SA에 적용할 fwmark */
XFRMA_SET_MARK_MASK /* fwmark 마스크 */
XFRMA_SA_PCPU /* per-CPU SA 분산 (6.7+) */
/* PF_KEY → NETLINK_XFRM 매핑 예시 */
/* SADB_ADD → XFRM_MSG_NEWSA */
/* SADB_DELETE → XFRM_MSG_DELSA */
/* SADB_ACQUIRE → XFRM_MSG_ACQUIRE */
/* SADB_X_SPDADD → XFRM_MSG_NEWPOLICY */
/* SADB_EXPIRE → XFRM_MSG_EXPIRE */
CONFIG_NET_KEY는 커널에 남아 있지만 새로운 xfrm 기능(if_id, packet offload, per-CPU SA, ESN 확장 등)은
NETLINK_XFRM에만 추가됩니다. PF_KEY를 사용하는 Racoon/ipsec-tools는 2015년 이후 사실상 관리되지 않으며,
최신 커널의 기능을 활용할 수 없습니다. 레거시 시스템에서 마이그레이션 시 strongSwan 또는 Libreswan으로 전환하세요.
NAT Traversal (NAT-T) 상세
ESP는 IP 프로토콜 번호 50을 사용하므로, 포트 번호가 없어 일반 NAT가 처리할 수 없습니다. NAT-T(NAT Traversal, RFC 3948)는 ESP 패킷을 UDP 4500 포트로 캡슐화하여 NAT 장비를 통과할 수 있게 합니다.
UDP 4500 위에서는 세 가지가 공존합니다. IKE는 UDP 헤더 뒤에
4바이트 Non-ESP Marker(0x00000000)를 두고, NAT keepalive는
1바이트 0xFF만 보내며, UDP-encapsulated ESP는 곧바로
ESP Header의 0이 아닌 SPI가 시작됩니다.
/* NAT-T 감지: IKEv2 NAT_DETECTION_*_IP payload */
/* IKE_SA_INIT 교환에서 양쪽이 NAT 감지 해시 전송:
* HASH = SHA-1(SPIi | SPIr | IP | port)
* 수신 측에서 자신의 IP/port로 재계산한 해시와 비교
* → 불일치하면 경로 상에 NAT 존재 → NAT-T 활성화
*/
/* 커널 NAT-T 처리: net/ipv4/esp4.c + net/ipv4/udp.c */
/* 수신: UDP 4500 소켓에 ESP-in-UDP 핸들러 등록 */
static int esp4_rcv_cb(struct sk_buff *skb)
{
/* 1. UDP 헤더 제거 */
/* 2. SPI로 xfrm_state 검색 */
/* 3. encap_type = UDP_ENCAP_ESPINUDP 설정 */
/* 4. esp_input()으로 복호화 진행 */
}
/* 송신: xfrm_state에 encap 정보가 있으면 UDP 래핑 */
struct xfrm_encap_tmpl {
__u16 encap_type; /* UDP_ENCAP_ESPINUDP (2) */
__be16 encap_sport; /* 로컬 UDP 포트 (4500) */
__be16 encap_dport; /* 원격 UDP 포트 (4500) */
xfrm_address_t encap_oa; /* 원본 주소 (NAT 이전) */
};
- Full Cone NAT — NAT-T로 문제 없이 통과
- Restricted/Port Restricted NAT — Keep-alive 패킷(20~30초 간격)으로 NAT 매핑 유지 필요
- Symmetric NAT — 목적지마다 다른 외부 포트 할당. IKE에서 감지한 포트와 ESP의 실제 매핑이 다를 수 있어 연결 실패 가능. MOBIKE의 주소 업데이트로 완화
- 이중 NAT — 양쪽 모두 NAT 뒤에 있는 경우. NAT-T 필수이며, 양쪽 IKE 데몬이 모두 NAT를 감지해야 함
xfrm interface vs VTI
리눅스에서 route-based VPN을 구현하는 두 가지 방법이 있습니다: 레거시 VTI(Virtual Tunnel Interface)와 커널 4.19에서 도입된 xfrm interface입니다. xfrm interface는 VTI의 한계를 해결하고 현대 VPN 아키텍처에 필수적인 기능을 제공합니다.
| 특성 | VTI (ip_vti) | xfrm interface (커널 4.19+) |
|---|---|---|
| 인터페이스 생성 | ip tunnel add vti0 mode vti ... |
ip link add xfrm0 type xfrm ... |
| SA 바인딩 | 터널 src/dst IP 주소로 매칭 | if_id 정수값으로 매칭 (IP 무관) |
| 다중 터널 | 동일 피어에 하나의 VTI만 가능 | 서로 다른 if_id로 다중 터널 가능 |
| 네트워크 네임스페이스 | 제한적 (SA와 같은 netns에만) | 완전 지원 (인터페이스와 SA 분리 가능) |
| IPv4/IPv6 통합 | vti (IPv4), vti6 (IPv6) 별도 | 단일 인터페이스로 IPv4/IPv6 모두 처리 |
| 멀티 테넌트 | 비실용적 | VRF + netns + if_id 조합으로 완전 격리 |
| 라우팅 통합 | 기본적 | 완전한 route-based VPN (BGP/OSPF over IPSec) |
# xfrm interface 생성 및 설정
# 1. xfrm interface 생성 (if_id=42로 SA와 바인딩)
ip link add xfrm0 type xfrm dev eth0 if_id 42
ip addr add 10.10.0.1/30 dev xfrm0
ip link set xfrm0 up
# 2. SA에 if_id 지정
ip xfrm state add src 203.0.113.1 dst 198.51.100.1 \
proto esp spi 0x1000 mode tunnel if_id 42 \
aead 'rfc4106(gcm(aes))' 0x$(openssl rand -hex 20) 128
# 3. 정책에 if_id 지정
ip xfrm policy add dir out if_id 42 \
src 0.0.0.0/0 dst 0.0.0.0/0 \
tmpl src 203.0.113.1 dst 198.51.100.1 proto esp mode tunnel
# 4. 라우팅: xfrm interface를 통해 터널 트래픽 라우팅
ip route add 192.168.2.0/24 dev xfrm0
# 멀티 터널 시나리오 (서로 다른 피어에 대해 별도 xfrm interface)
ip link add xfrm1 type xfrm dev eth0 if_id 43
ip link add xfrm2 type xfrm dev eth0 if_id 44
# → BGP/OSPF 동적 라우팅을 각 xfrm interface에서 실행 가능
# 네임스페이스 격리 (멀티 테넌트)
ip netns add tenant1
ip link set xfrm0 netns tenant1
ip netns exec tenant1 ip addr add 10.10.0.1/30 dev xfrm0
ip netns exec tenant1 ip link set xfrm0 up
# → tenant1 네임스페이스 내에서만 IPSec 터널 접근 가능
ip xfrm policy의 셀렉터로 트래픽을 직접 매칭합니다.
설정이 간단하지만 동적 라우팅과 호환이 어렵습니다.
Route-based VPN은 xfrm interface에 라우팅 엔트리를 추가하여 트래픽을 유도합니다.
BGP/OSPF 같은 동적 라우팅 프로토콜을 IPSec 위에서 실행할 수 있어
대규모 사이트 간 VPN(수백 개 터널)에 필수적입니다.
install_routes_xfrmi를 사용할 때 IKE/ESP 패킷 자체가
xfrm interface로 다시 라우팅되지 않도록 fwmark 또는 peer에 대한
throw route를 별도로 두라고 권장합니다. 일반적인 패턴은
socket-default.fwmark와 set_mark_out으로 IKE/ESP를 표시하고,
table 220 같은 xfrmi 전용 라우팅 테이블에서 그 mark를 제외하는 방식입니다.
xfrm 네임스페이스와 네트워크 격리
리눅스 네트워크 네임스페이스(netns)는 xfrm SPD/SAD를 완전히 격리합니다. 각 네임스페이스는 독립된 정책/상태 테이블, 통계 카운터, sysctl 파라미터를 가지며, xfrm interface는 네임스페이스 간 이동이 가능하여 멀티 테넌트 VPN 아키텍처를 구현할 수 있습니다.
# 멀티 테넌트 VPN 네임스페이스 설정 예시
# 1. 테넌트 네임스페이스 생성
ip netns add tenant-a
ip netns add tenant-b
# 2. xfrm interface 생성 (호스트 netns)
ip link add xfrm-a type xfrm dev eth0 if_id 100
ip link add xfrm-b type xfrm dev eth0 if_id 200
# 3. xfrm interface를 테넌트 netns로 이동
ip link set xfrm-a netns tenant-a
ip link set xfrm-b netns tenant-b
# 4. 테넌트별 주소/라우팅 설정
ip netns exec tenant-a bash -c '
ip addr add 10.10.1.1/30 dev xfrm-a
ip link set xfrm-a up
ip route add 192.168.100.0/24 dev xfrm-a
'
ip netns exec tenant-b bash -c '
ip addr add 10.10.2.1/30 dev xfrm-b
ip link set xfrm-b up
ip route add 192.168.200.0/24 dev xfrm-b
'
# 5. SA/SP는 호스트 netns에서 설치 (if_id로 바인딩)
ip xfrm state add src 203.0.113.1 dst 198.51.100.1 \
proto esp spi 0x100 mode tunnel if_id 100 \
aead 'rfc4106(gcm(aes))' 0x$(openssl rand -hex 20) 128
ip xfrm policy add dir out if_id 100 \
src 0.0.0.0/0 dst 0.0.0.0/0 \
tmpl src 203.0.113.1 dst 198.51.100.1 proto esp mode tunnel
# 6. 네임스페이스 간 xfrm 이벤트 모니터링
ip xfrm monitor all-nsid # 모든 netns의 xfrm 이벤트 통합 관측
# 7. 테넌트별 xfrm 통계 확인
ip netns exec tenant-a cat /proc/net/xfrm_stat
- Cilium은 노드 간 Pod 트래픽을 IPSec(ESP)로 암호화합니다. 커널 xfrm을 사용하며, per-node SA를 자동 관리합니다.
cilium encrypt status로 확인. - Calico도 IPSec 모드(
ipsecMode: Always)를 지원하며, LibreSwan을 IKE 데몬으로 사용합니다. - WireGuard 대안: Cilium은 커널 5.6+에서 WireGuard도 지원하며, xfrm 대비 설정이 단순하고 성능이 좋을 수 있습니다.
- Service Mesh: mTLS(Istio/Linkerd)는 L7에서 암호화하므로 IPSec과 중복 적용하면 이중 암호화 오버헤드가 발생합니다.
- 네임스페이스 격리: Pod별 netns에서 xfrm 통계가 독립되므로, 장애 진단 시 올바른 netns에서
/proc/net/xfrm_stat을 확인해야 합니다.
Anti-replay 메커니즘
Anti-replay는 공격자가 캡처한 ESP 패킷을 재전송하는 것을 방지합니다. 수신 측은 슬라이딩 윈도우 비트맵을 유지하여 이미 처리한 시퀀스 번호의 패킷을 거부합니다.
/* net/xfrm/xfrm_replay.c — anti-replay 검사 (ESN 모드) */
static int xfrm_replay_check_esn(struct xfrm_state *x,
struct sk_buff *skb, __be32 net_seq)
{
u32 seq = ntohl(net_seq);
struct xfrm_replay_state_esn *replay_esn = x->replay_esn;
u32 wsize = replay_esn->replay_window;
u32 top = replay_esn->seq; /* 최신 수신 시퀀스 */
u32 bottom = top - wsize + 1; /* 윈도우 왼쪽 경계 */
/* Case 1: 윈도우 오른쪽 밖 → 새 패킷, 수락 */
if (likely(seq > top))
return 0;
/* Case 2: 윈도우 왼쪽 밖 → 너무 오래된 패킷, 드롭 */
if (seq < bottom)
return -EINVAL; /* XfrmInSeqOutOfWindow */
/* Case 3: 윈도우 내 → 비트맵 검사 */
u32 diff = top - seq;
u32 pos = diff / 32;
u32 bit = 1 << (diff % 32);
if (replay_esn->bmp[pos] & bit)
return -EINVAL; /* XfrmInStateReplay — 중복 패킷 */
return 0; /* 윈도우 내 미수신 패킷, 수락 */
}
/* 윈도우 업데이트: 패킷 수락 후 비트맵 갱신 */
static void xfrm_replay_advance_esn(struct xfrm_state *x, __be32 net_seq)
{
/* seq > top이면 윈도우 오른쪽으로 슬라이드 */
/* 이동 과정에서 벗어난 비트들은 0으로 클리어 */
/* 새 seq 위치의 비트를 1로 설정 */
}
ip xfrm state add ... replay-window 2048로 확대하거나,
ESN 활성화 시 최대 4096까지 설정 가능합니다.
/proc/net/xfrm_stat의 XfrmInSeqOutOfWindow 카운터가 증가하면
윈도우 확대가 필요합니다.
RFC 4301은 서로 다른 QoS/재정렬 특성을 가진 트래픽을 같은 selector 하나로 몰아넣지 말라고 봅니다. 대역폭이 높고 RSS/ECMP 재정렬이 심한 환경이라면 윈도우만 키우는 것보다, 트래픽 클래스를 SA 단위로 나누는 쪽이 더 안정적일 수 있습니다.
IPSec 하드웨어 오프로드
소프트웨어 ESP 처리는 CPU 집약적이어서 10Gbps 이상 환경에서 병목이 됩니다. 리눅스 커널의 공식 XFRM device API는 두 가지 오프로드만 정의합니다: crypto offload와 packet offload입니다. 업계에서 말하는 "inline crypto", "full offload", "DPU offload"는 대개 이 둘, 특히 packet offload의 구현 형태를 가리키는 벤더 용어입니다.
| 커널 모드 | 하드웨어가 하는 일 | 커널이 계속 하는 일 | 대표 장비 | 비고 |
|---|---|---|---|---|
| Crypto offload | encrypt/decrypt만 수행 | ESP 헤더 추가/제거, 시퀀스 번호, replay, 정책 판단, 수명 관리는 커널이 담당 | Intel QAT, 일부 SmartNIC/NIC의 crypto mode | ip xfrm state ... offload dev eth0. 실패 시 소프트웨어 fallback이 가능한 경우가 많음 |
| Packet offload | encrypt/decrypt + encapsulation, 그리고 SA/policy 상태 일부를 HW가 유지 | 커널은 key manager와 정책/상태 동기화 주체로 남음 | NVIDIA ConnectX 계열, Intel E810, 일부 DPU | offload packet 필요. state뿐 아니라 policy offload까지 함께 다뤄야 함 |
crypto offload와 packet offload 두 가지뿐입니다.
벤더가 말하는 inline crypto는 대개 packet offload의 데이터 경로 구현을,
full offload / DPU offload는 packet offload에 스위칭/steering까지 결합한 상품 형태를 뜻합니다.
Crypto API 관점의 IPsec 오프로드: xfrmdev_ops 콜백, crypto vs packet offload 선택 기준, kTLS/MACsec과의 비교, PCI 가속기(QAT) 연동은 Crypto Framework — 네트워크 암호화 오프로드에서 종합적으로 다룹니다.
/* include/net/xfrm.h — H/W 오프로드 구조체 */
struct xfrm_dev_offload {
struct net_device *dev; /* 오프로드 대상 NIC */
struct net_device *real_dev; /* bond/vlan 하위 실제 디바이스 */
unsigned long offload_handle; /* 드라이버 전용 핸들 */
u8 dir : 2; /* XFRM_DEV_OFFLOAD_IN / OUT */
u8 type : 2; /* CRYPTO / PACKET */
u8 flags : 2; /* XFRM_DEV_OFFLOAD_FLAG_ACE 등 */
};
/* NIC 드라이버가 구현하는 xdo_dev_* 콜백 */
struct xfrmdev_ops {
int (*xdo_dev_state_add)(struct net_device *dev,
struct xfrm_state *x,
struct netlink_ext_ack *extack);
void (*xdo_dev_state_delete)(struct net_device *dev,
struct xfrm_state *x);
void (*xdo_dev_state_free)(struct net_device *dev,
struct xfrm_state *x);
bool (*xdo_dev_offload_ok)(struct sk_buff *skb,
struct xfrm_state *x);
/* packet offload는 정책 콜백도 필요 */
int (*xdo_dev_policy_add)(struct xfrm_policy *p,
struct netlink_ext_ack *extack);
};
# Inline crypto offload 설정 (NVIDIA ConnectX-6 Dx 예시)
# 1. crypto offload: 암복호화만 HW
ip xfrm state add src 203.0.113.1 dst 198.51.100.1 \
proto esp spi 0x1000 mode tunnel \
aead 'rfc4106(gcm(aes))' 0x$(openssl rand -hex 20) 128 \
offload dev eth0 dir out
# 2. packet offload: state + policy를 HW와 동기화
ip xfrm state add src 203.0.113.1 dst 198.51.100.1 \
proto esp spi 0x2000 mode transport \
aead 'rfc4106(gcm(aes))' 0x$(openssl rand -hex 20) 128 \
sel src 203.0.113.1/32 dst 198.51.100.1/32 proto tcp dport 443 \
offload packet dev eth0 dir out
ip xfrm policy add src 203.0.113.1/32 dst 198.51.100.1/32 proto tcp dport 443 \
dir out offload packet dev eth0 \
tmpl src 203.0.113.1 dst 198.51.100.1 proto esp mode transport
# 3. 오프로드 상태 확인
ip xfrm state list
ip xfrm policy list
# → "offload packet dev eth0" 또는 "offload dev eth0" 표시
# Intel QAT crypto offload (AEAD)
# QAT 드라이버 로드 → openssl engine → strongSwan에서 QAT 플러그인 사용
modprobe qat_4xxx # Intel 4세대 QAT 디바이스
# strongSwan: charon.plugins.openssl.engine_id = qatengine
# 오프로드 실패 시 자동 소프트웨어 폴백
# ethtool -k eth0 | grep esp
# esp-hw-offload: on → 하드웨어 오프로드 활성화됨
- Crypto offload: 도입이 가장 쉽습니다. 커널 의미론이 거의 그대로 유지되어 디버깅과 fallback이 단순합니다.
- Packet offload: 성능은 가장 좋지만 driver가 policy/state/lifetime 통계를 정확히 올려야 합니다. unsupported면 단순 fallback이 어려울 수 있습니다.
- DPU/inline/full: 커널 generic 타입이 아니라 packet offload 위에 벤더 데이터 경로를 얹은 형태로 이해하는 편이 정확합니다.
strongSwan/Libreswan 실전 설정
IKE 데몬은 커널 xfrm과 협력하여 SA의 자동 생성/갱신/삭제를 처리합니다. 현대 리눅스 환경에서는 strongSwan(swanctl)과 Libreswan(ipsec.conf)이 주로 사용됩니다.
# /etc/swanctl/swanctl.conf — strongSwan site-to-site 설정
connections {
site-to-site {
version = 2 # IKEv2 전용
local_addrs = 203.0.113.1
remote_addrs = 198.51.100.1
local {
auth = pubkey # X.509 인증서 인증
certs = server.pem
id = vpn.example.com
}
remote {
auth = pubkey
id = vpn.peer.com
}
proposals = aes256gcm128-x25519-sha256 # IKE SA 암호 스위트
dpd_delay = 30s # DPD 간격
children {
lan-to-lan {
local_ts = 192.168.1.0/24 # 로컬 트래픽 셀렉터
remote_ts = 192.168.2.0/24 # 원격 트래픽 셀렉터
esp_proposals = aes256gcm128-x25519 # CHILD SA 암호 스위트
rekey_time = 3600s # 1시간마다 rekey
replay_window = 2048 # Anti-replay 윈도우
start_action = start # 부팅 시 자동 연결
dpd_action = restart # DPD 실패 시 재연결
# hw_offload = packet # inline crypto 오프로드 (지원 NIC)
}
}
}
}
# Road Warrior (모바일 클라이언트) 설정
connections {
roadwarrior {
version = 2
local_addrs = %any # 서버: 모든 주소에서 수신
pools = pool-ipv4 # 클라이언트에게 IP 할당
local {
auth = pubkey
certs = server.pem
}
remote {
auth = eap-mschapv2 # EAP 인증 (사용자/비밀번호)
eap_id = %any
}
children {
rw-child {
local_ts = 0.0.0.0/0 # 모든 트래픽 터널링
}
}
}
}
pools {
pool-ipv4 {
addrs = 10.10.0.0/24
dns = 8.8.8.8, 8.8.4.4
}
}
| 작업 | strongSwan (swanctl) | Libreswan (ipsec) |
|---|---|---|
| 설정 로드 | swanctl --load-all |
ipsec auto --add conn-name |
| 연결 시작 | swanctl --initiate --child lan-to-lan |
ipsec auto --up conn-name |
| SA 목록 | swanctl --list-sas |
ipsec whack --trafficstatus |
| 연결 종료 | swanctl --terminate --child lan-to-lan |
ipsec auto --down conn-name |
| 디버그 로그 | swanctl --log --level 2 |
ipsec whack --debug-all |
| 인증서 목록 | swanctl --list-certs |
ipsec whack --listcerts |
| 설정 파일 | /etc/swanctl/swanctl.conf |
/etc/ipsec.conf + /etc/ipsec.secrets |
| 커널 연동 | charon.plugins.kernel-netlink |
pluto 데몬 → NETLINK_XFRM |
charon 데몬은 kernel-netlink 플러그인으로
NETLINK_XFRM 소켓을 통해 커널과 통신합니다.
swanctl --load-all 실행 시 설정이 charon에 로드되고,
IKEv2 교환 완료 후 XFRM_MSG_NEWSA/XFRM_MSG_NEWPOLICY로
SA/SP를 커널에 주입합니다.
kernel-netlink 플러그인 설정:
charon.plugins.kernel-netlink.xfrm_acq_expires = 165 (ACQUIRE 타임아웃),
charon.plugins.kernel-netlink.set_mark = yes (fwmark 연동).
IPSec 성능 튜닝
IPSec 성능은 암호 알고리즘, CPU 아키텍처, 패킷 크기, NIC 설정에 크게 의존합니다. 고성능 환경에서는 체계적인 벤치마크와 프로파일링이 필수적입니다.
| 알고리즘 | x86_64 (AES-NI) | ARM64 (NEON/CE) | 비고 |
|---|---|---|---|
| AES-128-GCM | ~40 Gbps | ~8 Gbps (ARMv8 CE) | AES-NI + CLMUL 하드웨어 가속. 가장 보편적 |
| AES-256-GCM | ~32 Gbps | ~6 Gbps | AES-128 대비 ~20% 느림 (4 라운드 추가) |
| ChaCha20-Poly1305 | ~15 Gbps | ~10 Gbps (NEON) | AES-NI 없는 환경에서 고성능. ARM에서 AES-GCM보다 빠를 수 있음 |
| AES-256-CBC + HMAC-SHA256 | ~12 Gbps | ~3 Gbps | 2-pass 처리. 레거시 호환용. AEAD 대비 ~60% 느림 |
# CPU affinity와 RPS/RFS 최적화
# ESP 처리를 특정 CPU에 고정하여 캐시 효율 극대화
# 1. NIC 인터럽트를 특정 CPU에 바인딩
# (CPU 0~3: 일반 트래픽, CPU 4~7: ESP 처리)
for i in /proc/irq/*/smp_affinity_list; do
irq=$(echo $i | grep -oP '/proc/irq/\K[0-9]+')
cat /proc/irq/$irq/actions | grep -q eth0 && echo "4-7" > $i
done
# 2. RPS (Receive Packet Steering) — 소프트웨어 수신 분산
echo f0 > /sys/class/net/eth0/queues/rx-0/rps_cpus # CPU 4-7
echo 4096 > /sys/class/net/eth0/queues/rx-0/rps_flow_cnt
# 3. xfrm 관련 sysctl 파라미터
sysctl -w net.core.xfrm_larval_drop=1 # SA 미완성 시 패킷 즉시 드롭 (대기 안 함)
sysctl -w net.core.xfrm_acq_expires=30 # ACQUIRE 타임아웃 (초)
sysctl -w net.core.xfrm_aevent_rseqth=2 # replay 이벤트 시퀀스 임계값
sysctl -w net.ipv4.xfrm4_gc_thresh=32768 # xfrm dst 가비지 컬렉션 임계값
# perf 프로파일링 — ESP 처리 핫스팟 식별
perf top -C 4-7 -g # ESP 처리 CPU에서 실시간 프로파일
perf record -C 4-7 -g -- sleep 10 # 10초 샘플링
perf report --sort=dso,sym # 심볼별 CPU 사용량
# 주요 핫스팟: gcm_hash_crypt_*, aesni_ctr_enc, esp_output/input
# 암호 알고리즘 벤치마크 (커널 crypto API 테스트)
modprobe tcrypt sec=1 mode=211 # AES-GCM 벤치마크
dmesg | tail -50 # 결과 확인
- SAD 해시 테이블 — SA 수가 많으면 해시 충돌 증가.
xfrm4_gc_thresh를 SA 수의 2배 이상으로 설정 - SPD 검색 — 정책이 많으면 선형 검색이 병목. 셀렉터를 최대한 구체적으로 설정하고, 불필요한 정책 제거
- CHILD_SA rekey 폭풍 — 모든 터널이 동시에 rekey되면 CPU 스파이크.
rekey_time에rand_time을 추가하여 분산:rand_time = 600s - NAPI 배치 처리 — ESP 복호화가 비동기(crypto_aead)이므로 NAPI 폴링과 상호작용.
net.core.busy_poll으로 레이턴시 최적화 가능 - PCPU xfrm 캐시 — 커널 4.14+에서 per-CPU xfrm 정책 캐시 도입. 멀티코어 환경에서 lock contention 감소
- 모니터링 —
/proc/net/xfrm_stat의 각 카운터를 Prometheus 등으로 수집하여 이상 징후(XfrmInError 급증 등) 조기 감지
IPSec과 QoS / DSCP 처리
터널 모드에서는 원본 IP 헤더가 ESP 안으로 들어가므로, 외부 IP 헤더의 TOS/DSCP 필드를 어떻게 설정할지가 QoS 정책에 직접 영향을 줍니다. 리눅스 xfrm은 세 가지 모드를 지원합니다.
| DSCP 모드 | 동작 | 설정 방법 | 용도 |
|---|---|---|---|
| Copy (기본값) | 내부 IP의 DSCP를 외부 IP에 복사 | ip xfrm state ... flag noecn 미설정 (기본) |
ISP QoS 정책이 DSCP를 존중하는 환경. VoIP/영상 트래픽 우선 처리 |
| Set (고정값) | 외부 IP DSCP를 특정 값으로 고정 (tc/iptables로) | iptables -t mangle -A POSTROUTING -o eth0 -p esp -j DSCP --set-dscp-class CS1 |
트래픽 분석 방지. 모든 ESP 패킷이 동일한 DSCP를 갖게 하여 내부 트래픽 유형 은닉 |
| Map | 내부 DSCP를 외부 DSCP로 매핑 (1:1이 아닌 매핑) | tc filter ... action skbedit priority N와 조합 |
내부 DSCP 6개를 외부 DSCP 2개로 축소하는 등의 정책 |
# DSCP 복사 확인 — 터널 모드 송신 패킷 캡처
tcpdump -i eth0 -v esp | grep -i tos
# TOS 0xb8 (DSCP EF) → 내부 DSCP가 외부에 복사됨
# 외부 DSCP를 고정값으로 변경 (트래픽 분석 방지)
iptables -t mangle -A POSTROUTING -o eth0 -p esp \
-j DSCP --set-dscp 0
# ECN 전파 확인
sysctl net.ipv4.tunnel4.ecn # 1이면 ECN 전파 활성
# tc로 ESP 트래픽 QoS 클래스 지정
tc qdisc add dev eth0 root handle 1: htb default 30
tc class add dev eth0 parent 1: classid 1:10 htb rate 500mbit
tc filter add dev eth0 parent 1: protocol ip u32 \
match ip protocol 50 0xff flowid 1:10 # ESP=proto 50
flag noecn으로 ECN 전파를 비활성화할 수 있지만 권장하지 않습니다.
커널 빌드 옵션 (CONFIG_XFRM)
IPSec/xfrm 기능은 커널 빌드 시 여러 CONFIG 옵션으로 제어됩니다. 모듈로 빌드하면 필요할 때만 로드할 수 있지만, 고성능 환경에서는 built-in이 유리합니다.
| CONFIG 옵션 | 기본값 | 설명 | 의존성 |
|---|---|---|---|
CONFIG_XFRM |
y | xfrm 프레임워크 코어. 이것이 없으면 IPSec 전체가 비활성화됩니다 | NET |
CONFIG_XFRM_USER |
m | NETLINK_XFRM 유저스페이스 인터페이스. IKE 데몬 통신에 필수 | XFRM |
CONFIG_XFRM_INTERFACE |
m | xfrm interface (if_id 기반 route-based VPN). 커널 4.19+ | XFRM, NET_L3_MASTER_DEV |
CONFIG_XFRM_SUB_POLICY |
n | 서브 정책 지원. 일반 환경에서는 불필요하며 성능 영향 있음 | XFRM |
CONFIG_XFRM_MIGRATE |
n | MOBIKE SA 마이그레이션 지원 | XFRM |
CONFIG_XFRM_STATISTICS |
y | /proc/net/xfrm_stat 통계 카운터. 디버깅에 필수이므로 항상 활성화 권장 | XFRM, PROC_FS |
CONFIG_XFRM_ESPINTCP |
n | TCP 캡슐화(ESP-in-TCP). 매우 제한적인 방화벽 환경에서 ESP/UDP 모두 차단 시 사용 | XFRM, INET_ESPINTCP |
CONFIG_INET_ESP |
m | IPv4 ESP 프로토콜 처리 (esp4.c) | XFRM, CRYPTO_AEAD |
CONFIG_INET6_ESP |
m | IPv6 ESP 프로토콜 처리 (esp6.c) | XFRM, IPV6, CRYPTO_AEAD |
CONFIG_INET_AH |
m | IPv4 AH 프로토콜. 현대 환경에서는 거의 불필요 | XFRM, CRYPTO_HASH |
CONFIG_INET_IPCOMP |
m | IPv4 IPCOMP 압축. strongSwan 기본 제안에 포함될 수 있음 | XFRM, CRYPTO_DEFLATE |
CONFIG_NET_KEY |
m | PF_KEY 소켓 인터페이스 (레거시). Racoon 등 오래된 IKE에만 필요 | XFRM |
CONFIG_CRYPTO_GCM |
m | AES-GCM AEAD. 현대 IPSec의 사실상 필수 알고리즘 | CRYPTO_AEAD, CRYPTO_AES |
CONFIG_CRYPTO_CHACHA20POLY1305 |
m | ChaCha20-Poly1305 AEAD. AES-NI 없는 환경의 대안 | CRYPTO_AEAD |
# 현재 커널의 xfrm 관련 설정 확인
zgrep CONFIG_XFRM /proc/config.gz 2>/dev/null || \
grep CONFIG_XFRM /boot/config-$(uname -r)
# 로드된 xfrm 모듈 확인
lsmod | grep -E 'xfrm|esp[46]|ah[46]|ipcomp|af_key'
# ESP 모듈 수동 로드
modprobe esp4
modprobe esp6
# Crypto 알고리즘 가용성 확인
cat /proc/crypto | grep -A4 'gcm(aes)'
# driver: generic vs aesni → 하드웨어 가속 여부 확인
CONFIG_XFRM, CONFIG_XFRM_USER,
CONFIG_INET_ESP, CONFIG_CRYPTO_GCM,
CONFIG_XFRM_STATISTICS가 필요합니다. Route-based VPN이라면
CONFIG_XFRM_INTERFACE를, MOBIKE가 필요하면 CONFIG_XFRM_MIGRATE를
추가하세요. 임베디드/컨테이너 커널에서는 CONFIG_INET_AH와 CONFIG_NET_KEY를
빼서 공격 표면을 줄일 수 있습니다.
주요 1차 자료
- RFC 4301 — SPD/SAD, selector, BYPASS/DISCARD/PROTECT 기본 의미
- RFC 4303 — ESP packet format, transport/tunnel mode, ICV 범위
- RFC 3948 — UDP encapsulation of ESP, NAT keepalive, UDP 4500
- RFC 4106 — AES-GCM-ESP의 IV/ICV 길이와 KEYMAT
- RFC 7296 — IKEv2, IKE_SA_INIT/IKE_AUTH/CREATE_CHILD_SA, NAT detection
- RFC 8221 — ESP/AH 알고리즘 요구사항 최신 정리
- Linux kernel xfrm_device.rst — 공식 offload API와 callback 설명
- Linux kernel xfrm_proc.rst — /proc/net/xfrm_stat 카운터 의미
- Linux kernel xfrm_sync.rst — NEWAE, replay/lifetime 동기화, HA 관점
- strongSwan Route-based VPN — xfrmi, if_id, install_routes_xfrmi, routing loop 회피
- Linux kernel ipsec.rst — IPComp corner case와
level use관련 메모
관련 문서
- WireGuard — 현대적인 VPN 프로토콜
- 네트워크 보안 — Flooding 방어, Netlink, Unix Domain Socket
- Linux Crypto Framework (Crypto API) — 암호화 알고리즘 프레임워크
- Netfilter — 패킷 필터링 및 NAT