IPSec & xfrm
Linux xfrm/IPSec 스택을 심층 분석합니다. SA/SP 데이터베이스 구조와 정책 매칭, ESP/AH/IPCOMP 처리 경로, 터널/트랜스포트 모드 차이, 재키잉과 수명 관리, Crypto API 및 NIC 오프로드 연동, strongSwan/Libreswan 운영 시 발생하는 성능 저하·재전송·MTU 문제의 진단 포인트까지 실무 관점으로 정리합니다.
핵심 요약
- 패킷 수명주기 — ingress, 처리, egress 경로를 연결합니다.
- 큐/버퍼 모델 — sk_buff와 큐 지점의 역할을 분리합니다.
- 정책/데이터 분리 — 제어 평면과 데이터 평면을 구분합니다.
- 성능 지표 — PPS, 지연, 드롭 원인을 함께 분석합니다.
- 오프로딩 경계 — NIC/XDP/DPDK 경계를 명확히 유지합니다.
단계별 이해
- 경로 고정
문제가 발생한 ingress/egress 지점을 먼저 특정합니다. - 큐 관찰
백로그와 드롭 위치를 계측합니다. - 정책 반영 확인
라우팅/필터 변경이 데이터 경로에 반영됐는지 봅니다. - 부하 검증
실제 트래픽 패턴에서 재현성을 확인합니다.
xfrm 프레임워크와 IPSec 심화
xfrm(transform)은 리눅스 커널의 IPSec 구현 프레임워크입니다. 패킷의 암호화, 인증, 압축, 캡슐화를 처리하며, SA(Security Association)와 SP(Security Policy) 데이터베이스로 관리됩니다.
RX: 수신 → ESP 복호화/검증 → SA 매칭 → SP 정책 검증 → 평문 전달
xfrm 아키텍처
IPSec 프로토콜 비교
| 프로토콜 | IP 번호 | 기능 | 보호 범위 | 주의사항 |
|---|---|---|---|---|
| ESP | 50 | 암호화 + 인증 (선택적) | 페이로드 전체 (터널: 원본 IP 헤더 포함) | NAT 환경에서 UDP 캡슐화(port 4500) 필요. 가장 보편적 |
| AH | 51 | 인증만 (암호화 없음) | IP 헤더 포함 전체 패킷 (변경 가능 필드 제외) | NAT와 호환 불가 (IP 헤더가 인증 범위). 현대 환경에서 거의 미사용 |
| IPCOMP | 108 | 페이로드 압축 | ESP/AH 전에 페이로드 압축 | 압축 효과 없으면 원본 전송. ESP와 조합 사용 |
터널 모드 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 이벤트 모니터링
커널 xfrm 내부 구조
/* net/xfrm/xfrm_state.c — Security Association */
struct xfrm_state {
struct xfrm_id id; /* (daddr, spi, proto) */
xfrm_address_t saddr; /* 소스 주소 */
struct xfrm_lifetime_cfg lft; /* 수명: 바이트, 패킷, 시간 */
struct xfrm_algo_auth *aalg; /* 인증 알고리즘 (HMAC-SHA256 등) */
struct xfrm_algo *ealg; /* 암호 알고리즘 (AES-CBC 등) */
struct xfrm_algo *calg; /* 압축 알고리즘 (deflate 등) */
struct xfrm_algo_aead *aead; /* AEAD (AES-GCM 등) */
u8 mode; /* XFRM_MODE_TRANSPORT / TUNNEL */
u32 replay_maxdiff; /* Anti-replay 윈도우 */
struct xfrm_replay_state_esn *replay_esn;
/* H/W offload (SmartNIC inline crypto) */
struct xfrm_dev_offload xso;
};
/* net/ipv4/esp4.c — ESP 패킷 처리 */
/* esp_output(): 송신 패킷 암호화 (POSTROUTING 단계) */
/* esp_input(): 수신 패킷 복호화 (PREROUTING 단계) */
권장 알고리즘 조합
| 용도 | 암호화 | 인증 | 비고 |
|---|---|---|---|
| 권장 (AEAD) | AES-GCM-128/256 | (내장) | 단일 패스 암호화+인증. H/W 가속 최적. 현대 표준 |
| 레거시 호환 | AES-CBC-256 | HMAC-SHA-256 | 구형 장비 호환. CBC는 패딩 오라클 취약점 주의 |
| 고성능 (ChaCha20) | ChaCha20-Poly1305 | (내장) | AES-NI 없는 환경에서 고성능. ARM 모바일에 적합 |
| 금지 | DES, 3DES, NULL | MD5, SHA-1 | 보안 취약. 커널에서 지원하지만 사용 금지 |
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 지원
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
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 (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, 패킷마다 증가)로 구성되며,
ICV는 항상 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 지원은 필수가 아님
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의 주소를 동적으로 변경합니다.
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를 함께 사용할 때 순서 주의 필요
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가 가장 낮은(= 우선순위 높은) 정책이 선택됩니다.
/* 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의 보안 원칙을 구현합니다.
NAT Traversal (NAT-T) 상세
ESP는 IP 프로토콜 번호 50을 사용하므로, 포트 번호가 없어 일반 NAT가 처리할 수 없습니다. NAT-T(NAT Traversal, RFC 3948)는 ESP 패킷을 UDP 4500 포트로 캡슐화하여 NAT 장비를 통과할 수 있게 합니다.
/* 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(수백 개 터널)에 필수적입니다.
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 카운터가 증가하면
윈도우 확대가 필요합니다.
IPSec 하드웨어 오프로드
소프트웨어 ESP 처리는 CPU 집약적이어서 10Gbps 이상 환경에서 병목이 됩니다. 리눅스 커널은 3가지 수준의 하드웨어 오프로드를 지원합니다.
| 오프로드 모드 | 처리 위치 | 커널 관여 | 지원 하드웨어 | 성능 |
|---|---|---|---|---|
| Crypto offload | 암호화/복호화만 HW | ESP 헤더 처리, 시퀀스 번호 관리는 커널 | Intel QAT, AMD CCP | CPU 50~70% 절감 |
| Inline crypto | NIC가 ESP 암호화/복호화 | SA 설정만. 패킷 처리에서 커널 개입 최소 | NVIDIA ConnectX-6 Dx, Intel E810 | CPU 90%+ 절감, line-rate 근접 |
| Full offload | NIC/DPU가 전체 IPSec 처리 | SA/SP 설정만. 패킷 경로 완전 HW | NVIDIA BlueField DPU | CPU 해방, 100Gbps+ |
/* 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 (inline) / FULL */
u8 flags : 2; /* XFRM_DEV_OFFLOAD_FLAG_ACE 등 */
};
/* NIC 드라이버가 구현하는 xdo_dev_* 콜백 */
struct xfrmdev_ops {
int (*xdo_dev_state_add)(struct xfrm_state *x,
struct netlink_ext_ack *extack);
void (*xdo_dev_state_delete)(struct xfrm_state *x);
void (*xdo_dev_state_free)(struct xfrm_state *x);
bool (*xdo_dev_offload_ok)(struct sk_buff *skb,
struct xfrm_state *x);
/* PACKET/FULL 오프로드 시 정책 콜백도 구현 */
int (*xdo_dev_policy_add)(struct xfrm_policy *p, ...);
};
# Inline crypto offload 설정 (NVIDIA ConnectX-6 Dx 예시)
# 1. SA 생성 시 offload 지정
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. 오프로드 상태 확인
ip xfrm state list
# → "offload type packet dev eth0 dir out" 표시
# 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 (QAT): 기존 서버에 PCIe 카드 추가만으로 성능 개선. SA 수 제한 없음. 암호화 외 처리는 여전히 CPU
- Inline crypto (ConnectX-6/E810): NIC 교체 필요하지만 line-rate 근접. SA 수는 NIC 메모리에 제한 (수천~수만). AES-GCM만 지원하는 경우 많음
- Full offload (BlueField DPU): 가장 높은 성능이지만 비용이 높음. 클라우드/가상화 환경에서 OVS + IPSec을 DPU에 완전 오프로드
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 급증 등) 조기 감지
관련 문서
- WireGuard — 현대적인 VPN 프로토콜
- 네트워크 보안 — Flooding 방어, Netlink, Unix Domain Socket
- Linux Crypto Framework (Crypto API) — 암호화 알고리즘 프레임워크
- Netfilter — 패킷 필터링 및 NAT