Kernel TLS (kTLS)
Linux Kernel TLS(kTLS)를 심층 분석합니다. TLS 1.2/1.3 레코드 처리의 커널 오프로딩 경로, ULP 기반 소켓 통합, sendfile/zero-copy 성능 가속, NIC 암호화 오프로드, OpenSSL·Nginx 연계 방식, 적용 가능한 트래픽 패턴과 한계, 성능 회귀·호환성 이슈를 진단하는 운영 디버깅 절차까지 실무 관점으로 설명합니다.
핵심 요약
- 패킷 수명주기 — ingress, 처리, egress 경로를 연결합니다.
- 큐/버퍼 모델 — sk_buff와 큐 지점의 역할을 분리합니다.
- 정책/데이터 분리 — 제어 평면과 데이터 평면을 구분합니다.
- 성능 지표 — PPS, 지연, 드롭 원인을 함께 분석합니다.
- 오프로딩 경계 — NIC/XDP/DPDK 경계를 명확히 유지합니다.
단계별 이해
- 경로 고정
문제가 발생한 ingress/egress 지점을 먼저 특정합니다. - 큐 관찰
백로그와 드롭 위치를 계측합니다. - 정책 반영 확인
라우팅/필터 변경이 데이터 경로에 반영됐는지 봅니다. - 부하 검증
실제 트래픽 패턴에서 재현성을 확인합니다.
kTLS (Kernel TLS) 심화
kTLS (Kernel TLS)는 TLS 레코드 계층의 암호화/복호화를 커널 소켓 계층에서 수행하는 메커니즘입니다.
전통적으로 TLS는 OpenSSL, GnuTLS 등 유저스페이스 라이브러리가 전담했지만,
커널 4.13(TX)과 4.17(RX)부터 대칭키 암호화 처리를 커널로 이동시켜
sendfile() 제로카피, splice, NIC 하드웨어 오프로드 등의 이점을 제공합니다.
setsockopt(SOL_TLS)로 커널에 전달합니다.
이후의 레코드 암호화/복호화만 커널이 처리합니다.
kTLS 아키텍처
유저스페이스 TLS vs kTLS 비교
| 항목 | 유저스페이스 TLS (전통적) | kTLS (커널 TLS) |
|---|---|---|
| 암호화 위치 | 유저스페이스 (OpenSSL 등) | 커널 소켓 계층 (net/tls/) |
| 핸드셰이크 | 유저스페이스 | 유저스페이스 (동일) |
| sendfile() 지원 | 불가 — 데이터를 유저스페이스로 읽고 암호화 후 send() | 가능 — 커널이 파일 → 암호화 → TCP 직접 전달 (제로카피) |
| splice() 지원 | 불가 | 가능 — 파이프를 통한 커널 내 데이터 전달 |
| 컨텍스트 스위칭 | read() → 유저스페이스 암호화 → write() (2회 syscall) | sendfile() 1회 syscall로 완료 |
| 메모리 복사 | 커널→유저→(암호화)→커널 (2~3회 복사) | 커널 내 처리 (0~1회 복사) |
| HW 오프로드 | 불가 (NIC가 유저스페이스 버퍼에 접근 불가) | 가능 — NIC가 TLS 레코드 암/복호화 수행 |
| 정적 파일 서빙 성능 | 기준 (1x) | sendfile kTLS: ~2-4x 처리량 향상 |
| 커널 버전 요구 | 제한 없음 | TX: 4.13+, RX: 4.17+, TLS 1.3: 5.1+ |
kTLS 커널 내부 구조체
/* include/net/tls.h — kTLS 핵심 구조체 */
/* TLS 버전 및 암호 스위트 정보를 담는 컨텍스트 */
struct tls_context {
struct tls_prot_info prot_info; /* TLS 버전, 암호 알고리즘 정보 */
u8 tx_conf : 3; /* TX 모드: SW, HW, HW_RECORD */
u8 rx_conf : 3; /* RX 모드: SW, HW, HW_RECORD */
u8 zerocopy_sendfile : 1; /* 제로카피 sendfile 지원 여부 */
u8 rx_no_pad : 1; /* TLS 1.3 패딩 비활성화 */
int (*push_pending_record)(struct sock *sk, int flags);
void (*sk_write_space)(struct sock *sk);
void *priv_ctx_tx; /* TX 구현별 컨텍스트 (SW 또는 HW) */
void *priv_ctx_rx; /* RX 구현별 컨텍스트 */
struct net_device *netdev; /* HW offload 디바이스 (NULL = SW) */
struct cipher_context tx; /* 송신 암호 컨텍스트 */
struct cipher_context rx; /* 수신 암호 컨텍스트 */
struct scatterlist *partially_sent_record;
u16 partially_sent_offset;
struct list_head list; /* 전역 컨텍스트 리스트 */
refcount_t refcount;
struct rcu_head rcu;
};
/* TLS 프로토콜 정보 */
struct tls_prot_info {
u16 version; /* TLS_1_2_VERSION / TLS_1_3_VERSION */
u16 cipher_type; /* TLS_CIPHER_AES_GCM_128 등 */
u16 prepend_size; /* 레코드 헤더 크기 */
u16 tag_size; /* AEAD 태그 크기 (16바이트 for GCM) */
u16 overhead_size; /* 전체 오버헤드 = prepend + tag */
u16 iv_size; /* IV 크기 */
u16 salt_size; /* salt 크기 (GCM: 4바이트) */
u16 rec_seq_size; /* 레코드 시퀀스 번호 크기 */
u16 aad_size; /* AAD (Additional Auth Data) 크기 */
u16 tail_size; /* TLS 1.3 content type (1바이트) */
};
/* 소프트웨어 TX 컨텍스트 */
struct tls_sw_context_tx {
struct crypto_aead *aead_send; /* AEAD 암호 인스턴스 (AES-GCM 등) */
struct tls_strparser strp; /* TLS 레코드 파서 */
struct sk_msg tx_msg;
struct list_head tx_list; /* 전송 대기 레코드 리스트 */
atomic_t encrypt_pending; /* 비동기 암호화 진행 카운트 */
spinlock_t encrypt_compl_lock;
int async_notify;
u8 async_capable : 1; /* 비동기 암호화 가능 여부 */
/* 비동기 암호화: 암호화 완료를 기다리지 않고
* 다음 레코드를 처리하여 파이프라인 효율 향상.
* 완료 콜백에서 TCP 전송 큐에 데이터 삽입. */
};
/* 소프트웨어 RX 컨텍스트 */
struct tls_sw_context_rx {
struct crypto_aead *aead_recv; /* AEAD 복호 인스턴스 */
struct strparser strp; /* TCP 바이트스트림 → TLS 레코드 분리 */
struct sk_buff_head rx_list; /* 복호화된 레코드 리스트 */
void (*saved_data_ready)(struct sock *sk);
struct sk_buff *recv_pkt; /* 현재 처리 중인 수신 레코드 */
u8 reader_present;
u8 async_capable : 1;
u8 zc_capable : 1; /* 수신 제로카피 가능 */
u8 reader_contended : 1;
atomic_t decrypt_pending;
};
strparser(net/strparser/)는 TCP 수신 데이터를 파싱하여 완전한 TLS 레코드를 추출합니다.
TLS 레코드 헤더의 길이 필드를 읽어 레코드가 완성될 때까지 데이터를 축적하고,
완전한 레코드가 모이면 복호화 콜백을 호출합니다.
kTLS 설정 API — setsockopt() 흐름
kTLS 활성화는 TLS 핸드셰이크 완료 후 setsockopt()로 대칭키를 커널에 전달하는 것으로 시작됩니다.
OpenSSL 3.0+에서는 SSL_set_options(ssl, SSL_OP_ENABLE_KTLS)로 자동 처리됩니다.
/* include/uapi/linux/tls.h — 유저스페이스 API 정의 */
/* TLS 소켓 옵션 레벨 */
#define SOL_TLS 282
/* TLS 소켓 옵션 */
#define TLS_TX 1 /* 송신(TX) kTLS 활성화 */
#define TLS_RX 2 /* 수신(RX) kTLS 활성화 */
/* 지원 TLS 버전 */
#define TLS_1_2_VERSION 0x0303
#define TLS_1_3_VERSION 0x0304
/* 지원 암호 스위트 */
#define TLS_CIPHER_AES_GCM_128 51
#define TLS_CIPHER_AES_GCM_256 52
#define TLS_CIPHER_AES_CCM_128 53
#define TLS_CIPHER_CHACHA20_POLY1305 54
#define TLS_CIPHER_SM4_GCM 55 /* 커널 6.0+ */
#define TLS_CIPHER_SM4_CCM 56 /* 커널 6.0+ */
#define TLS_CIPHER_ARIA_GCM_128 57 /* 커널 6.2+ */
#define TLS_CIPHER_ARIA_GCM_256 58 /* 커널 6.2+ */
/* AES-128-GCM 암호 정보 구조체 (가장 널리 사용) */
struct tls12_crypto_info_aes_gcm_128 {
struct tls_crypto_info info; /* version + cipher_type */
unsigned char iv[8]; /* 명시적 IV (nonce의 가변 부분) */
unsigned char key[16]; /* AES-128 대칭키 */
unsigned char salt[4]; /* 암묵적 nonce (고정 부분) */
unsigned char rec_seq[8]; /* 초기 레코드 시퀀스 번호 */
};
/* AES-256-GCM 암호 정보 구조체 */
struct tls12_crypto_info_aes_gcm_256 {
struct tls_crypto_info info;
unsigned char iv[8];
unsigned char key[32]; /* AES-256 대칭키 */
unsigned char salt[4];
unsigned char rec_seq[8];
};
/* ChaCha20-Poly1305 암호 정보 구조체 */
struct tls12_crypto_info_chacha20_poly1305 {
struct tls_crypto_info info;
unsigned char iv[12];
unsigned char key[32];
unsigned char salt[0]; /* ChaCha20에는 salt 없음 */
unsigned char rec_seq[8];
};
/* ━━━ kTLS 활성화 예제 (유저스페이스 C 코드) ━━━ */
#include <linux/tls.h>
#include <netinet/tcp.h>
/* 1. 일반 TCP 소켓 생성 + TLS 핸드셰이크 (OpenSSL 등) */
int sockfd = socket(AF_INET, SOCK_STREAM, 0);
/* ... connect() + SSL_do_handshake() ... */
/* 2. 핸드셰이크 후 대칭키 추출 (TLS 1.2 + AES-128-GCM 예시) */
struct tls12_crypto_info_aes_gcm_128 crypto_info;
memset(&crypto_info, 0, sizeof(crypto_info));
crypto_info.info.version = TLS_1_2_VERSION;
crypto_info.info.cipher_type = TLS_CIPHER_AES_GCM_128;
/* OpenSSL에서 키/IV/시퀀스 번호 추출 */
SSL_get_key_material(ssl, crypto_info.key, crypto_info.iv,
crypto_info.salt, crypto_info.rec_seq);
/* 3. SOL_TLS + TLS_TX: 커널에 TX 대칭키 전달 → kTLS TX 활성화 */
setsockopt(sockfd, SOL_TLS, TLS_TX,
&crypto_info, sizeof(crypto_info));
/* 4. SOL_TLS + TLS_RX: 커널에 RX 대칭키 전달 → kTLS RX 활성화 */
struct tls12_crypto_info_aes_gcm_128 crypto_info_rx;
/* ... RX 키 설정 ... */
setsockopt(sockfd, SOL_TLS, TLS_RX,
&crypto_info_rx, sizeof(crypto_info_rx));
/* 5. 이후 send()/recv()는 커널이 자동으로 TLS 레코드 암/복호화
* sendfile()도 제로카피로 동작 */
sendfile(sockfd, filefd, &offset, count);
/* → 커널: 파일 페이지 → AES-GCM 암호화 → TLS 레코드 → TCP 전송
* 유저스페이스 복사 없음! */
kTLS TX 경로 — 송신 처리
/* net/tls/tls_main.c — kTLS 초기화 */
/* setsockopt(SOL_TLS, TLS_TX) 호출 시 진입 */
static int do_tls_setsockopt_conf(struct sock *sk,
sockptr_t optval, unsigned int optlen,
int tx)
{
struct tls_context *ctx = tls_get_ctx(sk);
struct tls_crypto_info *crypto_info;
/* 1. 유저스페이스에서 전달한 암호 정보 복사 */
copy_from_sockptr(&crypto_info, optval, ...);
/* 2. NIC HW offload 시도 */
if (tx)
rc = tls_set_device_offload(sk, ctx); /* HW 가능 시 HW 모드 */
/* 3. HW 불가 → SW 모드 폴백 */
if (rc)
rc = tls_set_sw_offload(sk, ctx, tx);
/* → AEAD 인스턴스(aes-gcm) 할당
* → TCP prot을 tls_prots[TLS_SW]로 교체
* → sendmsg/sendpage 콜백이 kTLS 함수로 후킹 */
}
/* net/tls/tls_sw.c — 소프트웨어 TX 경로 */
/* send() / sendmsg() 시 호출되는 kTLS TX 함수 */
int tls_sw_sendmsg(struct sock *sk, struct msghdr *msg, size_t size)
{
struct tls_context *tls_ctx = tls_get_ctx(sk);
struct tls_sw_context_tx *ctx = tls_ctx->priv_ctx_tx;
/* 루프: 유저 데이터를 TLS 레코드 크기(max 16KB)로 분할 */
while (msg_data_left(msg)) {
/* a) 유저 데이터를 scatterlist에 수집 */
tls_push_data(sk, msg, size, flags, TLS_RECORD_TYPE_DATA);
/* b) TLS 레코드 헤더 생성 (content type, version, length) */
tls_fill_prepend(tls_ctx, rec->aad_space, ...);
/* c) AEAD 암호화 (AES-128-GCM) */
crypto_aead_encrypt(aead_req);
/* → IV(nonce) = salt(4B) || explicit_iv(8B)
* → AAD = seq_num(8B) || record_header(5B) [TLS 1.2]
* → AAD = record_header(5B) [TLS 1.3]
* → 평문 → AES-GCM → 암호문 + 16B 태그 */
/* d) 암호화된 레코드를 TCP 전송 큐에 삽입 */
tls_push_record(sk, flags, record_type);
}
}
/* sendfile() 제로카피 경로 */
int tls_sw_sendpage(struct sock *sk, struct page *page,
int offset, size_t size, int flags)
{
/* sendfile() → do_sendpage() → tls_sw_sendpage()
*
* 파일 페이지 캐시를 직접 scatterlist에 매핑:
* page cache → sg_set_page(sg, page, ...) → AEAD 암호화 → TCP
*
* 유저스페이스로의 데이터 복사 없음!
* 기존 방식: read(file→user) + SSL_write(user→kernel) = 2회 복사
* kTLS sendfile: 파일 → 커널 암호화 → TCP = 0회 유저 복사 */
}
kTLS RX 경로 — 수신 처리
/* net/tls/tls_sw.c — 소프트웨어 RX 경로 */
/* strparser가 완전한 TLS 레코드를 수신하면 호출 */
static void tls_strp_msg_ready(struct tls_strparser *strp,
struct sk_buff *skb)
{
/* TCP 바이트 스트림에서 완전한 TLS 레코드를 감지
*
* TLS Record 구조:
* ┌──────────┬─────────┬────────┬───────────┬─────┐
* │ContentType│ Version │ Length │ Encrypted │ Tag │
* │ (1B) │ (2B) │ (2B) │ Payload │(16B)│
* └──────────┴─────────┴────────┴───────────┴─────┘
* ← 5B header → ← Length bytes →
*
* strparser가 Length 필드를 파싱하여 완전한 레코드 경계 결정
*/
}
/* recv() / recvmsg() 시 호출 */
int tls_sw_recvmsg(struct sock *sk, struct msghdr *msg,
size_t len, int flags, int *addr_len)
{
struct tls_sw_context_rx *ctx = ...;
/* 1. strparser가 축적한 완전한 TLS 레코드 가져오기 */
skb = tls_strp_msg_dequeue(&ctx->strp);
/* 2. TLS 레코드 헤더에서 content type 확인 */
/* - TLS_RECORD_TYPE_DATA(23): 일반 데이터 → 복호화 후 유저에 전달
* - TLS_RECORD_TYPE_ALERT(21): TLS 경고 → 에러 처리
* - TLS_RECORD_TYPE_HANDSHAKE(22): 재협상 → 유저스페이스에 전달
* - TLS 1.3: 항목이 항상 APPLICATION_DATA(23)이고,
* 실제 content type은 복호화 후 마지막 바이트에서 확인 */
/* 3. AEAD 복호화 */
err = decrypt_skb(sk, skb, msg);
/* → crypto_aead_decrypt(aead_req)
* → MAC(GCM 태그) 검증 + 복호화
* → 실패 시 TLS_ALERT_BAD_RECORD_MAC 반환 */
/* 4. 복호화된 평문을 유저스페이스 버퍼에 복사 */
err = skb_copy_datagram_msg(skb, rxm->offset, msg, chunk);
/* 제로카피 RX (TLS_RX_EXPECT_NO_PAD 설정 시):
* 유저 버퍼에 직접 복호화하여 중간 커널 버퍼 복사 제거 */
}
sendfile() 제로카피 — kTLS의 핵심 이점
kTLS의 가장 큰 이점은 sendfile() 시스템 콜을 TLS 연결에서 사용할 수 있다는 것입니다.
웹서버의 정적 파일 서빙에서 유저스페이스 데이터 복사를 완전히 제거하여 처리량을 크게 향상시킵니다.
/* ━━━ nginx kTLS sendfile 예시 ━━━ */
/* nginx 1.21.4+ 에서 kTLS 활성화 시:
*
* 정적 파일 서빙 흐름:
* 1. 클라이언트 TLS 핸드셰이크 (OpenSSL)
* 2. OpenSSL이 setsockopt(SOL_TLS, TLS_TX) 호출 → kTLS TX 활성화
* 3. 파일 요청 시 sendfile(client_fd, file_fd, ...) 직접 호출
* 4. 커널: 페이지 캐시 → kTLS 암호화 → TCP 전송
*
* nginx.conf 설정:
* http {
* ssl_conf_command Options KTLS; ← kTLS 활성화
* sendfile on; ← sendfile 활성화
* }
*/
/* 커널 sendfile 경로 (kTLS 활성화 시):
*
* sys_sendfile64()
* → do_sendfile()
* → do_splice_direct()
* → splice_file_to_pipe() ← 파일 → pipe (페이지 참조)
* → pipe_to_sendpage()
* → tls_sw_sendpage() ← kTLS: 페이지 → 암호화 → TCP
* → tls_push_data()
* → tls_do_encryption() ← AES-GCM 암호화
* → tls_push_record() ← TCP 전송 큐 삽입
*/
/* TLS_TX_ZEROCOPY_RO: 읽기 전용 페이지의 진정한 제로카피 (커널 5.19+)
*
* 일반 sendfile: 페이지 복사 후 암호화 (COW 방지)
* ZEROCOPY_RO: 페이지 직접 암호화 → NIC DMA (복사 없음)
*
* 조건: 페이지가 읽기 전용이어야 함 (파일 시스템 페이지 캐시 = OK) */
setsockopt(sockfd, SOL_TLS, TLS_TX_ZEROCOPY_RO, &val, sizeof(val));
kTLS 하드웨어 오프로드
kTLS HW 오프로드는 TLS 레코드의 암호화/복호화를 NIC 하드웨어에서 수행합니다. CPU가 암호 연산을 전혀 수행하지 않아 100Gbps+ 환경에서도 라인 레이트 TLS를 달성할 수 있습니다.
| 오프로드 모드 | 커널 설정 | 동작 | CPU 부하 |
|---|---|---|---|
| SW kTLS | CONFIG_TLS=y |
커널 CPU에서 AES-GCM 암호화/복호화 | AES-NI 사용 시 적당 (코어당 ~10-20Gbps) |
| HW kTLS TX | CONFIG_TLS_DEVICE=y |
NIC가 송신 데이터를 암호화 (TLS 레코드 생성) | 암호화 CPU 0% |
| HW kTLS RX | CONFIG_TLS_DEVICE=y |
NIC가 수신 데이터를 복호화 (TLS 레코드 해제) | 복호화 CPU 0% |
| HW kTLS Record | CONFIG_TLS_DEVICE=y |
NIC가 TLS 레코드 프레이밍까지 수행 | TLS 전체 처리 CPU 0% |
/* net/tls/tls_device.c — HW offload 설정 경로 */
int tls_set_device_offload(struct sock *sk, struct tls_context *ctx)
{
struct net_device *netdev;
struct tls_offload_context_tx *offload_ctx;
/* 1. 소켓이 바인딩된 NIC 디바이스 확인 */
netdev = get_netdev_for_sock(sk);
/* 2. NIC가 kTLS offload를 지원하는지 확인 */
if (!(netdev->features & NETIF_F_HW_TLS_TX))
return -EOPNOTSUPP;
/* 3. NIC 드라이버의 tls_dev_add 콜백 호출 */
rc = netdev->tlsdev_ops->tls_dev_add(netdev, sk,
TLS_OFFLOAD_CTX_DIR_TX, &ctx->crypto_send.info,
tcp_sk(sk)->write_seq);
/* → 드라이버: TLS 연결 정보(키, IV, seq)를 NIC HW에 설치
* → NIC가 이 연결의 모든 TX 패킷을 자동 암호화
*
* 지원 NIC:
* - Mellanox ConnectX-6 Dx / ConnectX-7: mlx5e_ktls_add_tx()
* - Intel E810 (ICE): ice_tls_dev_add()
* - Chelsio T6: cxgb4_ktls_dev_add()
*/
/* 4. TCP prot을 HW offload용으로 교체 */
tls_update_rx_zc_capable(ctx);
ctx->tx_conf = TLS_HW;
}
/* include/net/tls.h — NIC 드라이버가 구현하는 TLS offload ops */
struct tlsdev_ops {
int (*tls_dev_add)(struct net_device *netdev, struct sock *sk,
enum tls_offload_ctx_dir direction,
struct tls_crypto_info *crypto_info,
u32 start_offload_tcp_sn);
void (*tls_dev_del)(struct net_device *netdev,
struct tls_context *ctx,
enum tls_offload_ctx_dir direction);
int (*tls_dev_resync)(struct net_device *netdev,
struct sock *sk, u32 seq,
u8 *rcd_sn, enum tls_offload_ctx_dir direction);
};
/* NIC HW TX offload 패킷 전송 경로:
*
* tls_device_sendmsg() / tls_device_sendpage()
* → tls_push_data() ← 데이터를 SG 리스트에 수집
* → tls_push_record()
* → tcp_sendmsg_locked() ← 평문을 TCP로 전달
* → NIC TX 큐에 enqueue
* → NIC HW가 TLS 레코드 헤더 + 암호화 + 태그를 자동 생성
* → 와이어에 암호화된 TLS 레코드 전송
*
* CPU는 평문을 TCP에 전달만 → 암호화는 NIC가 라인 레이트로 처리
*/
# ━━━ kTLS HW Offload 상태 확인 ━━━
# NIC의 kTLS offload 지원 확인
ethtool -k eth0 | grep tls
# tls-hw-tx-offload: on ← TX 오프로드 지원
# tls-hw-rx-offload: on ← RX 오프로드 지원
# tls-hw-record: on ← 레코드 프레이밍 오프로드
# kTLS offload 활성화/비활성화
ethtool -K eth0 tls-hw-tx-offload on
ethtool -K eth0 tls-hw-rx-offload on
# kTLS 통계 확인 (HW offload 카운터)
ethtool -S eth0 | grep tls
# tx_tls_encrypted_packets: 1234567
# tx_tls_encrypted_bytes: 987654321
# tx_tls_ooo: 0 ← Out-of-order 패킷 (재전송)
# tx_tls_drop_no_sync_data: 0
# rx_tls_decrypted_packets: 654321
# rx_tls_decrypted_bytes: 543210987
# rx_tls_resync_req_pkt: 0 ← RX 동기화 재요청
# rx_tls_resync_req_start: 0
# rx_tls_resync_req_end: 0
# rx_tls_resync_res_ok: 0
# 커널 전역 kTLS 통계
cat /proc/net/tls_stat
# TlsCurrTxSw 42 ← 현재 SW TX 연결 수
# TlsCurrRxSw 38 ← 현재 SW RX 연결 수
# TlsCurrTxDevice 8 ← 현재 HW TX 연결 수
# TlsCurrRxDevice 6 ← 현재 HW RX 연결 수
# TlsTxSw 5000 ← 누적 SW TX 연결
# TlsRxSw 4500 ← 누적 SW RX 연결
# TlsTxDevice 1200 ← 누적 HW TX 연결
# TlsRxDevice 1000 ← 누적 HW RX 연결
# TlsDecryptError 0 ← 복호화 에러 (MAC 검증 실패 등)
# TlsRxDeviceResync 3 ← HW RX 재동기화 횟수
# TlsDecryptRetry 0
# TlsRxNoPadViolation 0 ← no_pad 위반
kTLS HW RX 재동기화 (Resync) 메커니즘
NIC RX 오프로드에서 TCP 재전송, 패킷 유실, 순서 변경이 발생하면 NIC의 TLS 레코드 시퀀스 번호와 실제 TCP 스트림이 불일치할 수 있습니다. 이때 커널과 NIC 사이의 재동기화(resync) 프로토콜이 동작합니다.
/* NIC RX offload 재동기화 흐름:
*
* 정상 상태:
* NIC가 수신 패킷의 TCP seq → TLS record seq 매핑을 유지
* → 패킷 도착 시 자동으로 복호화 후 커널에 전달
*
* 비정상 상태 (패킷 유실/재전송 등):
* NIC가 TLS 레코드 경계를 놓침 → 복호화 실패
*
* 1. NIC: 복호화 실패한 패킷을 암호문 상태로 커널에 전달
* (skb->decrypted = 0)
*
* 2. 커널 (tls_device.c):
* → SW fallback으로 해당 레코드 복호화
* → 정확한 TCP seq ↔ TLS record seq 매핑 계산
*
* 3. 커널 → NIC: tls_dev_resync() 호출
* → "TCP seq X부터 TLS record seq Y" 정보 전달
*
* 4. NIC: 새 매핑으로 HW 테이블 업데이트
* → 이후 패킷부터 다시 HW 복호화 재개
*
* resync 모드:
* - TLS_OFFLOAD_SYNC_TYPE_DRIVER_REQ: 드라이버가 resync 요청
* - TLS_OFFLOAD_SYNC_TYPE_CORE_NEXT_HINT: 커널이 힌트 제공
*/
/* mlx5 드라이버의 resync 구현 예시 */
/* drivers/net/ethernet/mellanox/mlx5/core/en_accel/ktls_rx.c */
static void mlx5e_ktls_rx_resync(struct net_device *netdev,
struct sock *sk,
u32 seq, u8 *rcd_sn)
{
/* NIC HW의 TLS RX 컨텍스트를 새 시퀀스로 업데이트
* → Flow Steering 규칙에 새 TCP seq/TLS seq 매핑 설치
* → 다음 패킷부터 HW 복호화 재개 */
}
kTLS에서 TLS 1.2 vs TLS 1.3 차이
| 항목 | TLS 1.2 | TLS 1.3 |
|---|---|---|
| kTLS 커널 지원 | 4.13+ (TX), 4.17+ (RX) | 5.1+ |
| Content Type 위치 | 레코드 헤더 (평문) | 암호문 뒤 마지막 바이트 (inner content type) |
| 레코드 헤더 | 실제 content type 포함 | 항상 APPLICATION_DATA(23)로 위장 |
| AEAD nonce | salt(4B) + explicit_iv(8B) = 12B | salt(4B) XOR padded_seq(12B) = 12B |
| AAD 구성 | seq_num(8B) + header(5B) = 13B | header(5B) = 5B |
| 패딩 | 없음 | 선택적 패딩 (content type 뒤에 0바이트 추가) |
| 0-RTT 데이터 | 미지원 | Early Data 지원 (kTLS에서는 유저스페이스에서 처리) |
| 키 업데이트 | 재핸드셰이크 (kTLS와 비호환) | KeyUpdate 메시지 (커널 6.0+에서 setsockopt으로 새 키 전달) |
/* TLS 1.3 레코드 처리의 커널 구현 차이 */
/* net/tls/tls_sw.c — TLS 1.3 암호화 시 content type 처리 */
static void tls_fill_prepend(struct tls_context *ctx,
char *buf, size_t plaintext_len,
unsigned char record_type)
{
/* TLS 1.2: 실제 content type을 헤더에 기록 */
/* TLS 1.3: 헤더에 항상 APPLICATION_DATA(23) 기록
* 실제 content type은 평문 끝에 추가 (inner content type)
*
* TLS 1.3 레코드 구조:
* ┌───────────────┬─────────┬──────────┬────────────────┐
* │ Header (5B) │ Encrypt(│ Inner │ AEAD Tag │
* │ type=23,ver, │ Payload │ Content │ (16B) │
* │ length │ │ Type(1B) │ │
* └───────────────┴─────────┴──────────┴────────────────┘
* ← 평문 헤더 → ← 암호화 영역 →
*
* 장점: 외부 관찰자가 content type을 알 수 없음 (프라이버시) */
if (prot->version == TLS_1_3_VERSION) {
buf[0] = TLS_RECORD_TYPE_DATA; /* 항상 23 */
/* plaintext 뒤에 실제 record_type 1바이트 추가 */
} else {
buf[0] = record_type; /* 실제 타입 (23, 22, 21 등) */
}
}
/* TLS 1.3 nonce 생성 (salt XOR seq) */
/* TLS 1.2: nonce = salt(4B) || explicit_iv(8B)
* TLS 1.3: nonce = salt(4B padded to 12B) XOR seq_num(padded to 12B)
*
* TLS 1.3에서는 explicit IV가 전송되지 않아 레코드당 8바이트 절약 */
kTLS 커널 빌드 및 설정
# ━━━ 커널 빌드 옵션 ━━━
# kTLS 소프트웨어 지원 (필수)
CONFIG_TLS=y # 또는 m (모듈)
# kTLS HW offload 지원 (선택)
CONFIG_TLS_DEVICE=y
# 관련 의존성
CONFIG_NET=y
CONFIG_INET=y
CONFIG_CRYPTO=y
CONFIG_CRYPTO_AEAD=y
CONFIG_CRYPTO_GCM=y # AES-GCM 지원
CONFIG_CRYPTO_CHACHA20POLY1305=y # ChaCha20-Poly1305 지원 (선택)
CONFIG_STREAM_PARSER=y # strparser (TLS 레코드 파싱)
# ━━━ 모듈 로드 확인 ━━━
lsmod | grep tls
# tls 126976 2
modinfo tls
# filename: /lib/modules/.../net/tls/tls.ko
# description: Transport Layer Security Support
# license: Dual BSD/GPL
# ━━━ OpenSSL kTLS 지원 확인 ━━━
# OpenSSL 3.0+에서 kTLS 활성화 확인
openssl version -a | grep KTLS
# OPENSSL_KTLS
# OpenSSL 빌드 시 kTLS 활성화
# ./Configure enable-ktls
# 런타임: SSL_CTX_set_options(ctx, SSL_OP_ENABLE_KTLS)
# GnuTLS: ktls 자동 감지 (3.7.2+)
# 커널 TLS 모듈이 로드되어 있으면 자동 사용
# ━━━ nginx kTLS 설정 ━━━
# nginx 1.21.4+ (OpenSSL 3.0+ 링크 필수)
# nginx.conf:
# http {
# ssl_conf_command Options KTLS;
# sendfile on;
# ssl_protocols TLSv1.2 TLSv1.3;
# }
# ━━━ HAProxy kTLS 설정 ━━━
# HAProxy 2.6+ (OpenSSL 3.0+ 링크 필수)
# haproxy.cfg:
# global
# ssl-engine ktls
# # 또는 환경변수: OPENSSL_KTLS=1
kTLS 성능 특성
| 시나리오 | 방식 | 처리량 (단일 코어) | CPU 사용률 | 비고 |
|---|---|---|---|---|
| 정적 파일 서빙 (100KB 파일) |
유저스페이스 TLS | ~3-5 Gbps | 100% | read() + SSL_write() |
| kTLS SW sendfile | ~8-12 Gbps | 100% | sendfile() 제로카피 | |
| kTLS HW offload | ~25-40 Gbps | ~20-30% | NIC 암호화 (ConnectX-6 Dx) | |
| 동적 콘텐츠 (짧은 응답) |
유저스페이스 TLS | ~2-4 Gbps | 100% | send() + SSL_write() |
| kTLS SW | ~3-5 Gbps | 100% | syscall 감소 효과 작음 | |
| 대용량 전송 (1GB+ 파일) |
kTLS SW sendfile | ~15-20 Gbps | 100% | 대용량에서 제로카피 이점 극대 |
| kTLS HW sendfile | ~40-100 Gbps | ~10-15% | NIC 라인 레이트 |
- 동적 콘텐츠에서의 이점 제한: kTLS의 주요 이점은 sendfile() 제로카피입니다. 동적으로 생성된 응답은 이미 유저스페이스 버퍼에 있으므로 제로카피 이점이 제한적. send()/sendmsg()에서는 syscall 오버헤드 감소와 유저-커널 경계 복사 1회 감소 정도
- 소량 데이터 오버헤드: TLS 레코드 헤더(5B) + AEAD 태그(16B) = 최소 21바이트 오버헤드. 매우 작은 메시지에서는 비율이 높아질 수 있음
- HW offload 연결 수 제한: NIC의 TLS 컨텍스트 테이블 크기에 따라 동시 오프로드 가능한 연결 수가 제한됨 (ConnectX-6 Dx: 수십만 연결). 초과 시 SW fallback
- TCP 재전송과 HW offload: TCP 재전송 시 NIC가 이미 전송한 레코드를 재암호화해야 하며, 이때 커널과 NIC 사이 동기화가 필요 (성능 일시 저하)
- renegotiation 비호환: TLS 1.2 renegotiation은 kTLS와 호환되지 않음. TLS 1.3 KeyUpdate는 커널 6.0+에서 지원
kTLS 디버깅 및 모니터링
# ━━━ kTLS 상태 모니터링 ━━━
# 1. 전역 kTLS 통계
cat /proc/net/tls_stat
# 주요 지표:
# TlsCurrTxSw / TlsCurrRxSw — 현재 SW 모드 연결 수
# TlsCurrTxDevice / TlsCurrRxDevice — 현재 HW 모드 연결 수
# TlsDecryptError — 복호화 실패 (MAC 불일치)
# TlsRxDeviceResync — HW RX 재동기화 횟수
# 2. 소켓별 kTLS 상태 확인 (ss 도구)
ss -tni | grep -A1 "kTLS"
# ss -e 옵션으로 소켓 확장 정보에서 kTLS 모드 확인
# 3. NIC HW offload 카운터
ethtool -S eth0 | grep tls
# tx_tls_encrypted_packets / bytes — HW 암호화된 패킷/바이트
# tx_tls_ooo — out-of-order (재전송 등)
# rx_tls_decrypted_packets / bytes — HW 복호화된 패킷/바이트
# ━━━ ftrace / tracepoints ━━━
# kTLS 관련 tracepoints 확인
ls /sys/kernel/debug/tracing/events/tls/ 2>/dev/null || echo "TLS tracepoints 없음"
# 대안: kretprobe로 kTLS 함수 추적
bpftrace -e '
kprobe:tls_sw_sendmsg {
printf("kTLS TX: pid=%d comm=%s size=%d\n",
pid, comm, arg2);
}
kprobe:tls_sw_recvmsg {
printf("kTLS RX: pid=%d comm=%s\n", pid, comm);
}
'
# kTLS 암호화 지연 측정
bpftrace -e '
kprobe:tls_do_encryption { @start[tid] = nsecs; }
kretprobe:tls_do_encryption /@start[tid]/ {
@latency_us = hist((nsecs - @start[tid]) / 1000);
delete(@start[tid]);
}
'
# ━━━ 문제 해결 ━━━
# kTLS 활성화 실패 디버깅
dmesg | grep -i tls
# TLS module loaded
# 또는 에러: "kTLS offload not supported" 등
# OpenSSL kTLS 활성화 확인 (strace)
strace -e setsockopt nginx -t 2>&1 | grep SOL_TLS
# setsockopt(5, SOL_TLS, TLS_TX, ...) = 0 ← 성공
# setsockopt(5, SOL_TLS, TLS_TX, ...) = -1 ENOPROTOOPT ← TLS 모듈 미로드
# TLS 모듈이 로드되지 않은 경우
modprobe tls
# ENOPROTOOPT(92) → modprobe tls 후 재시도
# HW offload 실패 시 SW fallback 확인
cat /proc/net/tls_stat | grep -E "TlsCurr|TlsTx|TlsRx"
# TlsCurrTxDevice = 0이면 HW offload 미작동
# → ethtool -k eth0 | grep tls 로 NIC 지원 확인
# → NIC 드라이버가 tlsdev_ops를 구현했는지 확인
bpf_msg_redirect_map()을 사용하여 kTLS 소켓 간 데이터를 커널 내에서 직접 전달합니다.
kTLS HW Offload 지원 NIC
| NIC 벤더 | 제품 | TX Offload | RX Offload | 커널 드라이버 | 최소 커널 |
|---|---|---|---|---|---|
| NVIDIA/Mellanox | ConnectX-6 Dx / ConnectX-7 | TLS 1.2/1.3 | TLS 1.2/1.3 | mlx5_core |
5.3+ (TX), 5.8+ (RX) |
| Intel | E810 (ICE) | TLS 1.2/1.3 | — | ice |
5.14+ |
| Chelsio | T6 | TLS 1.2 | — | cxgb4 |
5.3+ |
| Broadcom | NetXtreme-E (BCM5750X) | TLS 1.2 | — | bnxt_en |
5.8+ |
| Netronome | Agilio SmartNIC | TLS 1.2 | — | nfp |
5.2+ |
NETIF_F_HW_TLS_TX— NIC가 TX TLS offload를 지원 (ethtool -k에서tls-hw-tx-offload)NETIF_F_HW_TLS_RX— NIC가 RX TLS offload를 지원 (tls-hw-rx-offload)NETIF_F_HW_TLS_RECORD— NIC가 TLS 레코드 프레이밍까지 수행 (tls-hw-record)- NIC가 지원하지 않으면 자동으로 SW kTLS로 fallback (유저에게 투명)
관련 문서
- TCP 프로토콜 — TCP 심화 메커니즘
- Linux Crypto Framework (Crypto API) — 암호화 프레임워크
- WireGuard — VPN 프로토콜
- IPSec & xfrm — IPSec VPN