IPSec & xfrm
Linux xfrm/IPSec 스택을 심층 분석합니다. SPD/SAD와 selector·reqid·if_id·mark의 결합 방식, ESP/AH/IPCOMP 처리 경로, 터널(Tunnel)/트랜스포트/BEET 모드, NAT-T와 route-based VPN, ACQUIRE/EXPIRE/NEWAE 이벤트, Crypto API 및 NIC 오프로드, strongSwan/Libreswan 운영 시 발생하는 MTU·재전송(Retransmission)·정책 불일치 문제의 진단 포인트까지 실무 관점으로 정리합니다.
핵심 요약
- 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, 정책 라우팅(Routing)과 함께 봐야 합니다.
단계별 이해
- 정책 선택
패킷(Packet)이 SPD에서allow/block/protect중 무엇으로 판정되는지부터 확인합니다. - 상태 확보
보호가 필요하지만 SA가 없으면 커널이XFRM_MSG_ACQUIRE를 올리고, IKE 데몬이 협상 후 SA/SP를 주입합니다. - 변환 적용
송신은xfrm_lookup()과 bundle 생성 후 ESP/AH/IPCOMP를 적용하고, 수신은 SPI 기반 SA 조회 후 anti-replay와 ICV 검증을 수행합니다. - 정책 재검증
복호화(Decryption)된 수신 패킷은 다시 수신 정책과 대조되어, "복호화는 성공했지만 정책이 틀린" 패킷을 차단합니다. - 관측/튜닝
ip xfrm,/proc/net/xfrm_stat,ip xfrm monitor, NIC 오프로드 통계로 병목(Bottleneck)과 불일치를 좁혀갑니다.
xfrm 프레임워크와 IPSec
xfrm(transform)은 리눅스 커널의 IPSec 구현 프레임워크입니다. 패킷의 암호화(Encryption), 인증, 압축, 캡슐화(Encapsulation)를 처리하며, 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[] | 정책 우선순위(Priority) 충돌과 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 만료 시 캐시(Cache)가 무효화(Invalidation)됩니다. |
특히 reqid는 사람이 보기엔 사소한 숫자처럼 보이지만, IKE 데몬이
양방향 SA 쌍과 정책 템플릿을 안정적으로 묶는 데 핵심입니다. 반대로 mark,
output-mark, if_id는 라우팅 도메인과 터널 인터페이스를 분리할 때
중요하며, 강한 정책 분리 없이 default route 하나로 xfrmi를 붙이면 IKE/ESP 루프를 만들기 쉽습니다.
IPSec 프로토콜 비교
| 프로토콜 | IP 번호 | 기능 | 보호 범위 | 주의사항 |
|---|---|---|---|---|
| ESP | 50 | 기밀성 + 무결성(Integrity) + anti-replay | 페이로드(Payload) 전체 (터널: 원본 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 헤더를 유지하므로 디버깅(Debugging)은 쉽지만 주소 은닉 효과는 없습니다.
- 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 매핑(Mapping)을 갖습니다. 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(): 수신 패킷 복호화 */
위 코드는 핵심 필드만 발췌한 축약본입니다. 실제 커널 구조체(Struct)에는 lock/refcount, state cache, 보안 컨텍스트, offload 통계, garbage-collection 연결 정보까지 들어갑니다. 중요한 점은 state에도 selector, mark, if_id, reqid가 남아 있습니다는 것이고, 그래서 "정책만 맞고 상태는 다른 터널에 붙는" 오동작을 막을 수 있습니다.
SAD 해시 테이블(Hash Table)과 xfrm_state_lookup()
수신 경로에서 ESP 패킷이 도착하면, 커널은 (daddr, SPI, proto) 3-tuple을 키로
SAD 해시 테이블에서 O(1) 평균 시간에 SA를 검색합니다.
이 검색은 RCU(Read-Copy-Update) 보호 하에 수행되므로, 읽기 경로에서는 lock-free로 동작하며
대규모 SA 환경(수천 개 터널)에서도 성능이 저하되지 않습니다.
/* net/xfrm/xfrm_state.c — SAD 해시 테이블 검색 상세 */
/* 해시 함수: (daddr, spi, proto) → 버킷 인덱스 */
static inline unsigned int
__xfrm_state_hash(const xfrm_address_t *daddr,
__be32 spi, u8 proto,
unsigned int family)
{
/* jhash_3words: Jenkins hash로 3개 입력을 결합
* → 균일 분포 해시값 생성, 해시 충돌 최소화
* → 테이블 크기(hmask+1)로 모듈러 연산 */
return __xfrm_hash(daddr, spi, proto, family);
}
/* 수신 경로 핵심: SPI 기반 SA 검색 */
struct xfrm_state *
xfrm_state_lookup(struct net *net,
u32 mark,
const xfrm_address_t *daddr,
__be32 spi, u8 proto,
unsigned short family)
{
struct xfrm_state *x;
struct xfrm_state_hash *h;
unsigned int hash;
/* 1. 해시 버킷 인덱스 계산 */
hash = __xfrm_state_hash(daddr, spi, proto, family);
/* 2. RCU 보호 하에 체인 순회 (lock-free) */
rcu_read_lock();
h = rcu_dereference(net->xfrm.state_bydst);
hlist_for_each_entry_rcu(x, &h[hash].chain, byspi) {
/* 3. (daddr, spi, proto, family) 정확 일치 검사 */
if (x->id.spi == spi &&
x->id.daddr matches daddr &&
x->id.proto == proto &&
x->props.family == family) {
/* 4. mark/if_id 추가 검사 (있으면) */
/* 5. refcount 증가 후 반환 */
xfrm_state_hold(x);
rcu_read_unlock();
return x;
}
}
rcu_read_unlock();
return NULL; /* XfrmInNoStates 카운터 증가 */
}
/* 송신 경로: 정책 기반 SA 검색 (tmpl 매칭) */
struct xfrm_state *
xfrm_state_find(const xfrm_address_t *daddr,
const xfrm_address_t *saddr,
const struct flowi *fl,
struct xfrm_tmpl *tmpl,
struct xfrm_policy *pol)
{
/* 1. (daddr, saddr, reqid, proto, mode) 기반 해시 검색 */
/* 2. tmpl의 reqid, proto, mode와 SA 비교 */
/* 3. 유효한 SA가 없으면:
* - larval state 생성 (빈 껍데기 SA)
* - XFRM_MSG_ACQUIRE를 IKE 데몬에 전송
* - 패킷은 larval state에 큐잉 또는 드롭 (xfrm_larval_drop) */
}
xfrm4_gc_thresh를 기반으로 결정되며,
SA가 증가하면 버킷 수를 늘려 체인 길이를 짧게 유지합니다.
대규모 환경(수천 SA)에서도 평균 O(1) 검색이 보장되지만,
동일 버킷에 SA가 몰리면(해시(Hash) 충돌) 체인 순회 비용이 증가합니다.
sysctl net.ipv4.xfrm4_gc_thresh를 SA 수의 2배 이상으로 설정하면
충돌을 줄일 수 있습니다.
xfrm_state 상태 전이
SA(xfrm_state)는 생성부터 소멸까지 명확한 상태 전이를 거칩니다. 각 전이에서 커널은 XFRM Netlink 이벤트를 발생시키며, IKE 데몬은 이 이벤트를 수신하여 SA 협상/갱신/삭제를 수행합니다.
/* include/net/xfrm.h — SA 상태 정의 */
XFRM_STATE_VOID = 0, /* 초기화 전 */
XFRM_STATE_ACQ = 1, /* ACQUIRE 전송됨, IKE 응답 대기 */
XFRM_STATE_VALID = 2, /* 키 설치 완료, 패킷 처리 가능 */
XFRM_STATE_ERROR = 3, /* 오류 상태 */
XFRM_STATE_EXPIRED = 4, /* hard expire → GC 대기 */
XFRM_STATE_DEAD = 5, /* 삭제 예정, 참조 카운터 0 대기 */
/* net/xfrm/xfrm_state.c — soft/hard expire 처리 */
static void xfrm_timer_handler(struct timer_list *t)
{
struct xfrm_state *x = from_timer(x, t, timer);
/* lifetime 검사: bytes, packets, add_time, use_time */
if (hard_expired) {
/* hard expire: SA 즉시 무효화 */
x->km.state = XFRM_STATE_EXPIRED;
km_state_expired(x, 1, 0); /* hard=1 → XFRM_MSG_EXPIRE */
/* → IKE 데몬이 SA 삭제 처리 */
} else if (soft_expired) {
/* soft expire: rekey 시그널, 트래픽은 계속 처리 */
km_state_expired(x, 0, 0); /* hard=0 → XFRM_MSG_EXPIRE */
/* → IKE 데몬이 CREATE_CHILD_SA rekey 시작 */
}
}
IPSec/xfrm에 새 암호 알고리즘을 추가할 때 수정하는 위치
결론부터 말하면, "xfrm에 새 암호 알고리즘을 추가한다"는 작업은 보통 한 파일만 고치면 끝나지 않습니다.
실제 흐름은 Crypto API 구현 제공 → xfrm 알고리즘 설명 테이블 등록 → Netlink/PF_KEY로 SA에 붙이기 → ESP/AH 초기화 경로에서 transform 생성 순서로 이어집니다.
커널 최신 소스 기준으로는 net/xfrm/xfrm_algo.c, net/xfrm/xfrm_user.c, net/ipv4/esp4.c, net/ipv6/esp6.c, include/net/xfrm.h, include/uapi/linux/xfrm.h가 핵심 접점이며, 특히 사용자 공간(User Space)에서 SA를 주입하는 경로는 xfrm_user.c를 통과합니다.
esp4.c/esp6.c의 데이터 경로를 거의 건드리지 않습니다.
반대로 IV 길이, nonce 구성, trailer 형식, header 배치, AAD 구성이 기존 ESP 일반식과 다르면 esp4.c/esp6.c까지 수정 범위가 커집니다.
| 수정 지점 | 역할 | 언제 수정하는가 |
|---|---|---|
crypto/ 또는 드라이버 구현 파일 |
실제 암호 알고리즘 구현과 crypto_register_* 등록 |
알고리즘 자체가 커널 Crypto API에 아직 없을 때 |
net/xfrm/xfrm_algo.c |
IPsec이 이해하는 알고리즘 이름, SADB ID, IV 길이, truncation 길이, key bit 범위 정의 | xfrm에서 새 알고리즘 이름을 받아 SA에 연결하려면 거의 항상 필요 |
net/xfrm/xfrm_user.c |
Netlink 속성 검증, attach_aead() / attach_crypt() / attach_auth() 경로로 SA에 알고리즘 부착 |
새 UAPI를 추가하지 않는 한 로직 수정은 드물지만, 연결 지점을 이해하려면 반드시 봐야 함 |
net/ipv4/esp4.c, net/ipv6/esp6.c |
esp*_init_state()에서 transform 할당, 키 설정, authsize 설정, 송수신 암복호화 수행 |
ESP 포맷 처리 가정이 바뀌거나 별도 초기화 규칙이 필요할 때 |
include/uapi/linux/pfkeyv2.h |
SADB 알고리즘 ID 상수 제공 | 기존 SADB ID로 표현할 수 없는 새 표준 알고리즘을 추가할 때 |
include/uapi/linux/xfrm.h |
struct xfrm_algo, struct xfrm_algo_auth, struct xfrm_algo_aead 같은 UAPI 구조 정의 |
대부분은 수정하지 않지만, 새 속성 타입 자체를 도입하면 변경 필요 |
수정 흐름을 소스 코드 경로로 따라가면
/* 1. 사용자 공간(User Space): ip xfrm state add ... aead 'new-alg(...)' ... */
/* 2. net/xfrm/xfrm_user.c */
verify_newsa_info()
-> verify_aead() / verify_one_alg()
-> attach_aead() / attach_crypt() / attach_auth()
-> xfrm_aead_get_byname() / xfrm_ealg_get_byname() / xfrm_aalg_get_byname()
/* 3. net/xfrm/xfrm_algo.c */
/* aead_list / ealg_list / aalg_list / calg_list에서 이름과 속성 조회 */
/* 4. net/ipv4/esp4.c, net/ipv6/esp6.c */
esp_init_state() / esp6_init_state()
-> crypto_alloc_aead() 또는 authenc 기반 transform 생성
-> crypto_aead_setkey()
-> crypto_aead_setauthsize()
-> x->data 에 tfm 저장
/* 5. 실제 데이터 경로 */
esp_output() / esp6_output()
-> crypto_aead_encrypt()
esp_input() / esp6_input()
-> crypto_aead_decrypt()
즉, xfrm은 알고리즘 구현 자체를 직접 내장하지 않고, 먼저 이름과 제약을
xfrm_algo.c에서 찾은 뒤, ESP 초기화 코드가 그 이름으로 Crypto API transform을 할당하는 구조입니다.
따라서 Crypto API에 구현이 있어도 xfrm 매핑이 없으면 ip xfrm state add 단계에서 연결되지 않고,
반대로 xfrm_algo.c에 이름만 있어도 실제 Crypto API 구현이 없으면 esp_init_state()에서 transform 생성이 실패합니다.
실무 체크리스트
- Crypto API 구현이 이미 있는지 먼저 확인
/proc/crypto에 원하는 이름이 나타나는지 확인합니다. 없다면crypto/또는 하드웨어 드라이버에서crypto_register_aead(),crypto_register_skcipher(),crypto_register_ahash()같은 등록 단계부터 추가해야 합니다. - xfrm에서 받아들일 이름과 속성을 정의
net/xfrm/xfrm_algo.c의aead_list,ealg_list,aalg_list,calg_list중 해당 형식에 맞는 배열에 엔트리를 추가합니다. 여기서.name,.compat,.desc.sadb_alg_id,.desc.sadb_alg_ivlen,.desc.sadb_alg_minbits,.desc.sadb_alg_maxbits, AEAD라면.uinfo.aead.geniv와.uinfo.aead.icv_truncbits를 정합니다. - 새 SADB ID가 필요하면 UAPI 상수도 함께 추가
기존 PF_KEY 상수로 표현할 수 없는 알고리즘이면include/uapi/linux/pfkeyv2.h에 새SADB_X_EALG_*또는SADB_X_AALG_*계열 값을 정의해야 합니다. 이 단계는 사용자 공간 호환성과 직접 연결되므로 임의 숫자 재사용은 금지해야 합니다. - ESP 초기화 경로가 그 알고리즘을 실제로 만들 수 있는지 확인
net/ipv4/esp4.c와net/ipv6/esp6.c의esp_init_state(),esp6_init_state()는 AEAD이면crypto_alloc_aead()로, 전통적인 암호+인증 조합이면 authenc 이름을 조합하여 transform을 생성합니다. 기존 생성식에 맞지 않는 이름이나 key layout이면 이 함수에서 별도 분기 처리가 필요합니다. - 패킷 형식이 기존 ESP 규칙과 다르면 데이터 경로까지 수정
예를 들어 nonce 배치, explicit IV 길이, trailer 정렬, AAD 계산 규칙이 기존esp_output_head(),esp_output_tail(),esp_input()의 가정과 다르면esp4.c/esp6.c를 수정해야 합니다. 이 경우는 단순 알고리즘 등록이 아니라 사실상 "새 ESP 처리 변형"에 가깝습니다. - 사용자 공간 이름 매핑도 점검
커널만 고쳐도 strongSwan, Libreswan, iproute2가 그 이름을 내보내지 않으면 운영에서 바로 쓰기 어렵습니다. 특히 IKE Transform ID와 커널의 Crypto API 이름이 다를 수 있으므로, 사용자 공간 매핑 패치(Patch)가 별도로 필요할 수 있습니다.
AEAD 알고리즘 추가 예시로 보면
가장 흔한 경우는 새 AEAD 알고리즘이 이미 Crypto API에 존재하고, xfrm/IPSec이 그 이름을 새로 인식해야 하는 경우입니다.
이때 출발점은 보통 net/xfrm/xfrm_algo.c의 aead_list입니다.
최신 커널에서도 rfc4106(gcm(aes)), rfc4309(ccm(aes)), rfc7539esp(chacha20,poly1305) 같은 이름이 이 테이블에 들어 있습니다.
/* net/xfrm/xfrm_algo.c — 예시적인 AEAD 엔트리 형태 */
{
.name = "rfc9999(newaead(example))",
.uinfo = {
.aead = {
.geniv = "seqiv",
.icv_truncbits = 128,
}
},
.pfkey_supported = 1,
.desc = {
.sadb_alg_id = SADB_X_EALG_NEWAEAD_ICV16,
.sadb_alg_ivlen = 8,
.sadb_alg_minbits = 128,
.sadb_alg_maxbits = 256,
}
},
이 엔트리가 의미하는 것은 단순 문자열 등록이 아닙니다. IPSec 정책/상태 계층이 이 알고리즘을 어떤 SADB ID로 광고할지, IV 길이를 얼마로 볼지, 허용 key 길이 범위를 어떻게 검증할지, AEAD 인증 태그(Tag) 절단 길이를 몇 비트로 다룰지를 동시에 정의하는 것입니다.
/* net/xfrm/xfrm_user.c — SA 추가 시 대략적인 연결 흐름 */
static int attach_aead(struct xfrm_state *x, struct nlattr *rta, ...)
{
/* 사용자가 넘긴 alg_name을 기준으로 xfrm_algo.c 조회 */
algo = xfrm_aead_get_byname(ualg->alg_name, ualg->alg_icv_len, 1);
/* 성공하면 xfrm_state에 AEAD 정보 저장 */
x->props.ealgo = algo->desc.sadb_alg_id;
x->aead = p;
x->geniv = algo->uinfo.aead.geniv;
}
그 다음 단계에서 net/ipv4/esp4.c와 net/ipv6/esp6.c의
esp_init_aead()가 x->geniv와 x->aead->alg_name을 조합해
실제 Crypto API transform 이름을 만듭니다.
최신 esp4.c 기준으로는 대략 "seqiv(rfc4106(gcm(aes)))" 같은 문자열을 만들어
crypto_alloc_aead()에 넘기는 구조입니다.
/* net/ipv4/esp4.c / net/ipv6/esp6.c — 실제 초기화 지점 */
static int esp_init_aead(struct xfrm_state *x, ...)
{
/* "seqiv(alg_name)" 형식으로 transform 이름 생성 */
snprintf(aead_name, ..., "%s(%s)", x->geniv, x->aead->alg_name);
aead = crypto_alloc_aead(aead_name, 0, 0);
crypto_aead_setkey(aead, x->aead->alg_key, ...);
crypto_aead_setauthsize(aead, x->aead->alg_icv_len / 8);
x->data = aead;
}
xfrm_algo.c에 넣은 이름과
Crypto API가 실제로 export하는 이름이 맞아떨어져야 합니다.
이 둘이 조금이라도 다르면 attach_aead()는 통과했는데 crypto_alloc_aead()에서 실패하는 식의 반쪽 성공 상태가 생깁니다.
패치 초안 형태로 보면
실제 작업을 시작할 때는 아래처럼 "새 SADB 상수 추가 → xfrm_algo.c 엔트리 추가 → 사용자 공간에서 SA 주입 시험" 순서로 최소 패치를 잡는 것이 가장 안전합니다.
아래 코드는 가상의 예시이며, 실제 표준 번호나 SADB 상수 값으로 바로 사용하면 안 됩니다.
diff --git a/include/uapi/linux/pfkeyv2.h b/include/uapi/linux/pfkeyv2.h
@@
+#define SADB_X_EALG_NEWAEAD_ICV16 25
diff --git a/net/xfrm/xfrm_algo.c b/net/xfrm/xfrm_algo.c
@@
static struct xfrm_algo_desc aead_list[] = {
+{
+ .name = "rfc9999(newaead(example))",
+ .uinfo = {
+ .aead = {
+ .geniv = "seqiv",
+ .icv_truncbits = 128,
+ },
+ },
+ .pfkey_supported = 1,
+ .desc = {
+ .sadb_alg_id = SADB_X_EALG_NEWAEAD_ICV16,
+ .sadb_alg_ivlen = 8,
+ .sadb_alg_minbits = 128,
+ .sadb_alg_maxbits = 256,
+ },
+},
이 최소 패치가 의미하는 범위는 명확합니다.
xfrm_user.c의 attach_aead()는 기존 로직 그대로
xfrm_aead_get_byname()를 통해 이 엔트리를 찾고, 성공하면 x->props.ealgo와
x->geniv를 채웁니다.
이후 esp_init_aead()가 seqiv(rfc9999(newaead(example))) 형식의 transform을 만들 수 있으면, 데이터 경로 수정 없이도 SA 생성까지는 도달할 수 있습니다.
# 1. Crypto API 이름 확인
grep -A4 -F "rfc9999(newaead(example))" /proc/crypto
# 2. xfrm state 주입 시험
ip xfrm state add \
src 192.0.2.1 dst 192.0.2.2 \
proto esp spi 0x100 mode transport \
aead 'rfc9999(newaead(example))' 0x00112233445566778899aabbccddeeff00112233 128
# 3. 결과 확인
ip xfrm state list
dmesg | tail
Requested AEAD algorithm not found가 나오면 보통 xfrm_algo.c 엔트리 누락 또는
ICV 길이 불일치입니다.
반대로 SA 추가는 통과했지만 이후 Kernel was unable to initialize cryptographic operations가 나오면
대개 Crypto API transform 이름 또는 key/authsize 초기화가 맞지 않는 경우입니다.
authenc 조합을 추가할 때는 다르게 본다
AES-CBC + HMAC-SHA2처럼 AEAD가 아니라 암호화(Encryption) 알고리즘과 인증(Authentication) 알고리즘을 따로 주입하는 조합은
흐름이 약간 다릅니다.
이 경우에는 xfrm_user.c가 attach_crypt()로 x->ealg를 채우고,
attach_auth() 또는 attach_auth_trunc()로 x->aalg를 채운 뒤,
esp_init_authenc()가 최종적으로 authenc(...) 또는 authencesn(...) 이름의 AEAD 템플릿을 생성합니다.
/* net/xfrm/xfrm_user.c — CRYPT / AUTH 경로는 따로 붙습니다 */
attach_crypt(...)
-> algo = xfrm_ealg_get_byname(ualg->alg_name, 1);
-> x->ealg = p;
-> x->geniv = algo->uinfo.encr.geniv;
attach_auth(...)
-> algo = xfrm_aalg_get_byname(ualg->alg_name, 1);
-> x->aalg = p;
/* 이후 esp_init_authenc()가 authenc(auth, crypt) 조합 생성 */
즉, authenc 경로에서 새 조합 이름을 xfrm에 직접 등록하는 것이 아니라,
보통은 개별 ealg_list와 aalg_list 엔트리를 각각 준비한 뒤
ESP 초기화 코드가 이 둘을 묶어 Crypto API 템플릿 이름을 만들게 합니다.
/* net/xfrm/xfrm_algo.c — 예시적인 개별 엔트리 추가 형태 */
/* ealg_list 쪽 */
{
.name = "cbc(examplecipher)",
.compat = "examplecipher",
.uinfo = {
.encr = {
.geniv = "echainiv",
.blockbits = 128,
.defkeybits = 256,
},
},
.pfkey_supported = 1,
.desc = {
.sadb_alg_id = SADB_X_EALG_EXAMPLECBC,
.sadb_alg_ivlen = 16,
.sadb_alg_minbits = 128,
.sadb_alg_maxbits = 256,
},
},
/* aalg_list 쪽 */
{
.name = "hmac(examplesha)",
.compat = "examplesha",
.uinfo = {
.auth = {
.icv_truncbits = 128,
.icv_fullbits = 256,
},
},
.pfkey_supported = 1,
.desc = {
.sadb_alg_id = SADB_X_AALG_EXAMPLESHA_HMAC,
.sadb_alg_ivlen = 0,
.sadb_alg_minbits = 256,
.sadb_alg_maxbits = 256,
},
},
그 다음 net/ipv4/esp4.c의 esp_init_authenc()는
ESN 사용 여부까지 반영해 대략 아래와 같은 이름을 만듭니다.
/* ESN이 없으면 */
"echainiv(authenc(hmac(examplesha),cbc(examplecipher)))"
/* ESN이 있으면 */
"echainiv(authencesn(hmac(examplesha),cbc(examplecipher)))"
attach_auth(), attach_crypt(), esp_init_authenc() 세 군데로 늘어납니다.
# authenc 조합 시험 예시
ip xfrm state add \
src 192.0.2.1 dst 192.0.2.2 \
proto esp spi 0x200 mode transport \
enc 'cbc(examplecipher)' 0x00112233445566778899aabbccddeeff \
auth-trunc 'hmac(examplesha)' 0x00112233445566778899aabbccddeeff00112233445566778899aabbccddeeff 128
Requested CRYPT algorithm not found는 보통 ealg_list 누락,
Requested AUTH algorithm not found 또는 Requested AUTH_TRUNC algorithm not found는
보통 aalg_list 누락입니다.
SA 생성 후 Kernel was unable to initialize cryptographic operations가 나오면,
실제로는 authenc(...) 템플릿 이름이나 authsize/key layout 조합이 Crypto API가 기대하는 형식과 맞지 않는 경우가 많습니다.
언제 esp4.c / esp6.c까지 직접 고쳐야 하는가
아래 조건 중 하나라도 걸리면, 단순 테이블 추가로 끝나지 않고 ESP 코드까지 직접 봐야 합니다.
- explicit IV 길이가 기존 알고리즘들과 다를 때 —
x->props.header_len계산과crypto_aead_ivsize()의존 경로를 점검해야 합니다. - ICV 위치나 길이 계산 규칙이 다를 때 —
x->props.trailer_len와crypto_aead_authsize()기반 계산이 그대로 맞는지 확인해야 합니다. - AAD 구성이 ESP 일반식과 다를 때 — 시퀀스 번호, ESN(Extended Sequence Number), SPI를 어떤 형식으로 넘기는지 송수신 양쪽을 검토해야 합니다.
- nonce 조합 규칙이
seqiv로 표현되지 않을 때 —.uinfo.aead.geniv만으로는 부족할 수 있으므로 별도 초기화 또는 템플릿 지원이 필요합니다. - 압축, 인증, 암호화를 한 번에 묶는 새 포맷을 도입할 때 — 이 경우는 사실상 새 "알고리즘"이 아니라 새 "ESP 처리 방식"에 가깝습니다.
esp_init_aead()가 기존 방식대로 transform을 만들 수 있고,
esp_output() / esp_input()이 기존 ESP 패킷 형식 계산을 그대로 써도 되면,
보통은 xfrm_algo.c와 Crypto API 구현만으로 충분합니다.
이 전제가 깨지면 esp4.c/esp6.c 수정이 필요합니다.
net/ipv4/esp4.c나 net/ipv6/esp6.c에 새 알고리즘 이름만 하드코딩하면 되는 구조가 아닙니다.
실제로는 xfrm은 xfrm_algo.c의 설명 테이블을 통해 정책/상태 레벨에서 알고리즘을 인지하고,
ESP 코드는 그 결과물(x->aead, x->ealg, x->aalg)을 바탕으로 Crypto API transform을 생성합니다.
Crypto API 구현과 xfrm_algo.c입니다.
반면 ESP 포맷 자체를 확장하는 알고리즘/모드라면 xfrm_type 초기화와 송수신 데이터 경로까지 검토해야 하므로 난도가 훨씬 높습니다.
권장 알고리즘 조합
| 배포 기준 | 암호화 | 인증 | 비고 |
|---|---|---|---|
| 신규 기본값 | 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/가상화(Virtualization) 환경에서 유리할 수 있음 |
| 레거시만 허용 | 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와 단편화(Fragmentation) 상세
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 실시간(Real-time) 이벤트 | 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 지연(Latency). 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 소켓(Socket) 오류 |
# 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 불일치, 방화벽(Firewall) 차단 |
ip -s xfrm state (카운터 0?)ip route get 192.168.2.1tcpdump -i eth0 esp
|
트래픽이 xfrm policy에 매칭되는지 확인. 라우팅 테이블(Routing Table)에 터널 경로 추가. 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(RFC 4106)은 SA 내부의 salt와 패킷마다 달라지는 explicit nonce를 합쳐 nonce 재료를 구성하므로,
패킷 구조를 볼 때는 "wire에는 explicit nonce가 실리고 salt는 실리지 않는다"라고 이해해야 정확합니다.
RFC 4106 기준 ICV는 8/12/16바이트가 가능하지만 일반적인 배포와 하드웨어 구현은 16바이트(128비트)를 기본값으로 씁니다.
ESP AEAD의 AAD / scatterlist 레이아웃
ESP AEAD 경로를 실제 커널 코드 수준에서 이해하려면 "무엇이 암호화 대상이고, 무엇이 인증 전용 데이터인지"를 메모리 레이아웃으로 보는 것이 가장 빠릅니다. ESP에서는 SPI + Seq(+ ESN 상위 비트)가 AAD이고, explicit nonce와 payload, trailer는 AEAD 입력 데이터 쪽에 붙습니다.
/* AES-GCM ESP 개념적 레이아웃 */
/* AAD: [ SPI(4) | Seq(4) | ESN_hi(옵션 4) ] */
/* crypt src: [ explicit_nonce | plaintext_or_ciphertext | pad | padlen | nexthdr ] */
/* crypt dst: [ explicit_nonce | ciphertext_or_plaintext | pad | padlen | nexthdr | ICV ] */
/* encrypt */
aead_request_set_ad(req, aad_len);
aead_request_set_crypt(req, src, dst, cryptlen, iv);
/* cryptlen = explicit_nonce + plaintext + trailer */
/* decrypt */
aead_request_set_ad(req, aad_len);
aead_request_set_crypt(req, src, dst, cryptlen, iv);
/* cryptlen = explicit_nonce + ciphertext + trailer + ICV */
| 구성 요소 | 암호화(Encryption) 시 위치 | 복호화(Decryption) 시 위치 | 의미 |
|---|---|---|---|
| AAD | 별도 길이(assoclen)로 지정 |
별도 길이(assoclen)로 지정 |
암호화는 되지 않지만 태그 계산에는 반드시 포함됩니다. ESP에서는 SPI, seq, ESN 상위 비트가 핵심입니다. |
| explicit nonce | AEAD 입력 데이터 앞부분 | AEAD 입력 데이터 앞부분 | 패킷에 실리는 per-packet 값입니다. SA 내부 salt와 합쳐 nonce 재료가 됩니다. |
| payload + trailer | 평문으로 입력 | 암호문으로 입력 | pad, padlen, next header까지 ESP 보호 범위에 포함됩니다. |
| ICV / tag | 출력 버퍼(Buffer) 끝에 추가 | 입력 버퍼 끝에 이미 포함 | 복호화 시에는 cryptlen 계산에 tag 길이까지 포함된다는 점이 흔한 실수 포인트입니다. |
cryptlen을 "암호문 길이만"으로 넣으면 안 됩니다.
AEAD 복호화는 입력 끝에 붙은 ICV까지 포함한 길이를 넘겨야 하므로, 태그 길이를 빼먹으면
XfrmInStateProtoError류의 인증 실패처럼 보이기 쉽습니다.
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 암호화 전에 페이로드를 압축하여 대역폭(Bandwidth)을 절약하는 프로토콜입니다. 암호화된 데이터는 높은 엔트로피로 인해 압축이 불가능하므로, 반드시 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로 흘려보냅니다.
ip xfrm state / policy 출력 해설
문서 전체에서 설명한 SPI, reqid, mode, replay-window, if_id, mark 같은 개념은 실제로는
ip xfrm state, ip xfrm policy 출력으로 확인하게 됩니다.
아래 예제는 "문서의 개념"과 "운영 중 실제로 보는 필드"를 직접 연결하기 위한 최소 해설입니다.
# SA 출력 예시
$ ip -s xfrm state
src 203.0.113.1 dst 198.51.100.1
proto esp spi 0x00001001 reqid 42 mode tunnel
replay-window 1024 flag esn
aead rfc4106(gcm(aes)) 0x... 128
encap type espinudp sport 4500 dport 4500 addr 0.0.0.0
sel src 192.168.10.0/24 dst 192.168.20.0/24
anti-replay context: seq 0x00012abc, oseq 0x00013456, bitmap 0xffffffff ...
lifetime config:
limit: soft(add_expires=3300s bytes=0 packets=0) hard(add_expires=3600s bytes=0 packets=0)
lifetime current:
91324872 bytes 118233 packets add 2026-04-22 18:00:00 use 2026-04-22 18:59:20
# policy 출력 예시
$ ip xfrm policy
src 192.168.10.0/24 dst 192.168.20.0/24
dir out priority 1000 ptype main
tmpl src 203.0.113.1 dst 198.51.100.1
proto esp reqid 42 mode tunnel level required
| 출력 필드 | 어디서 쓰이는가 | 실전 해석 |
|---|---|---|
proto esp spi 0x... |
SAD 조회 키 | 수신에서 XfrmInNoStates가 늘면 가장 먼저 SPI와 목적지 주소 쌍이 양쪽에서 맞는지 봅니다. |
reqid 42 |
policy tmpl ↔ state 연결 | state는 있는데 XfrmInTmplMismatch가 늘면 policy tmpl의 reqid와 state reqid가 엇갈렸을 가능성이 큽니다. |
mode tunnel |
ESP 헤더 해석, inner/outer IP 배치 | 한쪽은 tunnel, 다른 쪽은 transport면 복호화 뒤 XfrmInStateModeError나 정책 불일치로 이어질 수 있습니다. |
replay-window 1024 flag esn |
anti-replay 검사 | 재정렬이 심한 환경에서 XfrmInSeqOutOfWindow가 늘면 윈도우 크기와 ESN on/off 대칭성을 함께 봐야 합니다. |
aead rfc4106(gcm(aes)) ... 128 |
esp_input() / esp_output() 알고리즘 선택 |
XfrmInStateProtoError가 뜨면 알고리즘 이름, ICV 길이, 키 길이, 재키 직후 old/new SA 혼재를 우선 의심합니다. |
encap type espinudp ... |
NAT-T UDP 래핑 | NAT-T 환경에서 SA는 맞는데 UDP 4500만 보이면 encap 필드가 양쪽에 동일한지 확인합니다. |
sel src ... dst ... |
state selector 힌트 | 너무 넓거나 엇갈린 selector는 XfrmInStateMismatch와 정책 혼선을 만들 수 있습니다. |
lifetime config/current |
soft/hard expire, rekey 타이밍 | rekey 직전 장애는 current 증가 속도와 soft/hard 간격이 충분한지부터 봐야 합니다. |
tmpl ... proto esp reqid 42 mode tunnel level required |
수신/송신 정책 강제 조건 | policy는 패킷이 어떤 SA 계열로 보호되어야 하는지 정의합니다. state만 맞고 tmpl이 틀리면 복호화 후에도 드롭될 수 있습니다. |
CHILD_SA rekey 겹침 구간 해석
실무에서 가장 헷갈리는 순간은 새 SA는 이미 설치되었는데, 잠시 동안 old/new SA가 동시에 살아 있는 구간입니다. IKEv2의 make-before-break rekey는 이 겹침 구간 덕분에 무중단에 가깝게 동작하지만, 관측 지점을 잘못 해석하면 "중복 SA", "잘못된 SPI", "키가 두 벌이라 혼란"처럼 보이기 쉽습니다.
# 정상적인 rekey 중에는 같은 peer에 대해 SA가 2벌 보일 수 있습니다.
$ ip -s xfrm state | sed -n '/203.0.113.1 dst 198.51.100.1/,+18p'
src 203.0.113.1 dst 198.51.100.1
proto esp spi 0x00001001 reqid 42 mode tunnel
replay-window 1024 flag esn
lifetime current: 128731233 bytes 166223 packets ...
src 203.0.113.1 dst 198.51.100.1
proto esp spi 0x00001077 reqid 42 mode tunnel
replay-window 1024 flag esn
lifetime current: 183344 bytes 241 packets ...
# 이벤트는 보통 이런 순서로 관측됩니다.
$ ip xfrm monitor all
XFRM_MSG_EXPIRE hard=0 ...
XFRM_MSG_NEWSA spi=0x00001077 ...
XFRM_MSG_NEWPOLICY ...
# 이후 일정 시간 뒤 old SA DELSA
| 관측 현상 | 정상/이상 | 해석 |
|---|---|---|
| 같은 peer에 대해 SPI가 2개 보임 | 정상일 수 있음 | rekey 겹침 구간에서는 old/new CHILD_SA가 동시에 살아 있을 수 있습니다. reqid와 mode가 같고 새 SA 카운터가 막 증가하기 시작하면 대개 정상입니다. |
| old SA만 계속 증가하고 new SA는 0 유지 | 주의 | 커널 cutover가 아직 안 되었거나 피어가 아직 새 SPI로 송신하지 않는 상태입니다. 짧으면 정상, 길면 rekey 협상/정책 설치 순서를 봐야 합니다. |
| new SA만 증가하는데 old SA가 오래 안 지워짐 | 보통 경미 | 제거 지연이나 유예 시간이 남은 상태일 수 있습니다. 단, 오래 지속되면 stale SA 정리 지연을 의심합니다. |
rekey 직후 XfrmInNoStates 증가 |
이상 | 피어는 새 SPI로 보내기 시작했는데 로컬에 새 inbound SA가 아직 없거나 이미 old SA를 너무 빨리 지운 경우가 많습니다. |
rekey 직후 XfrmInStateProtoError 증가 |
이상 | 새 SA는 찾았지만 키/ICV/ESN 상태가 맞지 않는 경우가 많습니다. old/new SA 혼재 자체보다 새 SA 내용 불일치에 가깝습니다. |
rekey 직후 XfrmInSeqOutOfWindow 증가 |
경계 | 경로 재정렬, HA failover, NIC queue 이동이 겹치면 잠시 나타날 수 있습니다. 지속되면 replay-window와 ESN 상태를 재점검해야 합니다. |
ip xfrm monitor all에서EXPIRE(hard=0)다음에NEWSA가 실제로 오는지 확인합니다.ip -s xfrm state에서 old/new SA가 잠시 함께 보이는지, 그리고 어느 SPI의 카운터가 증가하는지 봅니다.XfrmInNoStates면 새 SA 설치 순서를,XfrmInStateProtoError면 새 SA의 키/알고리즘 일치를,XfrmInSeqOutOfWindow면 replay/재정렬을 우선 확인합니다.
| 이벤트 | 발생 시점 | 실무 해석 |
|---|---|---|
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 임계값 초과 또는 타이머(Timer) 만료 | 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 비트맵(Bitmap), 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)하거나 전용 네트워크로 격리(Isolation)해야 합니다.
xfrm과 네트워크 스택 통합
IPSec/xfrm 프레임워크는 Linux 네트워크 스택에 깊숙이 통합되어 있습니다. 다음 다이어그램은 전체 네트워크 플로우에서 xfrm이 어떻게 위치하고, Netfilter 훅 및 라우팅과 어떻게 상호작용하는지 보여줍니다.
| 처리 단계 | 송신 (TX) | 수신 (RX) |
|---|---|---|
| Netfilter 훅 위치 | OUTPUT → xfrm → POSTROUTING | xfrm_input(SA 조회) → esp_input(복호화) → PREROUTING → 수신 정책 검증 |
| 라우팅 타이밍 | 라우팅 후 xfrm_lookup() 호출 | PREROUTING과 수신 정책 검증을 거친 뒤 라우팅 결정 |
| xfrm 주요 함수 |
xfrm_lookup() → SPD 검색xfrm_output() → SA 적용esp_output() → 암호화
|
xfrm_input() → SA 매칭 + replay 검사esp_input() → 인증 검증 + 복호화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 훅과 별도로 동작하지만 완전히 분리된 계층은 아닙니다. 수신에서는
xfrm_input()과esp_input()이 PREROUTING보다 앞에서 실행됩니다. - 수신 경로: ESP 패킷은 먼저
xfrm_input()에서 SA 조회와 anti-replay 검사를 거치고, 이어esp_input()에서 인증 검증과 복호화를 마친 뒤 PREROUTING으로 들어갑니다. 따라서 일반 소프트웨어 경로에서 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 만료 시 캐시가 무효화됩니다.
esp_output() 상세 경로
ESP 송신 처리는 esp_output_head()에서 skb를 준비하고,
crypto_aead_encrypt()로 암호화한 뒤, 비동기 완료 콜백(Callback)에서
마무리하는 구조입니다. GSO super-packet이면 세그먼트 단위로 분할 후
각각 ESP 헤더를 삽입합니다.
/* net/ipv4/esp4.c — ESP 송신 처리 상세 */
static int esp_output_head(struct xfrm_state *x,
struct sk_buff *skb, struct esp_info *esp)
{
/* 1. skb에 ESP 헤더 + IV 공간 확보 (headroom)
* skb_push(skb, esp->hlen)
* hlen = sizeof(ip_esp_hdr) + ivlen */
/* 2. ESP 헤더 필드 채우기 */
esph->spi = x->id.spi;
esph->seq_no = htonl(XFRM_SKB_CB(skb)->seq.output.low);
/* 3. AEAD nonce 재료 준비
* explicit nonce는 패킷에 기록되고
* salt는 SA(KEYMAT)에 고정 보관됩니다. */
aead_request_set_crypt(req, src, dst, ivlen + clen, iv);
/* 4. ESP trailer 준비
* pad bytes + pad_length + next_header를 뒤에 붙입니다.
* CBC 계열은 블록 정렬 패딩이 필요하고,
* AEAD 계열도 ESP 형식상 trailer는 그대로 유지합니다. */
/* 5. skb 뒤에 ICV(MAC) 공간 확보 (tailroom)
* AES-GCM: 16바이트 ICV */
}
static int esp_output_tail(struct xfrm_state *x,
struct sk_buff *skb,
struct esp_info *esp)
{
/* 6. crypto_aead_encrypt() 호출
* → 동기 완료: 즉시 반환 (AES-NI 인라인 처리)
* → 비동기 완료: -EINPROGRESS 반환
* esp_output_done() 콜백에서 마무리
* (QAT 등 HW 가속기 사용 시) */
err = crypto_aead_encrypt(req);
if (err == -EINPROGRESS) {
/* 비동기: skb를 crypto 큐에 넣고 반환
* 완료 시 esp_output_done()이 호출되어
* xfrm_output_resume()으로 패킷 전송 재개 */
return err;
}
/* 7. 동기 완료: ICV가 skb에 기록됨
* ESP trailer(pad + padlen + NH)도 암호화 영역에 포함 */
}
/* ESP 비동기 완료 콜백 */
static void esp_output_done(void *data, int err)
{
struct sk_buff *skb = data;
/* 암호화 완료 → 패킷 전송 재개 */
xfrm_output_resume(skb->sk, skb, err);
/* → ip_finish_output() → POSTROUTING → NIC TX */
}
/* ESN(Extended Sequence Number) 시퀀스 할당 */
/* 송신 시: xfrm_replay_overflow()로 시퀀스 고갈 검사
* 32비트 seq: 10Gbps에서 ~7분 만에 소진
* ESN 64비트: 상위 32비트는 ICV 계산에만 사용, 패킷에 미포함
* → 10Gbps에서도 ~584년 사용 가능 */
perf top으로 확인하면 ESP 송신 경로에서 가장 CPU를 소모하는 부분은
crypto_aead_encrypt()(AES-GCM 암호화)와 skb_copy_bits()(scatter-gather 처리)입니다.
AES-NI가 있는 x86에서는 aesni_gcm_enc가 인라인 처리되어 동기 완료되지만,
소프트웨어 fallback이나 HW 가속기 경로에서는 비동기로 전환되어 콜백 오버헤드가 추가됩니다.
GSO가 활성화되면 64KB super-packet을 한 번의 crypto 컨텍스트로 처리하여
per-packet 초기화 오버헤드를 극적으로 줄입니다.
esp_input() / xfrm_input() 상세 경로
수신 경로는 송신 경로보다 더 자주 오해됩니다. 실제 커널 흐름은 먼저 SPI로 어떤 SA인지 찾고, 그 SA의 알고리즘과 replay 상태를 사용해 ESP를 검증/복호화한 뒤, 마지막에 평문 패킷이 수신 정책과 맞는지 다시 확인하는 세 단계로 이해하는 편이 정확합니다.
/* net/xfrm/xfrm_input.c + net/ipv4/esp4.c — 수신 처리 개요 */
int xfrm_input(struct sk_buff *skb, int nexthdr,
__be32 spi, int encap_type)
{
/* 1. 외부 ESP 헤더에서 (daddr, spi, proto) 추출 */
/* 2. SAD 해시에서 SA 조회 */
x = xfrm_state_lookup(net, &daddr, spi, nexthdr, family);
/* 3. anti-replay 사전 검사
* 윈도우 밖이거나 이미 본 seq면 여기서 탈락 */
xfrm_replay_check(x, skb, seq);
/* 4. 실제 프로토콜 입력 함수 호출 */
err = x->type->input(x, skb); /* ESP면 esp_input() */
/* 5. 성공 시 replay 비트맵 advance */
xfrm_replay_advance(x, seq);
/* 6. secpath에 처리된 SA 기록 */
/* 7. 복호화된 평문 패킷을 상위 수신 경로로 재주입 */
}
int esp_input(struct xfrm_state *x, struct sk_buff *skb)
{
/* 1. ESP 헤더와 explicit nonce 위치 계산 */
/* 2. AAD 구성: SPI + seq_no (+ ESN 상위 32비트) */
/* 3. crypto_aead_decrypt() 또는 authenc 경로 실행 */
/* 4. ICV 검증 실패 시 관련 xfrm_stat 카운터 증가 */
/* 5. 성공 시 ESP 헤더/nonce/ICV 제거, trailer 해석 */
/* 6. next header를 복원하고 inner IP/TCP/UDP 헤더 포인터 재설정 */
}
/* 그 뒤 평문 skb는 PREROUTING → xfrm_policy_check() → 라우팅으로 진행 */
- SA 조회가 먼저입니다 — 어떤 키와 알고리즘으로 검증할지 알아야 복호화를 시작할 수 있으므로
xfrm_input()의 SAD 조회가 선행됩니다. - replay 검사는 두 단계입니다 — 먼저 "받아도 되는 seq인가"를 보고, 복호화가 성공한 뒤에만 비트맵을 advance합니다. 그래서 인증 실패 패킷은 윈도우를 소모하지 않습니다.
- 정책 검증은 복호화 뒤입니다 — inbound policy는 외부 ESP 헤더가 아니라 복호화된 내부 주소/포트 기준으로 평가해야 의미가 있습니다.
- 관측 지점이 둘입니다 — NIC/드라이버 레벨에서는 여전히 ESP 헤더를 본 뒤 SA를 찾고, 일반 소프트웨어 경로의 PREROUTING/INPUT 규칙은 이미 평문을 보는 경우가 많습니다.
| 수신 단계 | 실패 시 대표 카운터 | 무엇이 틀린 상태인가 | 즉시 확인할 것 |
|---|---|---|---|
1. 외부 헤더 파싱xfrm_input() 진입 전후 |
XfrmInHdrErrorXfrmInBufferError |
ESP/AH 헤더 길이가 이상하거나 skb가 잘려 있어 SPI/seq 추출 자체가 불안정한 상태입니다. | tcpdump -vv -n -i <if> 'esp or udp port 4500'로 온와이어 길이와 단편화를 먼저 봅니다. |
2. SAD 조회xfrm_state_lookup() |
XfrmInNoStatesXfrmInStateInvalidXfrmInStateExpired |
수신 SPI/목적지/프로토콜에 맞는 SA가 없거나, 있어도 larval/dead/expired 상태입니다. | ip -s xfrm state로 SPI, dst/src, mode, reqid, 수명 카운터를 양쪽 장비에서 동시에 비교합니다. |
3. anti-replay 사전 검사xfrm_replay_check() |
XfrmInSeqOutOfWindowXfrmInStateReplayXfrmInStateSeqError |
시퀀스가 윈도우 밖이거나 이미 처리한 패킷이거나, ESN 상위 비트 상태가 어긋난 상태입니다. | RSS/ECMP/멀티패스 재정렬, HA failover 직후 시퀀스 점프, ESN on/off 불일치를 확인합니다. |
4. ESP 인증/복호화esp_input() |
XfrmInStateProtoErrorXfrmInError |
AAD, ICV, nonce, 키, 알고리즘 계열 중 하나가 맞지 않아 태그 검증 또는 복호화가 실패한 상태입니다. | 양쪽 SA의 AEAD 이름, ICV 길이, ESN 여부, NAT-T 여부, 재키 직후 old/new SA 혼재를 우선 확인합니다. |
| 5. 모드/selector 일치 검사 복호화 직후 |
XfrmInStateModeErrorXfrmInStateMismatch |
복호화는 되었지만 transport/tunnel 모드나 selector 해석이 패킷 실제 형태와 맞지 않는 상태입니다. | 한쪽은 tunnel, 다른 쪽은 transport로 설정되지 않았는지, xfrm interface와 mark/if_id가 어긋나지 않았는지 확인합니다. |
6. 수신 정책 재검증xfrm_policy_check() |
XfrmInTmplMismatchXfrmInNoPolsXfrmInPolBlockXfrmInPolError |
SA는 맞았지만 inbound policy가 없거나, 요구 템플릿(reqid/proto/mode)과 실제 적용 SA가 맞지 않는 상태입니다. | ip xfrm policy에서 dir in 정책과 tmpl의 proto/mode/reqid/level을 다시 봅니다. |
XfrmInNoStates는 "복호화가 실패했다"가 아니라 "어떤 키로 복호화할지 못 찾았다"에 가깝고,
XfrmInStateProtoError는 그 다음 단계인 "키는 찾았지만 인증/복호화가 실패했다"에 가깝습니다.
이 둘을 구분하지 않으면 SPI 문제를 키 문제로, 또는 그 반대로 오진하기 쉽습니다.
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)별로 독립된 해시 테이블(Hash Table)에서 수행됩니다.
패킷의 (src, dst, proto, sport, dport)를 셀렉터와 매칭하며,
여러 정책이 매칭되면 priority가 가장 낮은(= 우선순위 높은) 정책이 선택됩니다.
ip xfrm policy set hthresh4 LBITS RBITS,
ip xfrm policy set hthresh6 LBITS RBITS로 해시(Hash) 임계값을 조정할 수 있습니다.
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 이전) */
};
NAT-T 수신 경로 상세
NAT-T 수신 경로의 핵심은 "처음에는 UDP 4500 패킷으로 도착하지만, UDP encap 콜백이 이를 다시 ESP 처리 경로로 넘긴다"는 점입니다. 바깥쪽 네트워크 관측 지점은 UDP를 보고, xfrm은 안쪽에서 SPI를 기준으로 SA를 찾습니다.
/* UDP 4500 NAT-T 수신의 개념적 호출 흐름 */
/* 1. NIC/NAPI → IPv4/IPv6 입력 */
napi_gro_receive()
-> ip_rcv()
-> udp_rcv()
/* 2. UDP socket encap 훅 */
udp_queue_rcv_skb()
-> sk->sk_encap_type 확인
-> encap_rcv(skb) /* ESP-in-UDP 처리 */
/* 3. NAT-T 판별 */
/* - 앞 4바이트가 0이면 Non-ESP marker → IKE */
/* - 0xFF 1바이트면 keepalive */
/* - 그 외는 ESP SPI로 해석 */
/* 4. UDP 바깥껍질 제거 */
/* UDP 8B와 필요 시 marker를 skb에서 당겨 제거 */
/* 5. xfrm 수신 경로로 재주입 */
xfrm_input(skb, nexthdr=50, spi, encap_type)
-> xfrm_state_lookup()
-> xfrm_replay_check()
-> esp_input()
-> PREROUTING / 정책 검증
| 관측 지점 | 보이는 패킷 | 실무 해석 |
|---|---|---|
| NIC / RSS / ethtool 통계 | UDP dst 4500 |
하드웨어 큐 분산은 바깥쪽 5-튜플 기준으로 이뤄집니다. ESP SPI는 아직 큐 선택에 직접 쓰이지 않는 경우가 많습니다. |
| conntrack | UDP 세션 | NAT-T에서는 순수 ESP가 아니라 UDP conntrack이 생성됩니다. timeout이 짧으면 터널 자체보다 NAT 매핑이 먼저 죽을 수 있습니다. |
| udp encap 콜백 이후 | 내부 ESP | 여기서부터 SPI 기반 SA 조회와 replay 검사가 시작됩니다. 즉 NAT-T 문제와 ESP 문제의 경계가 이 지점입니다. |
| PREROUTING / INPUT | 복호화된 평문 | 일반 소프트웨어 경로에서는 이미 UDP 4500이 벗겨지고 ESP도 처리된 뒤라, 내부 주소/포트 기준 필터링으로 이해하는 편이 맞습니다. |
conntrack -L -p udp --dport 4500은 보이는데XfrmInNoStates가 증가하면 UDP/NAT는 통과했지만 ESP SA 매칭이 틀어진 경우가 많습니다.- UDP 4500 트래픽 자체가 끊기거나 keepalive 이후 매핑이 사라지면 ESP보다 먼저 NAT timeout, 방화벽, 경로 MTU를 의심하는 편이 빠릅니다.
forceencaps=yes같은 설정을 쓴 환경에서는 "왜 ESP가 아니라 UDP만 보이지?"가 정상일 수 있습니다. NAT-T 강제 여부를 먼저 확인하세요.
- 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로 다중 터널 가능 |
| 네트워크 네임스페이스(Namespace) | 제한적 (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-6 Dx 이상, Intel E810, Marvell OcteonTX2 cn10k(6.14+ outbound inline IPsec), 일부 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 콜백(Callback), crypto vs packet offload 선택 기준, kTLS/MACsec과의 비교, PCI 가속기(QAT) 연동은 암호화 하드웨어 가속 — 네트워크 암호화 오프로드에서 종합적으로 다룹니다.
/* 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 설정에 크게 의존합니다. 고성능 환경에서는 체계적인 벤치마크와 프로파일링(Profiling)이 필수적입니다.
| 알고리즘 | 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 폴링(Polling)과 상호작용.
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를
빼서 공격 표면을 줄일 수 있습니다.
IPSec과 conntrack 상호작용
IPSec 터널은 Netfilter conntrack과 복잡하게 상호작용합니다. 터널 모드에서는 외부 ESP 패킷과 내부 평문 패킷 각각에 대해 conntrack 엔트리가 생성되며, 이 이중 추적이 conntrack 테이블 고갈과 성능 저하의 원인이 됩니다.
| 문제 | 원인 | 해결 방법 |
|---|---|---|
| conntrack 테이블 고갈 | 터널당 2배 엔트리 소모. 1000개 동시 세션 = 2000개 conntrack | sysctl net.nf_conntrack_max=524288 확대, 또는 ESP 패킷에 NOTRACK 적용 |
| ESP conntrack timeout | ESP(proto 50) conntrack 기본 timeout 30초. DPD 간격보다 짧으면 매핑 소실 | nf_conntrack_generic_timeout 확대 또는 NAT-T 사용 |
| NAT-T UDP timeout | UDP conntrack 기본 stream timeout 120초. keepalive 간격보다 짧으면 NAT 매핑 소실 | keepalive 간격을 timeout의 절반 이하로 설정 (strongSwan: keep_alive = 20s) |
| IPSec + NAT 동시 사용 | SNAT가 내부 src를 변경하면 ESP 복호화 후 selector 불일치 | xfrm policy의 selector에 NAT 후 주소를 사용하거나, -j ACCEPT로 NAT 우회 |
# conntrack 상태 확인
conntrack -L -p esp # ESP conntrack 엔트리 목록
conntrack -L -p udp --dport 4500 # NAT-T 관련 conntrack
conntrack -C # 현재 conntrack 엔트리 수
# ESP 패킷에 NOTRACK 적용 (conntrack 우회)
iptables -t raw -A PREROUTING -p esp -j NOTRACK
iptables -t raw -A OUTPUT -p esp -j NOTRACK
# 또는 nftables로:
nft add rule inet raw prerouting ip protocol esp notrack
nft add rule inet raw output ip protocol esp notrack
# conntrack timeout 조정
sysctl -w net.netfilter.nf_conntrack_generic_timeout=600 # ESP용
sysctl -w net.netfilter.nf_conntrack_udp_timeout_stream=300 # NAT-T용
sysctl -w net.nf_conntrack_max=524288 # 최대 엔트리 수 확대
# IPSec 트래픽을 NAT에서 제외 (SNAT 충돌 방지)
iptables -t nat -A POSTROUTING -m policy --pol ipsec --dir out -j ACCEPT
# → xfrm policy로 보호되는 패킷은 SNAT 건너뜀
NOTRACK으로 우회하세요.
단, NOTRACK을 적용하면 해당 패킷에 대한 상태 기반 방화벽 규칙(-m state)이 동작하지 않으므로,
대신 -m policy --pol ipsec 매칭으로 IPSec 트래픽을 필터링하는 것이 안전합니다.
수동 키잉(Manual Keying) 가이드
IKE 데몬 없이 ip xfrm 명령만으로 IPSec SA/SP를 직접 설정할 수 있습니다.
학습, 테스트, 임베디드 환경에서 활용되지만, 키 교체(rekey)가 불가능하고
보안상 한계가 있으므로 운영 환경에서는 IKE 데몬 사용을 강력히 권장합니다.
- 키 교체 불가 — SA가 만료되면 트래픽이 차단됩니다. 수동으로 새 키를 설치해야 합니다.
- PFS(Perfect Forward Secrecy) 없음 — 키가 노출되면 과거 트래픽도 복호화됩니다.
- AEAD/CTR 모드 금지 — RFC 8221: 수동 키잉에서 AES-GCM, ChaCha20, AES-CTR은 nonce 재사용 위험 때문에 사용하면 안 됩니다. AES-CBC + HMAC-SHA256만 안전합니다.
- anti-replay 제한 — 수동 환경에서 시퀀스 카운터 리셋 시 replay 보호가 무력화됩니다.
#!/bin/bash
# 수동 키잉 — Site-to-Site 터널 전체 설정
# Host A: 203.0.113.1 (LAN: 192.168.1.0/24)
# Host B: 198.51.100.1 (LAN: 192.168.2.0/24)
# ── 키 생성 (양쪽 동일한 키 사용) ──
ENC_KEY=$(openssl rand -hex 16) # AES-128-CBC: 16바이트
AUTH_KEY=$(openssl rand -hex 32) # HMAC-SHA256: 32바이트
SPI_OUT="0x1001"
SPI_IN="0x1002"
# ══════════════════════════════════════
# Host A에서 실행
# ══════════════════════════════════════
# 1. 송신 SA (A → B)
ip xfrm state add \
src 203.0.113.1 dst 198.51.100.1 \
proto esp spi $SPI_OUT mode tunnel \
enc "cbc(aes)" 0x$ENC_KEY \
auth "hmac(sha256)" 0x$AUTH_KEY
# 2. 수신 SA (B → A)
ip xfrm state add \
src 198.51.100.1 dst 203.0.113.1 \
proto esp spi $SPI_IN mode tunnel \
enc "cbc(aes)" 0x$ENC_KEY \
auth "hmac(sha256)" 0x$AUTH_KEY
# 3. 송신 정책 (A → B)
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
# 4. 수신 정책 (B → A)
ip xfrm policy add \
src 192.168.2.0/24 dst 192.168.1.0/24 dir in \
tmpl src 198.51.100.1 dst 203.0.113.1 \
proto esp mode tunnel
# 5. 포워딩 정책 (라우터 역할 시)
ip xfrm policy add \
src 192.168.2.0/24 dst 192.168.1.0/24 dir fwd \
tmpl src 198.51.100.1 dst 203.0.113.1 \
proto esp mode tunnel
# ══════════════════════════════════════
# Host B에서 실행 (방향 반대)
# ══════════════════════════════════════
# 1. 송신 SA (B → A) — SPI_IN 사용
ip xfrm state add \
src 198.51.100.1 dst 203.0.113.1 \
proto esp spi $SPI_IN mode tunnel \
enc "cbc(aes)" 0x$ENC_KEY \
auth "hmac(sha256)" 0x$AUTH_KEY
# 2. 수신 SA (A → B) — SPI_OUT 사용
ip xfrm state add \
src 203.0.113.1 dst 198.51.100.1 \
proto esp spi $SPI_OUT mode tunnel \
enc "cbc(aes)" 0x$ENC_KEY \
auth "hmac(sha256)" 0x$AUTH_KEY
# 정책도 동일한 패턴으로 방향만 반전하여 설정
# (생략 — 위 Host A와 src/dst, dir in/out 반전)
# ── 검증 ──
ip xfrm state list # SA 확인 (양쪽에 각 2개씩)
ip xfrm policy list # SP 확인 (in/out/fwd)
ping -c 3 192.168.2.1 # 터널 테스트
ip -s xfrm state # 카운터 확인 (bytes 증가?)
# ── 삭제 ──
ip xfrm state flush # 모든 SA 삭제
ip xfrm policy flush # 모든 SP 삭제
# 수동 키잉 — Transport 모드 (호스트-to-호스트)
# Host A: 10.0.0.1, Host B: 10.0.0.2
# Host A에서:
ip xfrm state add src 10.0.0.1 dst 10.0.0.2 \
proto esp spi 0x2001 mode transport \
enc "cbc(aes)" 0x$(openssl rand -hex 16) \
auth "hmac(sha256)" 0x$(openssl rand -hex 32)
ip xfrm state add src 10.0.0.2 dst 10.0.0.1 \
proto esp spi 0x2002 mode transport \
enc "cbc(aes)" 0x(동일키) \
auth "hmac(sha256)" 0x(동일키)
ip xfrm policy add src 10.0.0.1 dst 10.0.0.2 dir out \
tmpl src 10.0.0.1 dst 10.0.0.2 proto esp mode transport
ip xfrm policy add src 10.0.0.2 dst 10.0.0.1 dir in \
tmpl src 10.0.0.2 dst 10.0.0.1 proto esp mode transport
xfrm 관련 sysctl 파라미터
xfrm 서브시스템의 동작은 여러 sysctl 파라미터로 조절됩니다. 이 파라미터들은 SA 획득 타임아웃, 가비지 컬렉션(Garbage Collection) 임계값, HA 동기화 간격 등 운영에 직접적으로 영향을 미칩니다.
| 파라미터 | 기본값 | 설명 | 튜닝 가이드 |
|---|---|---|---|
net.core.xfrm_acq_expires |
30 | ACQUIRE 후 IKE 데몬 응답 대기 시간(Latency)(초). 초과 시 larval SA가 삭제되고 패킷이 드롭됩니다. | IKE 협상이 느린 환경(인증서 검증, 원격 RADIUS)에서는 60~120으로 확대 |
net.core.xfrm_aevent_etime |
10000 | NEWAE(Anti-replay Event) 발생 시간 임계값(ms). HA 동기화 간격을 결정합니다. | HA failover 시 replay 오류를 줄이려면 1000~2000으로 감소. 동기화 트래픽 증가에 주의 |
net.core.xfrm_aevent_rseqth |
2 | NEWAE 발생 시퀀스 임계값. N 패킷마다 이벤트를 발생시킵니다. | HA 정밀도를 높이려면 1~2 유지. 높이면 동기화 빈도 감소 |
net.core.xfrm_larval_drop |
1 | 1이면 larval SA(키 미설치) 도착 패킷을 즉시 드롭. 0이면 큐잉 후 대기합니다. | 기본값 1 권장. 0으로 설정하면 ACQUIRE 동안 패킷이 쌓여 메모리 소모 위험 |
net.ipv4.xfrm4_gc_thresh |
32768 | xfrm dst 캐시(bundle) 가비지 컬렉션 임계값. 이 값을 넘으면 GC가 트리거됩니다. | 대규모 SA 환경에서는 SA 수의 2~4배로 설정. 너무 낮으면 빈번한 GC로 성능 저하 |
net.ipv6.xfrm6_gc_thresh |
32768 | IPv6 xfrm dst 캐시 GC 임계값. IPv4와 동일한 역할입니다. | 위와 동일 |
# xfrm sysctl 전체 확인
sysctl -a 2>/dev/null | grep xfrm
# 고성능 VPN 게이트웨이 권장 설정
sysctl -w net.core.xfrm_acq_expires=60 # ACQUIRE 타임아웃 확대
sysctl -w net.core.xfrm_larval_drop=1 # larval 패킷 즉시 드롭
sysctl -w net.ipv4.xfrm4_gc_thresh=65536 # dst 캐시 확대
# HA 환경 설정
sysctl -w net.core.xfrm_aevent_etime=1000 # 1초마다 NEWAE
sysctl -w net.core.xfrm_aevent_rseqth=2 # 2패킷마다 이벤트
# /etc/sysctl.d/99-ipsec.conf 에 영구 저장
cat > /etc/sysctl.d/99-ipsec.conf <<'EOF'
net.core.xfrm_acq_expires = 60
net.core.xfrm_larval_drop = 1
net.ipv4.xfrm4_gc_thresh = 65536
EOF
sysctl --system
ESP-in-TCP 캡슐화
일부 제한적인 방화벽 환경에서는 UDP 4500(NAT-T)마저 차단됩니다.
이 경우 ESP 패킷을 TCP 스트림으로 캡슐화하는 ESP-in-TCP를 사용할 수 있습니다.
커널 5.6+에서 CONFIG_XFRM_ESPINTCP 옵션으로 지원되며,
strongSwan의 TCP 캡슐화 기능과 함께 동작합니다.
| 특성 | NAT-T (UDP 4500) | ESP-in-TCP |
|---|---|---|
| 오버헤드 | +8B (UDP 헤더) | +22B (TCP 헤더 + 길이 프리픽스) |
| TCP-in-TCP 문제 | 없음 (UDP 기반) | 있음: 내부 TCP와 외부 TCP 재전송이 겹쳐 성능 급감 (TCP meltdown) |
| 방화벽 통과 | UDP 4500 허용 필요 | TCP 443(HTTPS)으로 위장 가능 |
| 커널 지원 | 완전 (모든 커널) | 5.6+ (CONFIG_XFRM_ESPINTCP) |
| 성능 | 일반 ESP 대비 ~95% | 일반 ESP 대비 ~40~60% (TCP meltdown으로 인한 저하) |
| IKE 데몬 지원 | strongSwan, Libreswan | strongSwan 5.9+, Libreswan 4.6+ |
# ESP-in-TCP 설정 (strongSwan)
# /etc/swanctl/swanctl.conf
connections {
tcp-tunnel {
version = 2
encap = yes # NAT-T 강제
remote_addrs = vpn.example.com
remote_port = 443 # TCP 443 포트
local {
auth = eap-mschapv2
}
remote {
auth = pubkey
}
children {
tcp-child {
local_ts = 0.0.0.0/0
esp_proposals = aes256gcm128-x25519
}
}
}
}
# strongSwan 서버 측 TCP 리스너 활성화
# /etc/strongswan.conf 또는 swanctl.conf
charon {
port = 500
port_nat_t = 4500
# TCP 캡슐화 리스너
plugins {
socket-default {
# TCP 포트 443에서 IKE/ESP-in-TCP 수신
}
}
}
# 커널 빌드 옵션 확인
zgrep CONFIG_XFRM_ESPINTCP /proc/config.gz 2>/dev/null || \
grep CONFIG_XFRM_ESPINTCP /boot/config-$(uname -r)
# CONFIG_XFRM_ESPINTCP=y 또는 =m 이어야 함
IP-TFS / AggFrag (RFC 9347, 6.14+)
IP-TFS(IP Traffic Flow Security)는 ESP 페이로드 안에 내부 IP 패킷을 집계(Aggregation)하고 큰 내부 패킷을 여러 ESP 레코드로 분할(Fragmentation)하는 캡슐화 모드입니다. IETF가 RFC 9347로 표준화했고, 리눅스 커널은 6.14에서 메인라인에 통합되었습니다. 목표는 두 가지입니다:
- 트래픽 분석 저항(Traffic Flow Confidentiality) — 외부 관찰자가 내부 패킷 크기·간격으로 상위 프로토콜이나 사용자 행동을 추론하지 못하도록, 일정 크기·일정 주기 ESP 패킷 스트림으로 평탄화합니다.
- MTU 효율 — 작은 내부 패킷 여러 개를 하나의 ESP 페이로드에 모아 헤더 오버헤드를 줄이고, 경로 MTU보다 큰 내부 패킷은 IP-level 단편화 없이 ESP 단편으로 분할해 PMTUD 의존도를 낮춥니다.
| 특성 | 일반 ESP (터널 모드) | IP-TFS / AggFrag |
|---|---|---|
| 외부 패킷 크기 | 내부 패킷 크기에 비례 | 고정(또는 정책 기반 평탄화) |
| 외부 패킷 주기 | 애플리케이션 송신 패턴에 비례 | 일정 간격(타이머 기반) |
| 혼잡 제어(Congestion Control) | 해당 없음(상위 프로토콜이 담당) | CC 모드는 ECN/loss 신호 기반 자체 페이싱 |
| 큰 내부 패킷 처리 | IP fragment 또는 PMTUD에 의존 | ESP 레코드 사이로 단편화 — IP fragment 회피 |
| 오버헤드 | 패킷당 ESP 헤더/trailer 1세트 | 여러 내부 패킷이 ESP 1개를 공유 — 평균 오버헤드 감소 |
| 커널 지원 | 모든 버전 | 6.14+ (mainline) |
| 표준 | RFC 4303 (ESP) | RFC 9347 (IP-TFS / AggFrag) |
# IP-TFS 활성화 — ip xfrm state에 iptfs 키워드 추가 (iproute2 6.4+)
# 송신 측 SA
ip xfrm state add src 10.0.0.1 dst 10.0.0.2 \
proto esp spi 0x1000 mode iptfs reqid 1 \
aead 'rfc4106(gcm(aes))' 0x$(openssl rand -hex 20) 128 \
iptfs-pkt-size 1400 # 외부 패킷 크기(고정) \
iptfs-max-queue-size 524288 # 송신 큐 한도(B) \
iptfs-init-delay 100000 # 첫 패킷 대기 us — 집계 효율 \
iptfs-drop-time 1000000 # 재조립 타임아웃 us \
iptfs-reorder-window 3 # 재정렬 윈도우
# 수신 측은 정책에 mode iptfs 명시 — 협상은 IKEv2 확장으로 진행
ip xfrm policy add src 192.168.1.0/24 dst 192.168.2.0/24 \
dir out \
tmpl src 10.0.0.1 dst 10.0.0.2 proto esp mode iptfs reqid 1
# 통계 확인 — iptfs 전용 카운터가 xfrm_stat에 추가됨
ip -s xfrm state get src 10.0.0.1 dst 10.0.0.2 proto esp spi 0x1000
# 커널 빌드 옵션
# CONFIG_XFRM_IPTFS=m (또는 =y)
- 대역 vs 지연:
iptfs-init-delay가 클수록 집계 효율(헤더 오버헤드↓)이 좋아지지만, 애플리케이션 RTT가 늘어납니다. 인터랙티브 트래픽(SSH 등)에는 짧게, 대역 위주 워크로드에는 길게. - 혼잡 제어: CC 모드는 RFC 9347 §2.3.2 알고리즘으로 자체 페이싱하므로 비탄성 트래픽(VoIP, RTP)이 섞이면 손실에 민감해질 수 있습니다.
- HW 오프로드: 6.14 시점 mainline은 SW 처리만 지원합니다. NIC가 ESP packet offload를 지원해도 IP-TFS 모드에서는 fallback이 발생할 수 있습니다.
- strongSwan 6.0+가 IKEv2의 USE_AGGFRAG 협상 확장을 지원합니다.
공식 문서 기준 최신 운영 메모
최종 점검: stable 6.19.13 (2026-04-18), mainline 7.0 (2026-04-12), docs.kernel.org XFRM 문서 기준입니다.
현재 XFRM 공식 문서는 IPsec을 단순히 SPD/SAD와 터널 모드 설명으로 끝내지 않습니다. 최신 운영에서 중요한 축은 하드웨어 오프로드, /proc/net/xfrm_stat 해석, SA lifetime/replay 동기화입니다. 즉 2026년 기준의 xfrm 실무는 "정책을 올렸다"가 아니라 "오프로드와 카운터와 재동기화가 실제로 맞는가"를 보는 쪽으로 이동했습니다.
XFRM 오프로드는 두 종류입니다
공식 xfrm_device 문서에 따르면 커널이 지원하는 하드웨어 오프로드는 두 가지입니다. crypto offload는 NIC가 암복호화만 담당하고 캡슐화 등 나머지는 커널이 처리합니다. packet offload는 NIC가 암복호화뿐 아니라 캡슐화와 정책 상태까지 더 깊게 맡습니다. packet offload에서는 RX 성공 시 XFRM 스택 일부가 우회되므로, 일반 소프트웨어 경로와 계측 위치가 달라집니다.
- crypto offload에서는 커널이 여전히 IPsec 헤더를 넣고 드라이버는 암복호화와 헤더 보정 중심으로 동작합니다.
- packet offload에서는 NIC와 커널이 SA/정책 상태를 동기화해야 하므로 실패 시 영향 범위가 더 큽니다.
- 드라이버는
NETIF_F_HW_ESP,NETIF_F_HW_ESP_TX_CSUM기능 비트와xfrmdev_ops콜백을 제공해야 합니다.
xfrm_input() 이전에 NIC가 무엇을 끝냈는지 먼저 확인해야 합니다.
xfrm_stat 카운터를 먼저 읽어야 합니다
xfrm_proc 문서는 /proc/net/xfrm_stat 카운터가 왜 드롭되었는지를 구분해 준다고 설명합니다. 특히 XfrmInNoStates는 SPI/주소/프로토콜이 맞는 SA를 못 찾은 경우이고, XfrmInStateProtoError는 키나 알고리즘 계열 오류, XfrmInStateSeqError는 replay window 밖 시퀀스 오류입니다. 실무에서는 "IPsec이 안 된다"는 증상을 이 카운터 분류 없이 보면 strongSwan 설정 문제인지, replay 문제인지, 오프로드 폴백 문제인지 구분이 늦어집니다.
XfrmInNoStates— SA 식별 실패XfrmInStateProtoError— 키/알고리즘/변환 계열 오류XfrmInStateSeqError— 시퀀스 번호가 윈도우 밖XfrmInStateExpired— state 만료
HA 동기화는 NEWAE 이벤트를 봐야 합니다
xfrm_sync 문서에 따르면 lifetime 값과 replay 값을 동기화하기 위한 핵심 이벤트는 XFRM_MSG_NEWAE입니다. 커널은 replay threshold 초과 또는 timeout 발생 시 XFRM_AE_CR, XFRM_AE_CE 플래그와 함께 이벤트를 보냅니다. 또한 GETAE 응답에는 항상 lifetime/replay 값 TLV가 들어오며, threshold TLV는 요청 시에만 포함됩니다. 따라서 HA 구성에서는 add/del/upd 이벤트만 복제해도 충분하지 않고, replay/lifetime 계측 이벤트를 함께 수집해야 takeover 후 rekey와 anti-replay 상태가 맞습니다.
주요 1차 자료
- RFC 4301 — SPD/SAD, selector, BYPASS/DISCARD/PROTECT 기본 의미
- RFC 4302 — AH(Authentication Header) 프로토콜 명세, 무결성 보호 범위와 mutable field 처리
- 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 2409 — IKEv1(ISAKMP/Oakley) 키 교환 프로토콜, Main Mode/Aggressive Mode 정의 (역사적 참고용)
- RFC 7296 — IKEv2, IKE_SA_INIT/IKE_AUTH/CREATE_CHILD_SA, NAT detection
- RFC 5996 — IKEv2bis, RFC 4306/4718을 통합한 IKEv2 개정판 (RFC 7296의 전신)
- RFC 4555 — MOBIKE(IKEv2 Mobility and Multihoming), IP 주소 변경 시 SA 유지 메커니즘
- RFC 6311 — IKEv2 고가용성(HA) 확장, 클러스터 간 SA 동기화 프로토콜
- RFC 7383 — IKEv2 메시지 단편화(Fragmentation), MTU 제약 환경에서의 대형 페이로드 처리
- RFC 8221 — ESP/AH 알고리즘 요구사항 최신 정리
- RFC 8247 — IKEv2 알고리즘 요구사항, 암호 스위트별 MUST/SHOULD/MAY 등급 정리
- RFC 9347 — IP-TFS / AggFrag(Aggregation and Fragmentation) ESP 모드, 트래픽 분석 저항과 MTU 효율(리눅스 6.14 메인라인)
- Linux kernel xfrm/index.rst — XFRM 서브시스템 공식 문서 인덱스 페이지(Page)
- 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 관점
- Linux kernel xfrm_interface.rst — XFRM 인터페이스(xfrmi) 설계, if_id 매핑, 라우팅 통합 설명
- Linux kernel ipsec.rst — IPComp corner case와
level use관련 메모 - Linux kernel xfrm_proc —
/proc/net/xfrm_stat카운터 의미 - Linux kernel xfrm_sync — lifetime/replay 동기화와
XFRM_MSG_NEWAE이벤트 - strongSwan Route-based VPN — xfrmi, if_id, install_routes_xfrmi, routing loop 회피
- strongSwan swanctl.conf 레퍼런스 — 연결·자격증명·풀·권한 등 전체 설정 항목 참고 문서
- strongSwan 테스트 인프라 — 자동화 테스트 환경 구성과 시나리오별 테스트 케이스 실행 방법
- Libreswan 위키 — Libreswan IKEv1/IKEv2 구현체의 공식 문서, 설정 예제와 트러블슈팅 가이드
- ip-xfrm(8) man page —
ip xfrm state/policy/monitor명령어 전체 옵션과 사용법 참고 - NIST SP 800-77 Rev. 1 — IPsec VPN 구축 가이드, 아키텍처 선택·알고리즘 권고·운영 고려사항 포함
관련 문서
- 포스트 양자 보안 전환 — ESP와 IKE를 분리해서 보는 전환 기준을 정리합니다.
- WireGuard — 현대적인 VPN 프로토콜
- 네트워크 보안 — Flooding 방어, Netlink, Unix Domain Socket
- Linux Crypto Framework (Crypto API) — 암호화 알고리즘 프레임워크
- Netfilter — 패킷 필터링 및 NAT
- kTLS — L4-7 TLS 커널 오프로드
- eBPF 보안 정책 — BPF 기반 네트워크 보안 정책
- MACsec (IEEE 802.1AE) — L2 암호화와 계층 비교