Kernel TLS (kTLS)

Linux Kernel TLS(kTLS)를 최신 커널 기준으로 분석합니다. TCP_ULP="tls" 활성화, SOL_TLS UAPI, sendfile/splice, TLS 1.3 KeyUpdate, control record, NIC 오프로드, OpenSSL·GnuTLS 연계, 운영 디버깅(Debugging)까지 실무 관점으로 설명합니다.

전제 조건: TCP 프로토콜Linux Crypto Framework (Crypto API) 문서를 먼저 읽으세요. kTLS는 TLS 전체를 커널로 옮기는 기능이 아니라, 이미 협상된 대칭키를 커널 레코드 계층에 설치해 TCP 송수신 경로를 바꾸는 기능입니다.
일상 비유: 이 개념은 택배 포장 작업을 물류센터 내부로 옮기는 것과 비슷합니다. 주소 확인과 접수(핸드셰이크)는 창구 직원이 계속 맡고, 실제 포장과 봉인(레코드 암복호화)만 물류 설비와 자동화 장비(kTLS/NIC offload)에 넘기는 구조입니다.

핵심 요약

  • ULP 부착 — TCP 소켓(Socket)에 TCP_ULP="tls"를 붙여 kTLS 경로를 활성화합니다.
  • 레코드 계층만 이동 — 인증서 검증, 핸드셰이크, 세션 정책은 여전히 유저스페이스 TLS 라이브러리가 담당합니다.
  • TX/RX 독립 설치TLS_TXTLS_RX는 별도 소켓 옵션이며, 재키잉도 방향별로 처리됩니다.
  • 핵심 이점 — 정적 파일 전송에서 sendfile(), splice(), 페이지 캐시(Page Cache) 직결 경로가 가장 큰 이점을 냅니다.
  • 운영 포인트 — KeyUpdate, resync, record size limit, device feature 확인이 성능보다 먼저 점검할 항목입니다.

단계별 이해

  1. 핸드셰이크 완료
    OpenSSL 또는 GnuTLS가 TLS 협상과 인증서 검증을 끝냅니다.
  2. ULP 연결
    setsockopt(SOL_TCP, TCP_ULP, "tls")로 소켓을 TLS ULP로 전환합니다.
  3. 키 설치
    TLS_TX/TLS_RX로 대칭키, IV, 레코드 시퀀스 번호를 커널에 넘깁니다.
  4. 데이터 경로 사용
    이후 send(), recv(), sendfile()이 kTLS 경로로 흘러갑니다.
  5. 예외 처리
    TLS 1.3 KeyUpdate, 재전송(Retransmission), device resync가 들어오면 방향별로 재설정과 모니터링이 필요합니다.
문서 기준 버전: 이 문서는 2026년 3월 7일 기준 최신 stable Linux 6.19.6과 현재 공개된 UAPI/공식 문서를 기준으로 정리합니다. 배포판 LTS 커널에서는 TLS_TX_MAX_PAYLOAD_LEN, TLS_RX_EXPECT_NO_PAD, TLS 1.3 KeyUpdate 동작이 일부 빠질 수 있습니다.
관련 표준: RFC 8446 (TLS 1.3), RFC 5246 (TLS 1.2), RFC 8449 (record_size_limit) — 종합 목록은 참고자료 — 표준 & 규격를 참고하세요.

TLS 기초 개념 — 핸드셰이크와 레코드 계층

kTLS를 이해하려면 먼저 TLS 프로토콜의 두 가지 핵심 계층을 구분해야 합니다. 핸드셰이크 계층(Handshake Layer)은 상대방 인증과 키 교환을 담당하고, 레코드 계층(Record Layer)은 협상된 대칭키로 실제 데이터를 암복호화합니다. kTLS가 커널로 옮기는 것은 레코드 계층만이며, 핸드셰이크는 여전히 유저스페이스 라이브러리가 담당합니다.

TLS 1.3 핸드셰이크 흐름과 kTLS 전환점 클라이언트 서버 핸드셰이크 계층 — 비대칭키 암호화 (유저스페이스 TLS 라이브러리) ClientHello 지원 cipher suite, 랜덤 값, key_share(ECDHE 공개키) ServerHello + EncryptedExtensions + Certificate + Finished 선택된 cipher, 서버 공개키, 인증서, 서명 ECDHE → 공유 비밀 HKDF로 대칭키 유도 ECDHE → 공유 비밀 HKDF로 대칭키 유도 Client Finished 핸드셰이크 완료 확인 MAC kTLS 전환점 setsockopt(SOL_TLS, TLS_TX/RX) — 대칭키를 커널에 설치 레코드 계층 — 대칭키 암호화 (커널 kTLS) Application Data (AES-GCM 암호화된 TLS 레코드) Application Data (AES-GCM 암호화된 TLS 레코드) sendfile() → 커널이 page cache에서 직접 암호화 → TCP 핸드셰이크: 비대칭키(RSA/ECDHE) — 느리지만 키 교환에 필수 레코드 계층: 대칭키(AES-GCM) — 빠르고 kTLS가 처리
구분핸드셰이크 계층레코드 계층 (kTLS)
목적 상대방 인증, 키 교환, cipher suite 협상 협상된 대칭키로 데이터 암복호화
암호화 방식 비대칭키 (RSA, ECDHE) + 서명 대칭키 (AES-GCM, ChaCha20-Poly1305)
처리 위치 유저스페이스 (OpenSSL, GnuTLS) 커널 소켓 계층 (net/tls/)
빈도 연결당 1회 (재연결 시 세션 재개 가능) 모든 데이터 전송마다 (레코드 단위)
성능 특성 CPU 집약적이지만 연결당 1회만 수행 매 바이트마다 수행 — kTLS 오프로드 이점이 여기서 발생
kTLS 관여 관여하지 않음 핵심 처리 영역 — sendfile(), HW offload 가능
비대칭키 vs 대칭키: 비대칭키 암호화(RSA, ECDHE)는 공개키와 개인키 쌍을 사용합니다. 안전하지만 대칭키보다 수백~수천 배 느리므로 초기 키 교환에만 사용합니다. 대칭키 암호화(AES-GCM)는 양쪽이 같은 키를 공유합니다. 매우 빠르고 하드웨어 가속(AES-NI, NIC offload)이 가능하므로 실제 데이터 전송에 사용합니다. kTLS의 역할은 이 빠른 대칭키 암호화를 커널에서 처리하여, sendfile() 같은 커널 내부 데이터 경로를 TLS에서도 활용할 수 있게 하는 것입니다.

kTLS (Kernel TLS)

kTLS (Kernel TLS)는 TLS 레코드 계층의 암호화(Encryption)/복호화(Decryption)를 커널 소켓 계층에서 수행하는 메커니즘입니다. 전통적으로 TLS는 OpenSSL, GnuTLS 같은 유저스페이스 라이브러리가 핸드셰이크와 레코드 계층을 모두 담당했지만, Linux는 커널 4.13부터 TX, 4.17부터 RX 경로를 커널로 넘길 수 있게 하여 sendfile(), splice(), NIC 하드웨어 오프로드 같은 커널 내부 데이터 경로를 TLS에서도 활용할 수 있게 했습니다.

kTLS 핵심 포인트: kTLS는 TLS 핸드셰이크나 인증서 검증을 하지 않습니다. 먼저 소켓을 TCP_ULP="tls"로 전환하고, 핸드셰이크가 끝난 뒤 협상된 대칭키를 setsockopt(SOL_TLS)로 커널에 설치합니다. 즉, 커널이 맡는 것은 레코드 계층과 데이터 경로이며, 키 추출과 정책 판단은 여전히 라이브러리 책임입니다.

kTLS 아키텍처

User Space Application (nginx, HAProxy, ...) TLS Library (OpenSSL, GnuTLS) sendfile() 제로카피 전송 splice() 파이프 전달 Kernel Space kTLS (net/tls/) TLS 레코드 암호화/복호화 · sendfile 제로카피 Crypto Framework (Crypto API) AES-GCM, ChaCha20-Poly1305 TCP 전송/수신 큐 NIC TLS Offload (선택적) — tls-hw-tx/rx-offload NIC Hardware SW kTLS: 커널 CPU 암호화 HW kTLS TX: NIC가 암호화 HW kTLS RX: NIC가 복호화 (기본 동작) (packet-based device offload) (드라이버와 NIC 조합에 따라 지원) TCP_ULP="tls" + SOL_TLS

AEAD 암호화 원리 — AES-GCM 동작 구조

kTLS가 사용하는 암호화 방식은 AEAD(Authenticated Encryption with Associated Data)입니다. AEAD는 암호화와 무결성 검증을 한 번에 수행하는 방식으로, TLS 레코드의 기밀성(Confidentiality)과 무결성(Integrity)을 동시에 보장합니다. kTLS에서 가장 널리 사용되는 AEAD 알고리즘은 AES-GCM(Galois/Counter Mode)입니다.

AES-GCM AEAD 암호화 구조 입력 대칭키 (Key) 16B (128) / 32B (256) Nonce (IV) 12바이트 (고유값) AAD (추가 인증 데이터) TLS 레코드 헤더 (암호화 안 됨) 평문 (Plaintext) 실제 전송 데이터 AES-GCM 엔진 CTR 모드 (Counter Mode) 평문 → AES 블록 암호화 → 암호문 GHASH (Galois Hash) AAD + 암호문 → 인증 태그 계산 출력 암호문 (Ciphertext) 평문과 같은 길이 인증 태그 (Auth Tag) 16바이트 — 위변조 감지용 Nonce (Number used Once) 같은 키로 같은 nonce를 두 번 사용하면 보안이 완전히 깨집니다. kTLS가 rec_seq를 자동 증가시켜 방지합니다. AAD (Additional Authenticated Data) 암호화되지 않지만 무결성이 보장됩니다. TLS 레코드 헤더가 AAD에 포함되어 헤더 위변조 시 복호화가 실패합니다. 인증 태그 (Auth Tag) 수신 측에서 같은 키/nonce/AAD로 태그를 재계산하여 일치 여부를 확인합니다. 불일치 시 TLS alert (bad_record_mac).
/* ━━━ AEAD 암호화의 핵심 개념 ━━━ */

/* 1. 일반 암호화 (AES-CBC 등)는 기밀성만 제공
 *    → 데이터가 암호화되지만, 위변조를 감지할 수 없음
 *    → 별도의 MAC(HMAC-SHA256 등)이 필요 → "Encrypt-then-MAC"
 *
 * 2. AEAD (AES-GCM, ChaCha20-Poly1305)는 한 번에 제공
 *    → 기밀성(Confidentiality): 데이터 암호화
 *    → 무결성(Integrity): 위변조 감지 (인증 태그)
 *    → 인증(Authentication): AAD를 통한 부가 데이터 보호
 *    → TLS 1.3은 AEAD만 허용 (CBC 계열 제거) */

/* kTLS에서 AES-GCM 암호화 흐름:
 *
 *  입력:
 *   ┌─────────────┐  ┌───────────┐  ┌────────────┐  ┌──────────┐
 *   │ Key (16/32B)│  │Nonce (12B)│  │ AAD (5~13B)│  │ 평문     │
 *   └──────┬──────┘  └─────┬─────┘  └──────┬─────┘  └────┬─────┘
 *          │               │               │              │
 *          ▼               ▼               ▼              ▼
 *   ┌──────────────────────────────────────────────────────────┐
 *   │              AES-GCM 엔진 (Crypto API)                  │
 *   │  CTR: AES 블록 암호로 카운터 암호화 → XOR → 암호문      │
 *   │  GHASH: GF(2^128) 곱셈으로 AAD+암호문 해시 → 태그       │
 *   └───────────────────┬────────────────────┬────────────────┘
 *                       │                    │
 *                       ▼                    ▼
 *                ┌──────────────┐   ┌───────────────┐
 *                │ 암호문       │   │ Auth Tag (16B)│
 *                │ (평문과 동일 │   │ 위변조 감지용 │
 *                │  길이)       │   │               │
 *                └──────────────┘   └───────────────┘
 *
 *  복호화 시: 태그 검증 실패 → 데이터 폐기 + TLS alert */

/* Crypto API 호출 예시 (kTLS 내부에서 사용하는 패턴) */
struct crypto_aead *aead;
struct aead_request *req;

/* AEAD 인스턴스 할당 */
aead = crypto_alloc_aead("gcm(aes)", 0, 0);
crypto_aead_setkey(aead, key, key_len);        /* 대칭키 설정 */
crypto_aead_setauthsize(aead, 16);              /* 태그 크기: 16바이트 */

/* 암호화 요청 설정 */
req = aead_request_alloc(aead, GFP_KERNEL);
aead_request_set_crypt(req, src_sg, dst_sg,     /* 입출력 scatterlist */
                       plaintext_len, iv);        /* 평문 길이, IV(nonce) */
aead_request_set_ad(req, aad_len);               /* AAD 길이 */

/* 암호화 실행 (동기 또는 비동기) */
err = crypto_aead_encrypt(req);
/* 결과: dst_sg에 암호문 + 16바이트 태그가 기록됨 */
왜 AEAD인가: TLS 1.2까지는 AES-CBC + HMAC-SHA256 같은 "Encrypt-then-MAC" 방식도 사용했지만, 패딩 오라클(Padding Oracle) 공격 등 여러 취약점이 발견되었습니다. TLS 1.3은 AEAD만 허용하여 이런 문제를 원천 차단합니다. kTLS도 AEAD 알고리즘만 지원하며, CBC 계열 cipher suite는 지원하지 않습니다.

유저스페이스 TLS vs kTLS 비교

항목유저스페이스 TLS (전통적)kTLS (커널 TLS)
암호화 위치 유저스페이스 (OpenSSL 등) 커널 소켓 계층 (net/tls/)
핸드셰이크 유저스페이스 유저스페이스 (동일)
sendfile() 지원 불가 — 데이터를 유저스페이스로 읽고 암호화 후 send() 가능 — 커널이 파일 → 암호화 → TCP 직접 전달 (제로카피)
splice() 지원 불가 가능 — 파이프를 통한 커널 내 데이터 전달
컨텍스트 스위칭(Context Switching) read() → 유저스페이스 암호화 → write() (2회 syscall) sendfile()/splice()에서는 크게 감소, 일반 send()에서는 차이가 제한적
메모리 복사 커널→유저→(암호화)→커널 (2~3회 복사) 커널 내 처리 (0~1회 복사)
HW 오프로드 불가 (NIC가 유저스페이스 버퍼(Buffer)에 접근 불가) 가능 — NIC가 TLS 레코드 암/복호화 수행
정적 파일 서빙 성능 기준 (1x) 가장 큰 개선 지점. workload에 따라 CPU 사용률과 복사 횟수 감소 폭이 크게 달라짐
커널 버전 요구 제한 없음 TX: 4.13+, RX: 4.17+, TLS 1.3: 5.1+, KeyUpdate RX 정지: 6.0+

kTLS 커널 내부 구조체(Struct)

/* 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의 역할: TCP는 바이트 스트림이므로 TLS 레코드 경계가 TCP 세그먼트와 일치하지 않을 수 있습니다. strparser(net/strparser/)는 TCP 수신 데이터를 파싱하여 완전한 TLS 레코드를 추출합니다. TLS 레코드 헤더의 길이 필드를 읽어 레코드가 완성될 때까지 데이터를 축적하고, 완전한 레코드가 모이면 복호화 콜백(Callback)을 호출합니다.

kTLS 설정 API — setsockopt() 흐름

저수준 관점에서 kTLS 활성화 순서는 TCP 연결TCP_ULP="tls" 부착핸드셰이크 완료TLS_TX/TLS_RX 설치입니다. OpenSSL은 SSL_OP_ENABLE_KTLS로 이 과정을 감쌀 수 있고, GnuTLS도 별도 옵션으로 커널 data path를 사용할 수 있지만, 밑단에서는 결국 같은 SOL_TLS UAPI를 호출합니다.

/* include/uapi/linux/tls.h — 유저스페이스 API 정의 */

/* TLS 소켓 옵션 레벨 */
#define SOL_TLS     282

/* TLS 소켓 옵션 */
#define TLS_TX      1     /* 송신(TX) kTLS 활성화 */
#define TLS_RX      2     /* 수신(RX) kTLS 활성화 */
#define TLS_TX_ZEROCOPY_RO 3  /* sendfile() 읽기 전용 zerocopy (device offload 전용) */
#define TLS_RX_EXPECT_NO_PAD 4 /* TLS 1.3 무패딩 가정으로 RX zerocopy 시도 */
#define TLS_TX_MAX_PAYLOAD_LEN 5 /* TX plaintext record size limit */

/* CMSG로 사용하는 control record API */
#define TLS_SET_RECORD_TYPE 1
#define TLS_GET_RECORD_TYPE 2

/* 지원 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 활성화 순서 ━━━ */

#include <linux/tls.h>
#include <netinet/tcp.h>

/* 1. 일반 TCP 소켓 생성 */
int sockfd = socket(AF_INET, SOCK_STREAM, 0);
const char ulp[] = "tls";

/* 2. connect() 후 TLS ULP 부착 */
connect(sockfd, ...);
setsockopt(sockfd, SOL_TCP, TCP_ULP, ulp, sizeof(ulp));

/* 3. 유저스페이스 TLS 라이브러리로 핸드셰이크 수행 */
/*    SSL_do_handshake() / gnutls_handshake() 등 */

/* 4. 라이브러리가 얻은 key/IV/rec_seq를 구조체에 채움
 *    아래 write/read 변수명은 개념 설명용이며 실제 추출 경로는 라이브러리 구현에 의존 */
struct tls12_crypto_info_aes_gcm_128 tx = { 0 };
struct tls12_crypto_info_aes_gcm_128 rx = { 0 };

tx.info.version = TLS_1_2_VERSION;
tx.info.cipher_type = TLS_CIPHER_AES_GCM_128;
memcpy(tx.iv, iv_write, sizeof(tx.iv));
memcpy(tx.rec_seq, seq_write, sizeof(tx.rec_seq));
memcpy(tx.key, key_write, sizeof(tx.key));
memcpy(tx.salt, salt_write, sizeof(tx.salt));

rx.info.version = TLS_1_2_VERSION;
rx.info.cipher_type = TLS_CIPHER_AES_GCM_128;
memcpy(rx.iv, iv_read, sizeof(rx.iv));
memcpy(rx.rec_seq, seq_read, sizeof(rx.rec_seq));
memcpy(rx.key, key_read, sizeof(rx.key));
memcpy(rx.salt, salt_read, sizeof(rx.salt));

/* 5. 방향별로 커널 레코드 계층 설치 */
setsockopt(sockfd, SOL_TLS, TLS_TX,
           &tx, sizeof(tx));
setsockopt(sockfd, SOL_TLS, TLS_RX,
           &rx, sizeof(rx));

/* 6. 이후 send()/recv()/sendfile()은 kTLS data path 사용 */
sendfile(sockfd, filefd, &offset, count);
/* → 커널: 파일 페이지 → TLS 레코드 → TCP 전송 */
라이브러리 연동 포인트: OpenSSL은 SSL_OP_ENABLE_KTLS로 kTLS 사용을 요청할 수 있고, BIO_get_ktls_send()/BIO_get_ktls_recv()로 실제 kernel data path 사용 여부를 확인할 수 있습니다. SSL_sendfile()은 kTLS가 활성화된 경우에만 동작합니다.

전체 동작 C 구현 예시 — kTLS 서버

아래 예시는 kTLS를 직접 활성화하는 최소 TLS 서버의 전체 흐름을 보여줍니다. OpenSSL로 핸드셰이크를 수행한 뒤 세션 키를 커널에 설치하고, sendfile()로 정적 파일을 전송하는 실무 패턴입니다. 에러 처리는 핵심 경로만 포함했습니다.

/* ━━━ kTLS 서버 전체 예시 (OpenSSL 3.0+, Linux 5.1+) ━━━ */

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/socket.h>
#include <sys/sendfile.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <netinet/in.h>
#include <netinet/tcp.h>
#include <openssl/ssl.h>
#include <openssl/err.h>

#define PORT       8443
#define BACKLOG    128
#define FILE_PATH  "/var/www/html/index.html"

static SSL_CTX *create_ssl_context(void)
{
    SSL_CTX *ctx = SSL_CTX_new(TLS_server_method());
    if (!ctx) {
        ERR_print_errors_fp(stderr);
        exit(1);
    }

    /* ━━━ kTLS 핵심 설정 ━━━ */
    SSL_CTX_set_options(ctx, SSL_OP_ENABLE_KTLS);

    /* (선택) device offload 환경에서 sendfile zerocopy */
    SSL_CTX_set_options(ctx, SSL_OP_ENABLE_KTLS_TX_ZEROCOPY_SENDFILE);

    /* TLS 1.3 우선, kTLS와 가장 잘 맞는 cipher suite */
    SSL_CTX_set_min_proto_version(ctx, TLS1_2_VERSION);
    SSL_CTX_set_max_proto_version(ctx, TLS1_3_VERSION);

    /* 인증서/키 로드 */
    if (SSL_CTX_use_certificate_file(ctx, "cert.pem", SSL_FILETYPE_PEM) <= 0 ||
        SSL_CTX_use_PrivateKey_file(ctx, "key.pem", SSL_FILETYPE_PEM) <= 0) {
        ERR_print_errors_fp(stderr);
        exit(1);
    }

    return ctx;
}

static int create_listen_socket(void)
{
    int fd = socket(AF_INET, SOCK_STREAM, 0);
    int opt = 1;
    setsockopt(fd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));

    struct sockaddr_in addr = {
        .sin_family = AF_INET,
        .sin_port   = htons(PORT),
        .sin_addr   = { .s_addr = INADDR_ANY },
    };
    bind(fd, (struct sockaddr *)&addr, sizeof(addr));
    listen(fd, BACKLOG);
    return fd;
}

static void handle_client(SSL_CTX *ctx, int client_fd)
{
    SSL *ssl = SSL_new(ctx);
    SSL_set_fd(ssl, client_fd);

    /* ━━━ 핸드셰이크 수행 ━━━
     * OpenSSL 내부에서 자동으로:
     *   1) setsockopt(SOL_TCP, TCP_ULP, "tls")
     *   2) 핸드셰이크 완료 후 키 추출
     *   3) setsockopt(SOL_TLS, TLS_TX, &crypto_info)
     *   4) setsockopt(SOL_TLS, TLS_RX, &crypto_info)
     */
    if (SSL_accept(ssl) <= 0) {
        ERR_print_errors_fp(stderr);
        goto cleanup;
    }

    /* ━━━ kTLS 활성화 상태 확인 ━━━ */
    int ktls_tx = BIO_get_ktls_send(SSL_get_wbio(ssl));
    int ktls_rx = BIO_get_ktls_recv(SSL_get_rbio(ssl));
    printf("kTLS TX: %s, RX: %s\n",
           ktls_tx ? "ON" : "OFF",
           ktls_rx ? "ON" : "OFF");

    /* ━━━ 요청 수신 (kTLS RX 경로) ━━━ */
    char buf[4096];
    int n = SSL_read(ssl, buf, sizeof(buf) - 1);
    if (n > 0) {
        buf[n] = 0;
        printf("Received: %.40s...\n", buf);
    }

    /* ━━━ 응답: HTTP 헤더 (일반 SSL_write → kTLS sendmsg) ━━━ */
    const char *hdr = "HTTP/1.1 200 OK\r\n"
                       "Content-Type: text/html\r\n"
                       "Connection: close\r\n\r\n";
    SSL_write(ssl, hdr, strlen(hdr));

    /* ━━━ 응답: 파일 본문 (kTLS sendfile 제로카피) ━━━ */
    if (ktls_tx) {
        /* kTLS가 활성화되어 있으면 SSL_sendfile() 사용 가능
         * → 커널: page cache → AEAD 암호화 → TCP
         * → 유저스페이스 복사 0회 */
        int filefd = open(FILE_PATH, O_RDONLY);
        if (filefd >= 0) {
            struct stat st;
            fstat(filefd, &st);
            SSL_sendfile(ssl, filefd, 0, st.st_size, 0);
            close(filefd);
        }
    } else {
        /* kTLS 미활성화: 유저스페이스 fallback */
        int filefd = open(FILE_PATH, O_RDONLY);
        if (filefd >= 0) {
            while ((n = read(filefd, buf, sizeof(buf))) > 0)
                SSL_write(ssl, buf, n);
            close(filefd);
        }
    }

cleanup:
    SSL_shutdown(ssl);
    SSL_free(ssl);
    close(client_fd);
}

int main(void)
{
    SSL_CTX *ctx = create_ssl_context();
    int listen_fd = create_listen_socket();

    printf("kTLS server listening on port %d\n", PORT);

    for (;;) {
        int client_fd = accept(listen_fd, NULL, NULL);
        if (client_fd < 0) continue;
        handle_client(ctx, client_fd);
    }

    SSL_CTX_free(ctx);
    close(listen_fd);
    return 0;
}
# ━━━ 빌드 및 실행 ━━━

# 자체 서명 인증서 생성 (테스트용)
openssl req -x509 -newkey rsa:2048 -keyout key.pem -out cert.pem \
    -days 365 -nodes -subj '/CN=localhost'

# 빌드 (OpenSSL 3.0+ 필요, enable-ktls 빌드 확장)
gcc -O2 -o ktls-server ktls-server.c -lssl -lcrypto

# tls 모듈 로드 확인
sudo modprobe tls

# 서버 실행
./ktls-server

# 테스트 (다른 터미널에서)
curl -k https://localhost:8443/

# kTLS 통계 확인
cat /proc/net/tls_stat | grep TlsCurr
# TlsCurrTxSw  1   ← SW kTLS TX 활성
# TlsCurrRxSw  1   ← SW kTLS RX 활성
코드 핵심 포인트:
  • SSL_OP_ENABLE_KTLS — OpenSSL에 kTLS 사용을 요청합니다. 이 옵션이 없으면 핸드셰이크 후에도 setsockopt(SOL_TLS)를 호출하지 않습니다.
  • BIO_get_ktls_send() — 실제로 kTLS TX가 잡혔는지 확인합니다. 커널 모듈 미로드, cipher suite 불일치 등의 이유로 실패할 수 있습니다.
  • SSL_sendfile() — kTLS TX가 활성화된 경우에만 동작합니다. 내부적으로 sendfile() 시스템 콜을 호출하여 커널 제로카피 경로를 탑니다.
  • fallback 필수: kTLS가 항상 잡힌다는 보장이 없으므로, SSL_write() fallback 경로를 반드시 구현해야 합니다.

kTLS 클라이언트 구현 예시 — HTTPS 요청

앞서 서버 예시를 보았으므로, 이번에는 클라이언트 측에서 kTLS를 활성화하여 HTTPS 요청을 보내는 최소 구현을 보여줍니다. 클라이언트에서도 SSL_OP_ENABLE_KTLS를 설정하면 핸드셰이크 완료 후 송수신 양방향에서 커널 데이터 경로를 활용할 수 있습니다.

/* ━━━ kTLS 클라이언트 예시 (OpenSSL 3.0+, Linux 5.1+) ━━━ */

#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <netinet/tcp.h>
#include <arpa/inet.h>
#include <openssl/ssl.h>
#include <openssl/err.h>

int main(int argc, char *argv[])
{
    const char *host = argc > 1 ? argv[1] : "127.0.0.1";
    int port = argc > 2 ? atoi(argv[2]) : 8443;

    /* ━━━ 1. SSL 컨텍스트 생성 및 kTLS 활성화 ━━━ */
    SSL_CTX *ctx = SSL_CTX_new(TLS_client_method());

    /* kTLS 사용 요청 — 핸드셰이크 후 커널에 키를 설치 */
    SSL_CTX_set_options(ctx, SSL_OP_ENABLE_KTLS);

    /* TLS 1.3 권장 — kTLS와 가장 잘 맞는 프로토콜 */
    SSL_CTX_set_min_proto_version(ctx, TLS1_2_VERSION);
    SSL_CTX_set_max_proto_version(ctx, TLS1_3_VERSION);

    /* 테스트 환경: 인증서 검증 생략 (운영 환경에서는 반드시 검증!) */
    SSL_CTX_set_verify(ctx, SSL_VERIFY_NONE, NULL);

    /* ━━━ 2. TCP 연결 생성 ━━━ */
    int sockfd = socket(AF_INET, SOCK_STREAM, 0);
    struct sockaddr_in addr = {
        .sin_family = AF_INET,
        .sin_port   = htons(port),
    };
    inet_pton(AF_INET, host, &addr.sin_addr);
    connect(sockfd, (struct sockaddr *)&addr, sizeof(addr));

    /* ━━━ 3. TLS 핸드셰이크 수행 ━━━
     * OpenSSL이 내부적으로:
     *   a) setsockopt(SOL_TCP, TCP_ULP, "tls")
     *   b) 핸드셰이크 완료 후 키 추출
     *   c) setsockopt(SOL_TLS, TLS_TX, &crypto_info)
     *   d) setsockopt(SOL_TLS, TLS_RX, &crypto_info)
     * 를 자동으로 수행합니다. */
    SSL *ssl = SSL_new(ctx);
    SSL_set_fd(ssl, sockfd);

    if (SSL_connect(ssl) <= 0) {
        ERR_print_errors_fp(stderr);
        goto cleanup;
    }

    /* ━━━ 4. kTLS 활성화 상태 확인 ━━━ */
    int ktls_tx = BIO_get_ktls_send(SSL_get_wbio(ssl));
    int ktls_rx = BIO_get_ktls_recv(SSL_get_rbio(ssl));
    printf("kTLS TX: %s, RX: %s, Protocol: %s\n",
           ktls_tx ? "ON" : "OFF",
           ktls_rx ? "ON" : "OFF",
           SSL_get_version(ssl));

    /* ━━━ 5. HTTPS 요청 전송 (kTLS TX 경로) ━━━ */
    const char *request =
        "GET / HTTP/1.1\r\n"
        "Host: localhost\r\n"
        "Connection: close\r\n\r\n";
    SSL_write(ssl, request, strlen(request));
    /* kTLS TX 활성화 시: SSL_write() 내부에서
     * 유저 데이터 → 커널 → tls_sw_sendmsg() → AES-GCM → TCP
     * 유저스페이스 암호화 라이브러리 코드를 건너뜀 */

    /* ━━━ 6. 응답 수신 (kTLS RX 경로) ━━━ */
    char buf[8192];
    int n;
    while ((n = SSL_read(ssl, buf, sizeof(buf) - 1)) > 0) {
        buf[n] = 0;
        printf("%s", buf);
    }
    /* kTLS RX 활성화 시: SSL_read() 내부에서
     * NIC → TCP → strparser → tls_sw_recvmsg() → AES-GCM 복호화 → 유저 버퍼
     * 유저스페이스 복호화 과정을 건너뜀 */

cleanup:
    SSL_shutdown(ssl);
    SSL_free(ssl);
    close(sockfd);
    SSL_CTX_free(ctx);
    return 0;
}
# 빌드 및 테스트
gcc -O2 -o ktls-client ktls-client.c -lssl -lcrypto

# tls 모듈 로드 확인
sudo modprobe tls

# 앞서 만든 kTLS 서버에 연결
./ktls-client 127.0.0.1 8443
# kTLS TX: ON, RX: ON, Protocol: TLSv1.3
# HTTP/1.1 200 OK
# ...

# kTLS 상태 확인
cat /proc/net/tls_stat | grep TlsCurr
# TlsCurrTxSw  2   ← 서버+클라이언트 각각 1개
# TlsCurrRxSw  2
클라이언트 vs 서버 kTLS 차이:
  • API는 동일: 클라이언트도 서버도 같은 SSL_OP_ENABLE_KTLSBIO_get_ktls_send()/recv()를 사용합니다.
  • TX/RX 키가 다름: TLS에서 클라이언트와 서버는 서로 다른 대칭키를 사용합니다. 클라이언트의 TX 키는 서버의 RX 키와 같고, 그 반대도 마찬가지입니다.
  • sendfile은 주로 서버 측: 정적 파일 서빙은 서버에서 하므로 SSL_sendfile()의 주된 이점은 서버 측에 있습니다. 클라이언트에서는 SSL_write()/SSL_read()의 커널 경로 최적화가 주된 이점입니다.
  • RX 경로 이점: 대용량 다운로드 클라이언트라면 kTLS RX가 복호화 효율을 높여줍니다.

kTLS 소켓 상태 전이

kTLS 소켓은 ULP 부착부터 종료까지 명확한 상태 전이를 거칩니다. 각 상태에서 sk->sk_prot이 교체되어 sendmsg()/recvmsg() 콜백이 달라집니다. 이 상태 전이를 이해하면 kTLS 디버깅에서 "어느 경로를 타고 왔는가"를 정확히 판별할 수 있습니다.

kTLS 소켓 상태 전이 (State Machine) TCP_ESTABLISHED 일반 TCP 소켓 (tcp_prot) setsockopt(TCP_ULP, "tls") TLS_BASE tls_context 할당됨 TX/RX 키 미설치 (핸드셰이크 진행 중) setsockopt(TLS_TX) setsockopt(TLS_TX) TLS_SW CPU 암복호화 (tls_sw_sendmsg) NIC offload 불가 시 자동 선택 TLS_HW NIC 암복호화 (tls_device_sendmsg) NETIF_F_HW_TLS_TX 지원 시 TLS_HW_RECORD (NIC 전체 오프로드) host stack 일부 우회 KeyUpdate 수신 RX 정지 (EKEYEXPIRED) 새 TLS_RX 설치 후 재개 TLS 1.3 rekey close() / SSL_shutdown() tls_context 해제, AEAD 인스턴스 정리 tx_conf/rx_conf: TLS_BASE(0) → TLS_SW(1) 또는 TLS_HW(2) → close 시 cleanup
/* 상태 전이에 따른 sk->sk_prot 교체 테이블 */

/* ┌────────────────┬─────────────────────────────────────┐
 * │ conf 값        │ sendmsg 콜백                        │
 * ├────────────────┼─────────────────────────────────────┤
 * │ TLS_BASE (0)   │ tls_base_sendmsg()  → 일반 TCP 전달  │
 * │ TLS_SW (1)     │ tls_sw_sendmsg()    → CPU 암호화     │
 * │ TLS_HW (2)     │ tls_device_sendmsg() → NIC 암호화    │
 * │ TLS_HW_REC (3) │ tls_device_sendmsg() → NIC 전체 오프로드 │
 * └────────────────┴─────────────────────────────────────┘
 *
 * close() 시 tls_sk_proto_close()가 호출되어:
 *   1. 미완료 레코드 flush
 *   2. 비동기 암호화 완료 대기
 *   3. HW offload: tls_dev_del() → NIC에서 TLS 컨텍스트 제거
 *   4. AEAD 인스턴스 해제 (crypto_free_aead)
 *   5. tls_context 해제 (kfree_rcu)
 */

static void tls_sk_proto_close(struct sock *sk, long timeout)
{
    struct tls_context *ctx = tls_get_ctx(sk);

    /* TX: 미완료 레코드가 있으면 flush */
    if (ctx->tx_conf == TLS_SW)
        tls_sw_cancel_work_tx(ctx);

    /* HW offload: NIC에서 연결 제거 */
    if (ctx->tx_conf == TLS_HW)
        tls_device_offload_cleanup_rx(sk);

    /* RX: 비동기 복호화 완료 대기 */
    if (ctx->rx_conf == TLS_SW)
        tls_sw_release_resources_rx(sk);

    /* tls_context를 RCU grace period 후 해제 */
    tls_ctx_free(sk, ctx);

    /* 원래 TCP close 호출 */
    ctx->sk_proto_close(sk, timeout);
}
상태 전이의 실무적 의미: tx_confrx_conf독립적입니다. TX만 SW이고 RX는 HW일 수 있습니다. 또한 TLS_BASE 상태에서는 핸드셰이크 트래픽이 여전히 일반 TCP로 흐르며, TLS_TX/TLS_RX를 설치한 순간부터 해당 방향의 데이터가 kTLS 경로를 탑니다.

레코드 경계, control message, record_size_limit

kTLS는 바이트 스트림을 TLS 레코드로 잘라 전송합니다. 기본적으로 각 send() 호출은 레코드 경계를 만들고, MSG_MORE를 주면 레코드 생성을 지연(Latency)하다가 최대 길이(plain text 기준 16KB)에 도달하거나 플래그가 내려갈 때 실제 레코드를 생성합니다. 이 성질 때문에 짧은 write가 매우 많은 워크로드에서는 성능 차이가 제한적일 수 있고, 반대로 sendfile()이나 큰 버퍼 송신에서는 레코드 경계와 복사 횟수를 커널이 더 효율적으로 다룰 수 있습니다.

/* ━━━ control record 송신과 record_size_limit 예시 ━━━ */

static int send_tls_alert(int sockfd, const void *buf, size_t len)
{
    struct msghdr msg = { 0 };
    struct iovec iov = { .iov_base = (void *)buf, .iov_len = len };
    unsigned char record_type = TLS_RECORD_TYPE_ALERT;
    char cbuf[CMSG_SPACE(sizeof(record_type))];
    struct cmsghdr *cmsg;

    msg.msg_iov = &iov;
    msg.msg_iovlen = 1;
    msg.msg_control = cbuf;
    msg.msg_controllen = sizeof(cbuf);

    cmsg = CMSG_FIRSTHDR(&msg);
    cmsg->cmsg_level = SOL_TLS;
    cmsg->cmsg_type = TLS_SET_RECORD_TYPE;
    cmsg->cmsg_len = CMSG_LEN(sizeof(record_type));
    *CMSG_DATA(cmsg) = record_type;

    return sendmsg(sockfd, &msg, 0);
}

/* TLS 1.2: limit 그대로, TLS 1.3: inner content type 1바이트 때문에 -1 해서 넣습니다 */
u16 rsl_tls12 = 4096;
u16 rsl_tls13 = 4095;
setsockopt(sockfd, SOL_TLS, TLS_TX_MAX_PAYLOAD_LEN, &rsl_tls13, sizeof(rsl_tls13));
control record 수신: recvmsg()에 CMSG 버퍼를 준비하면 커널이 TLS_GET_RECORD_TYPE로 레코드 타입을 돌려줍니다. 커널 문서 기준으로 한 번의 recvmsg()는 서로 다른 record type을 섞어 반환하지 않습니다. CMSG 버퍼 없이 alert/handshake 레코드를 받으면 에러가 날 수 있으므로, 라이브러리를 우회한 저수준 구현이라면 반드시 고려해야 합니다.

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 경로 — 수신 처리

ContentType (1B) Version (2B) Length (2B) Encrypted Payload (가변 길이) Tag (16B) ← 5B header → ← Length bytes → TLS Record 구조 (RFC 8446)
/* 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 설정 시):
 * 유저 버퍼에 직접 복호화하여 중간 커널 버퍼 복사 제거 */
}

TLS 1.3 KeyUpdate와 RX 재키잉

TLS 1.3의 KeyUpdate는 kTLS 운영에서 가장 자주 빠지는 예외 경로입니다. 커널은 RX 경로에서 KeyUpdate handshake 레코드를 감지하면 key_update_pending을 세우고 TlsRxRekeyReceived 통계를 증가시킵니다. 그 순간부터 유저스페이스 라이브러리가 새 RX 키를 TLS_RX로 다시 설치할 때까지 recv()EKEYEXPIRED를 반환하고, poll()도 읽기 가능 이벤트를 더 이상 내지 않습니다.

상대편 TLS 엔드포인트 Application Data 이후 KeyUpdate 전송 커널 RX (kTLS) handshake record 감지 key_update_pending = 1 유저 TLS 라이브러리 새 traffic secret 계산 setsockopt(TLS_RX) 재개 복호화 재시작 read 이벤트 복구 재키 설치 전 증상 recv()EKEYEXPIRED poll()은 read-ready를 보고하지 않음 운영 체크포인트 TlsRxRekeyReceived / TlsRxRekeyOk 라이브러리의 rekey 연계 지원 여부 확인
중요: 커널은 새 키를 받기 전까지 RX를 멈춰서 잘못된 키로 복호화하는 일을 막아주지만, 키/nonce 재사용을 검증해주지는 않습니다. 또 커널이 KeyUpdate를 지원하더라도, 실제 유저스페이스 TLS 라이브러리가 kTLS rekey를 끝까지 연결해주지 않으면 RX가 멈춘 것처럼 보일 수 있습니다.

sendfile() 제로카피 — kTLS의 핵심 이점

kTLS의 가장 큰 이점은 sendfile()splice() 같은 커널 내부 데이터 이동 경로를 TLS 연결에서도 그대로 유지할 수 있는 점입니다. 특히 페이지 캐시에 이미 있는 정적 파일을 보낼 때 유저스페이스 왕복 복사를 피할 수 있어, 웹서버와 프록시의 정적 응답 경로가 가장 먼저 이득을 봅니다.

기존 방식 (유저스페이스 TLS) Page Cache ①복사 User Buffer ②암호화 SSL_write() ③복사 TCP → read() + OpenSSL 암호화 + send() = 2회 syscall, 2~3회 메모리 복사 kTLS SW sendfile() Page Cache 직접 참조 kTLS 암호화 TCP → sendfile() 1회 syscall, 유저스페이스 복사 0회 (페이지 캐시 직접 암호화) kTLS HW Offload sendfile() Page Cache DMA TCP NIC 암호화 (HW AES-GCM) → sendfile() 1회 syscall, CPU 암호화 0% (NIC가 전담)
/* ━━━ OpenSSL 기반 kTLS sendfile 예시 ━━━ */

/* OpenSSL이 kTLS를 사용할 수 있게 요청 */
SSL_CTX_set_options(ctx, SSL_OP_ENABLE_KTLS);

/* Linux 전용: device offload 환경에서 sendfile zerocopy 요청 */
SSL_CTX_set_options(ctx, SSL_OP_ENABLE_KTLS_TX_ZEROCOPY_SENDFILE);

/* 핸드셰이크가 끝난 뒤 실제로 kTLS TX 경로가 잡혔는지 확인 */
if (BIO_get_ktls_send(SSL_get_wbio(ssl))) {
    /* OpenSSL 3.0+의 SSL_sendfile()은 kTLS가 켜진 경우에만 동작 */
    SSL_sendfile(ssl, filefd, 0, file_len, 0);
}

/* 저수준 커널 경로 개념:
 *   sendfile()
 *     → splice/direct sendpage
 *     → tls_sw_sendpage() 또는 tls_device_sendpage()
 *     → TCP
 *     → (필요 시) NIC device offload */

/* 읽기 전용 sendfile zerocopy는 결국 아래 UAPI를 통해 커널에 전달됩니다 */
unsigned int one = 1;
setsockopt(sockfd, SOL_TLS, TLS_TX_ZEROCOPY_RO, &one, sizeof(one));
TLS_TX_ZEROCOPY_RO 주의: 커널 문서 기준 이 옵션은 device offload 전용이며, 송신이 끝날 때까지 데이터가 절대 바뀌지 않는다는 가정이 필요합니다. 전송 중 파일이 바뀌면 원래 패킷(Packet)과 재전송 패킷의 내용이 달라져 수신 측에서는 TLS 무결성(Integrity) 오류처럼 보일 수 있습니다.

kTLS 하드웨어 오프로드

Linux 공식 문서는 kTLS 동작 모드를 TLS_SW, TLS_HW, TLS_HW_RECORD 세 가지로 설명합니다. 여기서 실무적으로 가장 중요한 것은 packet-based device offloadTLS_HW입니다. 이 모드에서는 Linux TCP 스택은 계속 살아 있고, NIC가 패킷별 암복호화만 맡습니다.

모드주요 경로장점주의점
SW kTLS 커널이 레코드 프레이밍과 암복호화를 직접 수행 host stack과 완전히 호환, 디버깅이 가장 단순 CPU 암호화 비용은 그대로 남음
HW kTLS TX 커널이 레코드 메타데이터를 만들고 NIC가 송신 암호화 수행 정적 파일·대용량 송신에서 CPU 절감 폭이 큼 5-tuple 매칭, 재전송, 드라이버 구현 상태에 영향받음
HW kTLS RX NIC가 수신 패킷을 복호화하고 커널은 record decap만 수행 고속 수신에서 CPU 사용량을 크게 줄일 수 있음 패킷 재정렬 시 resync와 software fallback이 중요
HW kTLS Record NIC/펌웨어(Firmware)가 Linux networking stack 일부를 사실상 대체 일부 환경에서는 더 높은 오프로드율 가능 공식 문서 기준 host firewall, QoS, packet scheduling 의존 환경에는 부적합
운영상 중요한 제약: 커널 문서 기준으로 오프로드 모드는 NIC 설정에 따라 자동 선택되며, 현재는 연결별로 명시적 opt-in/opt-out을 세밀하게 제어하는 방식이 아닙니다. 또한 소프트웨어 인터페이스를 거치는 터널(Tunnel)·가상화(Virtualization) 경로에서는 5-tuple 매칭이 흐려져 RX offload가 깨끗하게 동작하지 않을 수 있습니다.
/* 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에 설치
     * → 대표적으로 mlx5, ice, cxgb4 계열 드라이버에서 이런 패턴을 볼 수 있음
     * → 실제 지원 범위는 펌웨어/커널/드라이버 버전에 따라 확인 필요 */

    /* 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          ← full record/TCP offload 성격, host stack 우회 가능성 주의

# 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 검증 실패 등)
# TlsDeviceRxResync             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.2TLS 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
패딩(Padding) 없음 선택적 패딩 (content type 뒤에 0바이트 추가)
0-RTT 데이터 미지원 핸드셰이크와 함께 유저스페이스에서 처리, kTLS data path 이전 단계
키 업데이트 재핸드셰이크 (kTLS와 비호환) KeyUpdate 메시지 수신 시 RX 정지 후 새 TLS_RX 설치 필요
Header (5B) type=23, ver, length Encrypt(Payload) (가변 길이) Inner Content Type (1B) AEAD Tag (16B) ← 평문 헤더 → ← 암호화 영역 → TLS 1.3 레코드 구조 — 외부 관찰자에게 content type 은닉
/* 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)

# OpenSSL 3.2+ CLI는 -ktls 옵션 제공
openssl s_server -accept 8443 -cert cert.pem -key key.pem -WWW -ktls -sendfile
openssl s_client -connect 127.0.0.1:8443 -ktls

# sendfile zerocopy까지 요청하려면 SSL_OP_ENABLE_KTLS_TX_ZEROCOPY_SENDFILE 사용
# 단, Linux에서는 device offload 전용이며 송신 데이터 변경 금지

# GnuTLS는 GNUTLS_ENABLE_KTLS와 gnutls_record_send_file()를 제공하지만
# manual 기준 KTLS re-key 동작은 제약이 있으므로 RX KeyUpdate 운용 전 확인 필요

kTLS 성능 특성

시나리오예상 이점왜 이득이 나는가병목(Bottleneck)/주의점
정적 파일 + sendfile() 가장 큼 페이지 캐시를 그대로 사용하고 유저스페이스 왕복 복사를 없앰 파일이 자주 바뀌거나 sendfile을 쓰지 못하면 이점이 줄어듦
대용량 스트리밍 송신 중간~큼 커널이 레코드 경계와 버퍼 관리를 안정적으로 처리 암호화 자체는 여전히 CPU 또는 NIC 성능에 종속
짧은 동적 응답 작음~중간 copy 수와 경계 처리 일부가 줄더라도 sendfile 이점은 없음 작은 write가 많으면 MSG_MORE와 응답 묶음 전략이 더 중요
TLS 1.3 RX + no_pad 가정 조건부 TLS_RX_EXPECT_NO_PAD가 맞으면 유저 버퍼 직접 복호화 가능 신뢰하지 않는 peer에는 공격 벡터가 될 수 있고, 오판 시 재복호화 비용 발생
device offload CPU 절감 폭이 큼 NIC가 암복호화를 맡아 host CPU 여유가 커짐 연결 수 한도, resync, 5-tuple 매칭, 드라이버 성숙도 확인 필요
kTLS 성능 주의사항:
  • 진짜 이득은 정적 파일 경로에 집중: 동적 응답은 이미 유저 버퍼에 있으므로 kTLS가 있어도 sendfile 급의 이득은 잘 나오지 않습니다.
  • record 경계가 중요: 짧은 write가 매우 많으면 레코드 헤더와 AEAD tag 오버헤드(Overhead)가 상대적으로 커집니다. MSG_MORE와 적절한 batching을 같이 봐야 합니다.
  • device offload는 CPU를 절약하는 기술: 항상 지연이 줄어드는 것은 아니며, 연결 수 한도와 resync 비용이 결과를 좌우합니다.
  • TLS_RX_EXPECT_NO_PAD는 신뢰 경계 안에서만: 커널 문서가 명시적으로 비용 증가 공격 벡터를 경고합니다.
  • TLS 1.2 renegotiation은 별개: kTLS는 record layer 최적화이므로, 전통적인 renegotiation과 동일한 기대를 하면 안 됩니다.

kTLS 디버깅 및 모니터링

# ━━━ kTLS 상태 모니터링 ━━━

# 1. 전역 kTLS 통계
cat /proc/net/tls_stat
# 주요 지표:
# TlsCurrTxSw / TlsCurrRxSw       — 현재 SW 모드 연결 수
# TlsCurrTxDevice / TlsCurrRxDevice — 현재 HW 모드 연결 수
# TlsDecryptError                   — 복호화 실패 (MAC 불일치)
# TlsDeviceRxResync                 — HW RX 재동기화 횟수
# TlsTxRekeyOk / TlsRxRekeyOk       — 재키 성공
# TlsRxRekeyReceived                — KeyUpdate 수신 횟수

# 2. 사용자 라이브러리가 실제로 kTLS를 붙였는지 확인
strace -f -e trace=setsockopt ./your-server 2>&1 | grep -E "TCP_ULP|SOL_TLS"
# 기대 패턴:
# setsockopt(fd, SOL_TCP, TCP_ULP, "tls", ...)
# setsockopt(fd, SOL_TLS, TLS_TX, ...)
# setsockopt(fd, SOL_TLS, TLS_RX, ...)

# 3. OpenSSL 빠른 재현 (3.2+ CLI)
openssl s_server -accept 8443 -cert cert.pem -key key.pem -WWW -ktls -sendfile
openssl s_client -connect 127.0.0.1:8443 -ktls

# 4. NIC feature / 통계
ethtool -k eth0 | grep tls
ethtool -S eth0 | grep tls
# tx_tls_encrypted_packets / bytes — HW 암호화된 패킷/바이트
# tx_tls_ooo — out-of-order (재전송 등)
# rx_tls_decrypted_packets / bytes — HW 복호화된 패킷/바이트

# 5. KeyUpdate 문제 의심 시
cat /proc/net/tls_stat | grep -E "Rekey|Decrypt"
# recv() = -1 EKEYEXPIRED 이면 새 TLS_RX 키가 아직 커널에 설치되지 않은 상태

# ━━━ 추적 ━━━

# tracepoint가 없을 수도 있으므로 먼저 확인
ls /sys/kernel/debug/tracing/events/tls/ 2>/dev/null || echo "TLS tracepoints 없음"

# 대안: kprobe로 핵심 함수 추적
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]);
}
'

# ━━━ 문제 해결 ━━━

# ENOPROTOOPT / KTLS 미사용
modprobe tls
# 이후 OpenSSL이 BIO_get_ktls_send()/recv()에서 1을 주는지 확인

# HW offload가 안 붙는 경우
cat /proc/net/tls_stat | grep -E "TlsCurr(Tx|Rx)Device|TlsDeviceRxResync"
# 0이면 ethtool feature, routing 경로, 드라이버/펌웨어 지원을 순서대로 점검
kTLS + BPF sockmap: BPF sockmap과 kTLS를 조합하면 커널 내에서 TLS 복호화 → BPF 프로그램 → 다른 소켓으로 전달을 유저스페이스 개입 없이 처리할 수 있습니다. 이는 L7 프록시(Envoy, Cilium)에서 커널 사이드카 가속에 활용됩니다. bpf_msg_redirect_map()을 사용하여 kTLS 소켓 간 데이터를 커널 내에서 직접 전달합니다.

kTLS HW Offload 지원 판단법

확인 항목어디서 보나의미
feature 비트 ethtool -k eth0 | grep tls tls-hw-tx-offload, tls-hw-rx-offload, tls-hw-record 노출 여부가 1차 진단 포인트
드라이버 통계 ethtool -S eth0 | grep tls tx_tls_*, rx_tls_*, resync 계열 카운터가 실제 offload 동작 여부를 보여줌
커널 구성 CONFIG_TLS, CONFIG_TLS_DEVICE SW kTLS만 필요한지, device offload까지 필요한지 먼저 구분해야 함
대표 드라이버 예 mlx5_core, ice, cxgb4 공식 문서와 커널 소스에서 자주 보는 계열. 실제 지원 조합은 펌웨어와 커널 버전을 함께 봐야 함
연결 수 한도 devlink resource (드라이버가 노출하는 경우) 공식 offload 문서는 최대 연결 수를 devlink resource로 노출할 수 있다고 설명함
NETIF_F_HW_TLS 피처 플래그:
  • 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 (유저에게 투명)

참고자료

TLS In-Kernel 처리 — ULP 등록과 데이터 경로

kTLS의 핵심은 TCP 소켓에 ULP(Upper Layer Protocol)로 TLS를 등록하여, 기존 TCP sendmsg/recvmsg 콜백을 kTLS 전용 함수로 교체하는 것입니다. 이 절에서는 ULP 등록 메커니즘부터 tls_sw_sendmsg()tls_sw_recvmsg()의 내부 경로를 커널 소스 수준에서 분석합니다.

ULP 등록 → prot 교체 → kTLS 데이터 경로 setsockopt() SOL_TCP, TCP_ULP, "tls" tcp_set_ulp() ULP ops 탐색 및 init 호출 tls_init() tls_context 할당 prot 교체 tls_prots[TLS_BASE] setsockopt(SOL_TLS, TLS_TX) — do_tls_setsockopt_conf() tls_set_sw_offload() 또는 tls_set_device_offload() setsockopt(SOL_TLS, TLS_RX) — do_tls_setsockopt_conf() AEAD 인스턴스 할당, strparser 초기화 데이터 경로 (prot 교체 이후) send() / sendmsg() tls_sw_sendmsg() tls_push_data() → crypto_aead_encrypt() TCP 전송 큐 NIC NIC TCP 수신 큐 strparser TLS 레코드 조립 tls_sw_recvmsg() crypto_aead_decrypt() → 유저 버퍼 복사 sendfile() tls_sw_sendpage() page cache 직접 참조 → sg_set_page() 암호화 → TCP TX 경로 RX 경로: NIC → strparser → recvmsg → 복호화 → 유저 sendfile 제로카피 경로
/* net/tls/tls_main.c — ULP 등록 구조 */

/* ULP ops: TCP 소켓에 "tls" ULP를 등록하면 tls_init()이 호출됨 */
static struct tcp_ulp_ops tcp_tls_ulp_ops __read_mostly = {
    .name       = "tls",
    .owner      = THIS_MODULE,
    .init       = tls_init,          /* ULP 초기화 (tls_context 할당) */
    .update     = tls_update,        /* 소켓 옵션 변경 시 콜백 */
    .get_info   = tls_get_info,      /* getsockopt/diag 정보 제공 */
    .get_info_size = tls_get_info_size,
};

/* tls_init(): TCP 소켓에 TLS 컨텍스트를 부착 */
static int tls_init(struct sock *sk)
{
    struct tls_context *ctx;

    /* 1. tls_context 할당 및 초기화 */
    ctx = tls_ctx_create(sk);
    if (!ctx)
        return -ENOMEM;

    /* 2. sk->sk_prot을 tls_prots[TLS_BASE]로 교체
     *    이 시점에서는 아직 TX/RX 키가 설치되지 않았으므로
     *    sendmsg/recvmsg는 기존 TCP 동작을 유지 */
    tls_build_proto(sk);
    ctx->tx_conf = TLS_BASE;
    ctx->rx_conf = TLS_BASE;

    /* 3. 이후 setsockopt(SOL_TLS, TLS_TX/RX)로
     *    키를 설치하면 TLS_SW 또는 TLS_HW 모드로 전환 */
    return 0;
}

/* prot 교체 테이블 — conf 값에 따라 다른 콜백 세트 사용 */
enum {
    TLS_BASE,       /* ULP 부착만 완료, 키 미설치 */
    TLS_SW,         /* 소프트웨어 암복호화 */
    TLS_HW,         /* NIC 하드웨어 오프로드 */
    TLS_HW_RECORD, /* NIC 레코드 오프로드 */
    TLS_NUM_CONFIG,
};

/* tls_sw_sendmsg 내부 상세 — 레코드 분할과 암호화 루프 */
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;
    int record_type = TLS_RECORD_TYPE_DATA;

    /* CMSG에서 record type 추출 (alert, handshake 등) */
    if (msg->msg_controllen) {
        record_type = tls_get_record_type(sk, msg);
    }

    while (msg_data_left(msg)) {
        /* (a) 현재 open record에 데이터 추가 */
        tls_push_data(sk, msg, send, flags, record_type);

        /* (b) MSG_MORE가 아니거나 최대 레코드 크기 도달 시 */
        if (!(flags & MSG_MORE) || full) {
            /* TLS 레코드 헤더 생성 */
            tls_fill_prepend(tls_ctx, ...);

            /* AEAD 암호화 요청 (동기 또는 비동기) */
            tls_do_encryption(sk, tls_ctx, ctx, ...);

            /* 암호화된 레코드를 TCP 전송 큐에 삽입 */
            tls_push_record(sk, flags, record_type);
        }
    }
    return copied;
}
ULP 등록의 의미: TCP_ULP는 TCP 소켓 위에 프로토콜 계층을 추가하는 일반적 메커니즘입니다. kTLS 외에도 mptcp가 이 경로를 사용합니다. ULP를 부착하면 sk->sk_prot 테이블이 교체되어, 이후의 모든 sendmsg()/recvmsg() 호출이 TLS 래핑을 거칩니다.

Zero-Copy sendfile — splice 연동과 페이지(Page) 핀닝

kTLS에서 sendfile()파일 페이지 캐시를 유저스페이스로 복사하지 않고 커널 내에서 바로 TLS 레코드로 암호화하여 TCP에 전달합니다. 이 경로의 핵심은 페이지 캐시 페이지를 scatterlist에 직접 매핑(Mapping)하는 것으로, NGINX나 HAProxy 같은 웹서버에서 정적 파일 서빙 성능을 크게 향상시키는 주된 메커니즘입니다.

sendfile() Zero-Copy 경로 상세 sendfile() syscall 진입 do_splice_direct() 파일 → 소켓 파이프 tls_sw_sendpage() page를 scatterlist에 매핑 AEAD 암호화 AES-GCM / ChaCha20 Page Cache Page 0 Page 1 Page 2 get_page() 참조 카운트 증가 Scatterlist (SG) sg[0] sg[1] sg[2] sg_set_page(sg, page, len, off) TLS Record 출력 Hdr Encrypted Payload Tag → TCP 전송 큐에 삽입 splice() 연동 — 파이프 기반 제로카피 splice(file_fd, pipe_fd) → splice(pipe_fd, sock_fd) 파이프 버퍼의 page 참조를 kTLS sendpage()에 전달 — 복사 없이 암호화 후 전송 내부적으로 sendfile()도 splice 기반으로 구현됨 (do_splice_direct)
/* sendfile → kTLS 제로카피 경로 상세 */

/* 커널 내부: sendfile()의 실제 구현 */
/* fs/read_write.c */
SYSCALL_DEFINE4(sendfile64, int, out_fd, int, in_fd,
                loff_t __user *, offset, size_t, count)
{
    /* sendfile()은 내부적으로 splice 메커니즘을 사용 */
    return do_splice_direct(in_file, &pos,
                            out_file, &out_pos,
                            count, fl);
    /* → 파일의 page cache 페이지를 파이프 버퍼에 참조로 넣고
     *   소켓의 sendpage()를 호출하여 직접 전달 */
}

/* net/tls/tls_sw.c — sendpage 경로 */
int tls_sw_sendpage(struct sock *sk, struct page *page,
                    int offset, size_t size, int flags)
{
    /* page pinning: 페이지 참조 카운트 증가 */
    get_page(page);

    /* scatterlist에 페이지 직접 매핑 — 복사 없음! */
    sg_set_page(&sg[i], page, size, offset);

    /* AEAD 암호화: page cache의 데이터를 in-place로 읽어
     * 암호화된 결과를 별도 버퍼에 생성 (원본 page 변경 안 함) */
    tls_do_encryption(sk, tls_ctx, ctx, ...);

    /* 암호화 완료 후 TCP 전송 큐에 삽입
     * page는 TCP가 전송 완료할 때까지 pinned 상태 유지 */
    tls_push_record(sk, flags, record_type);
}

/* 성능 이점 요약:
 *
 * 전통적 경로 (유저스페이스 TLS):
 *   read(file_fd, buf, len)    ← Page Cache → User Buffer (복사 1)
 *   SSL_write(ssl, buf, len)   ← User Buffer → 암호화 → Kernel (복사 2)
 *   총: syscall 2회, 메모리 복사 2~3회
 *
 * kTLS sendfile 경로:
 *   sendfile(sock_fd, file_fd, ...)
 *   → Page Cache → scatterlist 참조 → 암호화 → TCP
 *   총: syscall 1회, 유저스페이스 복사 0회
 *
 * 대용량 정적 파일에서 CPU 사용량과 메모리 대역폭 절감이 가장 큼 */
splice와 sendfile의 관계: Linux에서 sendfile()은 내부적으로 do_splice_direct()를 호출합니다. 따라서 kTLS에서 sendfile과 splice는 본질적으로 같은 경로를 탑니다. splice()를 직접 사용하는 경우에도 동일한 tls_sw_sendpage() 콜백이 호출됩니다.
페이지 핀닝 주의: sendfile 경로에서 페이지 캐시 페이지는 전송이 완료될 때까지 get_page()로 참조 카운트(Reference Count)가 증가된 상태입니다. 전송 중에 원본 파일이 수정되면 (TLS_TX_ZEROCOPY_RO 미사용 시) 커널이 페이지를 복사하여 COW(Copy-On-Write)를 수행합니다. HW offload + TLS_TX_ZEROCOPY_RO 모드에서는 이 COW가 생략되므로 전송 중 파일 변경 시 TLS 무결성 오류가 발생할 수 있습니다.

HW TLS 오프로드 — NIC Crypto 엔진 상세

kTLS HW 오프로드는 NIC의 암호화 엔진이 TLS 레코드의 암복호화를 수행하여 호스트 CPU 부하를 크게 줄이는 기술입니다. 이 절에서는 NIC 내부에서 TLS 오프로드가 실제로 어떻게 동작하는지, 대표적인 Mellanox(mlx5)와 Intel(ice) 드라이버의 구현을 분석합니다.

kTLS HW Offload 아키텍처 Host (커널) Application sendmsg() / sendfile() kTLS (TLS_HW) 레코드 메타 생성만 TCP 스택 평문 세그먼트 생성 NIC 드라이버 TX 디스크립터 설정 SW Fallback 경로 재전송, 순서 변경 시 — tls_device_fallback_enc() CPU에서 해당 레코드만 암호화 후 NIC에 전달 NIC Hardware TX Pipeline TLS 컨텍스트 검색 AES-GCM Engine 하드웨어 암호화 TLS 헤더+태그 삽입 레코드 프레이밍 Wire 암호화된 TLS 레코드 수신 패킷 암호화된 TLS 레코드 AES-GCM Decrypt MAC 검증 + 복호화 skb->decrypted = 1 커널에 평문으로 전달 Host 수신 경로 SW 복호화 스킵 DMA RX 패킷 유실/재정렬 시: NIC가 암호문을 호스트에 전달 → SW fallback → resync
/* ━━━ Mellanox mlx5 kTLS TX offload 구현 ━━━ */
/* drivers/net/ethernet/mellanox/mlx5/core/en_accel/ktls_tx.c */

static int mlx5e_ktls_add_tx(struct net_device *netdev,
                              struct sock *sk,
                              struct tls_crypto_info *crypto_info,
                              u32 start_offload_tcp_sn)
{
    struct mlx5e_ktls_offload_context_tx *priv_tx;

    /* 1. NIC 펌웨어에 TLS 연결 컨텍스트 생성
     *    - 암호 알고리즘 (AES-GCM-128/256)
     *    - 대칭키, IV, salt, 시퀀스 번호
     *    - TCP 시작 시퀀스 번호 (레코드↔TCP 매핑) */
    mlx5e_ktls_create_tis(priv_tx);

    /* 2. Flow Steering 규칙 설치
     *    5-tuple → TLS TX 컨텍스트 매핑
     *    NIC가 해당 연결의 패킷을 식별하여 암호화 */
    mlx5e_ktls_add_flow(priv_tx, sk);

    /* 3. 이후 해당 소켓의 TX 패킷은:
     *    CPU: 평문 → TCP 세그먼트 생성 → DMA
     *    NIC: TLS 헤더 삽입 + AES-GCM 암호화 + 태그 추가
     *         → 와이어에 완성된 TLS 레코드 전송 */
    return 0;
}

/* ━━━ Intel ice kTLS 구현 ━━━ */
/* drivers/net/ethernet/intel/ice/ice_tls.c */

static int ice_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)
{
    /* Intel E810/E830 시리즈 NIC의 inline crypto 엔진 활용
     * - QAT(QuickAssist) 기반 AES-GCM 가속
     * - TX/RX 양방향 지원 (드라이버/펌웨어 버전 의존)
     * - 지원 암호: AES-128-GCM, AES-256-GCM */
    return ice_tls_setup_conn(pf, sk, crypto_info, direction);
}

/* ━━━ TX 재전송 시 SW fallback ━━━ */
/* net/tls/tls_device_fallback.c */

struct sk_buff *tls_device_fallback_enc(
    struct tls_offload_context_tx *ctx,
    struct sock *sk,
    struct sk_buff *skb)
{
    /* TCP 재전송 시 NIC가 원래 레코드 경계를 모르므로
     * CPU에서 해당 세그먼트를 소프트웨어로 암호화
     *
     * 흐름:
     * 1. 재전송 skb의 TCP seq로 TLS record seq 계산
     * 2. CPU에서 AES-GCM 암호화 수행
     * 3. 암호화된 skb를 NIC에 전달 (bypass offload)
     *
     * 이 fallback은 재전송이 빈번한 lossy 네트워크에서
     * CPU 오버헤드가 증가하는 원인 */
    return tls_enc_skb(tls_ctx, record, skb, ...);
}
NIC별 지원 상태:
  • Mellanox ConnectX-6 Dx+ — TX/RX 모두 지원, AES-GCM-128/256, mlx5_core 드라이버
  • Intel E810 (ice) — TX 위주 지원, AES-GCM, 펌웨어 버전에 따라 RX 지원 범위 상이
  • Chelsio T6 (cxgb4) — 초기 kTLS offload 구현 NIC 중 하나, TX 위주
  • Broadcom/Netxtreme (bnxt) — 일부 모델에서 TX offload 지원
실제 지원 여부는 NIC 모델, 펌웨어 버전, 커널 버전의 조합에 따라 달라지므로 ethtool -k로 반드시 확인해야 합니다.

kTLS 핸드셰이크 연동 — 유저스페이스 TLS 라이브러리

kTLS 자체는 TLS 핸드셰이크를 수행하지 않습니다. 핸드셰이크는 유저스페이스 TLS 라이브러리(OpenSSL, GnuTLS 등)가 전적으로 담당하며, 핸드셰이크가 끝난 후 협상된 키 재료(key material)를 커널에 설치하는 과정이 kTLS 활성화의 핵심입니다. 이 절에서는 OpenSSL과 GnuTLS에서 kTLS를 활성화하는 구체적인 setsockopt() 시퀀스를 분석합니다.

TLS 핸드셰이크 → kTLS 키 설치 시퀀스 Application TLS Library Kernel (kTLS) socket() + connect() setsockopt(SOL_TCP, TCP_ULP, "tls") tls_init() → TLS_BASE TLS Handshake ClientHello/ServerHello 인증서 검증, 키 교환 SSL_do_handshake() 키 추출 key, IV, salt, rec_seq setsockopt(SOL_TLS, TLS_TX, crypto_info) TX: TLS_SW 또는 TLS_HW setsockopt(SOL_TLS, TLS_RX, crypto_info) RX: TLS_SW 또는 TLS_HW send() / sendfile() kTLS 데이터 경로 활성 — 커널이 암복호화 수행 kTLS 활성 확인 BIO_get_ktls_send() == 1
/* ━━━ OpenSSL kTLS 활성화 전체 시퀀스 ━━━ */

#include <openssl/ssl.h>

/* 1. SSL 컨텍스트 설정 */
SSL_CTX *ctx = SSL_CTX_new(TLS_server_method());

/* 2. kTLS 사용 요청 */
SSL_CTX_set_options(ctx, SSL_OP_ENABLE_KTLS);

/* 3. (선택) device offload zerocopy 요청 */
SSL_CTX_set_options(ctx, SSL_OP_ENABLE_KTLS_TX_ZEROCOPY_SENDFILE);

/* 4. 인증서/키 로드 */
SSL_CTX_use_certificate_file(ctx, "cert.pem", SSL_FILETYPE_PEM);
SSL_CTX_use_PrivateKey_file(ctx, "key.pem", SSL_FILETYPE_PEM);

/* 5. 소켓 생성 및 연결 */
int sockfd = accept(listen_fd, ...);
SSL *ssl = SSL_new(ctx);
SSL_set_fd(ssl, sockfd);

/* 6. 핸드셰이크 수행 — 이 안에서 자동으로:
 *    a) setsockopt(SOL_TCP, TCP_ULP, "tls")
 *    b) 핸드셰이크 완료 후 키 추출
 *    c) setsockopt(SOL_TLS, TLS_TX, &crypto_info)
 *    d) setsockopt(SOL_TLS, TLS_RX, &crypto_info) */
SSL_accept(ssl);

/* 7. kTLS 활성화 확인 */
if (BIO_get_ktls_send(SSL_get_wbio(ssl))) {
    printf("kTLS TX 활성화됨\n");
    /* SSL_sendfile() 사용 가능 */
    SSL_sendfile(ssl, filefd, 0, file_size, 0);
}
if (BIO_get_ktls_recv(SSL_get_rbio(ssl))) {
    printf("kTLS RX 활성화됨\n");
}

/* ━━━ GnuTLS kTLS 활성화 ━━━ */

#include <gnutls/gnutls.h>

gnutls_session_t session;
gnutls_init(&session, GNUTLS_SERVER);

/* kTLS 활성화 플래그 */
gnutls_transport_set_int(session, sockfd);
gnutls_handshake_set_timeout(session, GNUTLS_DEFAULT_HANDSHAKE_TIMEOUT);

/* 핸드셰이크 수행 */
gnutls_handshake(session);

/* GnuTLS에서 kTLS sendfile */
gnutls_record_send_file(session, filefd, &offset, file_size);
/* 내부적으로 setsockopt(SOL_TLS) 호출하여 키 설치
 * 주의: GnuTLS의 kTLS rekey 지원은 제한적일 수 있음 */
핸드셰이크와 kTLS의 분리: kTLS는 핸드셰이크를 전혀 관여하지 않으므로, 인증서 검증 실패, 프로토콜 다운그레이드, ALPN 협상 등의 보안 결정은 라이브러리 설정에 전적으로 의존합니다. kTLS를 활성화했다고 해서 보안 수준이 달라지는 것이 아니라, 이미 결정된 보안 수준의 데이터 경로만 최적화하는 것입니다.

TLS 레코드 계층 — 암호화/복호화 상세

kTLS가 커널에서 처리하는 핵심은 TLS 레코드 계층(Record Layer)입니다. 모든 TLS 데이터는 레코드 단위로 구분되며, 각 레코드는 헤더(5바이트) + 암호화된 페이로드(Payload) + 인증 태그로 구성됩니다. 이 절에서는 TLS 1.2와 TLS 1.3의 레코드 구조 차이와, kTLS가 레코드를 생성하고 파싱하는 메커니즘을 상세히 설명합니다.

TLS 레코드 계층 구조 비교 TLS 1.2 레코드 구조 Content Type (1B) Version (2B) Length (2B) Explicit IV (Nonce) (8B) Encrypted Payload (max 16384B = 16KB) AEAD Auth Tag (16B) Header (5B, 평문) 암호화 영역 TLS 1.3 레코드 구조 항상 23 (APP_DATA) (1B) 0x0303 (위장) (2B) Length (2B) Encrypted Payload (max 16384B) Inner CT + Padding (1+NB) AEAD Auth Tag (16B) Header (5B, 평문) 전체 암호화 영역 (CT = Content Type) Content Type 값 20 — ChangeCipherSpec 21 — Alert (경고/오류) 22 — Handshake (핸드셰이크) 23 — ApplicationData (데이터) 24 — Heartbeat (TLS 1.2, 거의 사용 안 함) TLS 1.3: 외부 헤더는 항상 23, 실제 타입은 내부 AEAD 입력 (AES-GCM) TLS 1.2: Nonce = salt(4B) || explicit_iv(8B) AAD = seq_num(8B) || header(5B) = 13B TLS 1.3: Nonce = pad(salt,12B) XOR pad(seq,12B) AAD = header(5B) = 5B
/* ━━━ kTLS 레코드 생성 상세 ━━━ */

/* net/tls/tls_sw.c — TLS 레코드 헤더 채우기 */
static void tls_fill_prepend(struct tls_context *ctx,
                            char *buf,
                            size_t plaintext_len,
                            unsigned char record_type)
{
    struct tls_prot_info *prot = &ctx->prot_info;
    size_t ciphertext_len;

    /* TLS 레코드 헤더 생성 (5바이트) */
    if (prot->version == TLS_1_3_VERSION) {
        /* TLS 1.3: 외부 content type은 항상 APPLICATION_DATA */
        buf[0] = TLS_RECORD_TYPE_DATA;  /* 23 */

        /* 암호문 길이 = 평문 + inner_ct(1B) + tag(16B) */
        ciphertext_len = plaintext_len + prot->tail_size
                         + prot->tag_size;
    } else {
        /* TLS 1.2: 실제 content type 기록 */
        buf[0] = record_type;

        /* 암호문 길이 = explicit_iv(8B) + 평문 + tag(16B) */
        ciphertext_len = prot->iv_size + plaintext_len
                         + prot->tag_size;
    }

    /* version (2B) — TLS 1.3도 호환성을 위해 0x0303 기록 */
    buf[1] = 0x03;
    buf[2] = 0x03;

    /* length (2B, big-endian) */
    buf[3] = ciphertext_len >> 8;
    buf[4] = ciphertext_len & 0xff;
}

/* AEAD nonce 생성 */
static void tls_make_aead_nonce(char *nonce,
                               struct tls_prot_info *prot,
                               char *iv, char *rec_seq)
{
    if (prot->version == TLS_1_3_VERSION) {
        /* TLS 1.3: nonce = pad(iv,12) XOR pad(seq,12)
         * explicit IV가 전송되지 않아 레코드당 8바이트 절약 */
        memcpy(nonce, iv, prot->iv_size);
        for (int i = 0; i < 8; i++)
            nonce[prot->iv_size - 1 - i] ^= rec_seq[7 - i];
    } else {
        /* TLS 1.2: nonce = salt(4B) || explicit_iv(8B) */
        memcpy(nonce, prot->salt, prot->salt_size);
        memcpy(nonce + prot->salt_size, iv, prot->iv_size);
    }
}

/* 레코드 최대 크기:
 * - 평문 최대: 16384 바이트 (2^14, TLS 표준)
 * - TLS_TX_MAX_PAYLOAD_LEN으로 조정 가능 (RFC 8449)
 * - 작은 레코드: 지연 시간 감소, 인터랙티브 응답에 유리
 * - 큰 레코드: 오버헤드 비율 감소, 대용량 전송에 유리 */
레코드 크기 최적화: TLS_TX_MAX_PAYLOAD_LEN (커널 옵션 5)을 사용하면 레코드 크기를 줄여 인터랙티브 응답의 지연 시간(Time-to-First-Byte)을 개선할 수 있습니다. 반대로, 대용량 파일 전송에서는 기본 16KB 레코드가 가장 효율적입니다. RFC 8449 (record_size_limit)를 참조하세요.

수신 경로 상세 — tls_sw_recvmsg()와 비동기 복호화

kTLS의 RX 경로는 TX보다 복잡합니다. TCP는 바이트 스트림이므로 TLS 레코드 경계와 TCP 세그먼트 경계가 일치하지 않을 수 있으며, strparser가 이 문제를 해결합니다. 또한 비동기 복호화(async crypto)를 지원하여, 복호화 완료를 기다리는 동안 다른 레코드의 수신을 계속 처리할 수 있습니다.

kTLS RX 수신 경로 상세 NIC 수신 패킷 TCP 수신 큐 바이트 스트림 strparser TLS 레코드 경계 감지 tls_strp_msg_ready() 완전한 레코드 전달 rx_list에 추가 (큐잉) recv() / recvmsg() 호출 시 userspace recv() tls_sw_recvmsg() 진입 rx_list에서 dequeue 완전한 TLS 레코드 content type 확인 및 분기 (23? 21? 22?) DATA decrypt_skb() AEAD 복호화 유저 버퍼 복사 ALERT/HS CMSG로 전달 또는 에러 반환 비동기(Async) 복호화 모드 crypto_aead_decrypt() -EINPROGRESS 반환 다음 레코드 처리 계속 decrypt_pending++ 완료 콜백 (IRQ/softirq) decrypt_pending-- 복호화 완료 레코드 rx_list에 추가
/* ━━━ tls_sw_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 = ...;
    struct sk_buff *skb;
    int err, copied = 0;

    lock_sock(sk);

    do {
        /* 1. 이미 복호화된 레코드가 있으면 바로 사용 */
        skb = skb_peek(&ctx->rx_list);
        if (skb) {
            goto copy_data;
        }

        /* 2. strparser가 조립한 레코드 가져오기 */
        skb = ctx->recv_pkt;
        if (!skb) {
            /* 데이터 대기: sk_wait_data() */
            sk_wait_data(sk, &timeo, ...);
            continue;
        }

        /* 3. TLS 레코드 타입 확인 */
        u8 content_type = tls_record_content_type(skb);

        if (content_type == TLS_RECORD_TYPE_DATA) {
            /* 4a. 데이터 레코드 → 복호화 */
            err = decrypt_skb(sk, skb, msg);

            if (err == -EINPROGRESS) {
                /* 비동기 복호화: 완료 콜백을 기다림
                 * HW 가속기(QAT 등)가 복호화를 수행하는 경우
                 * → atomic_inc(&ctx->decrypt_pending)
                 * → 콜백에서 atomic_dec_and_test()로 완료 통지 */
                tls_decrypt_async_wait(ctx);
            }
        } else if (content_type == TLS_RECORD_TYPE_ALERT) {
            /* 4b. Alert → CMSG로 유저에게 전달 */
            tls_alert_recv(sk, msg, skb);
        } else if (content_type == TLS_RECORD_TYPE_HANDSHAKE) {
            /* 4c. Handshake (KeyUpdate 등) → 유저스페이스에 전달 */
            /* TLS 1.3 KeyUpdate 수신 시:
             *   ctx->key_update_pending = true
             *   이후 recv()는 -EKEYEXPIRED 반환
             *   새 TLS_RX 키가 설치될 때까지 */
        }

copy_data:
        /* 5. 복호화된 평문을 유저 버퍼에 복사 */
        err = skb_copy_datagram_msg(skb, rxm->offset, msg, chunk);
        copied += chunk;

    } while (copied < len);

    release_sock(sk);
    return copied;
}

/* Zero-copy RX (TLS_RX_EXPECT_NO_PAD):
 * TLS 1.3에서 패딩이 없다고 가정할 수 있으면
 * 유저 버퍼에 직접 복호화하여 중간 버퍼 복사를 제거
 *
 * 주의: 악의적인 peer가 패딩을 추가하면
 * 복호화 결과가 유저 버퍼를 초과하여 재복호화 필요
 * → 커널 문서가 경고하는 비용 증가 공격 벡터 */
strparser와 레코드 경계: TCP 세그먼트와 TLS 레코드는 1:1 대응하지 않습니다. 하나의 TCP 세그먼트에 여러 TLS 레코드가 포함될 수 있고, 반대로 하나의 TLS 레코드가 여러 TCP 세그먼트에 걸칠 수도 있습니다. strparser는 TLS 레코드 헤더의 Length 필드를 파싱하여 이 불일치를 해결합니다. 이 조립 과정이 있기 때문에 tls_sw_recvmsg()에서 즉시 복호화할 레코드가 없으면 데이터 대기(sk_wait_data)에 들어갑니다.

nginx/HAProxy kTLS 통합

nginx와 HAProxy는 kTLS를 지원하는 대표적인 웹서버/프록시입니다. 특히 정적 파일 서빙에서 SSL_sendfile()을 활용하면 sendfile 제로카피 경로를 통해 TLS 연결에서도 높은 처리량(Throughput)을 달성할 수 있습니다.

nginx/HAProxy kTLS 데이터 경로 nginx Worker Process (User Space) HTTP 요청 처리 파일 경로 결정 정적 파일 경로 ssl_sendfile on → SSL_sendfile() OpenSSL kTLS BIO_get_ktls_send() == 1 동적 응답 경로 SSL_write() — 일반 경로 Kernel Page Cache (정적 파일) kTLS sendpage() 제로카피 암호화 TCP NIC 결과 복사 0, syscall 1 sendfile() nginx kTLS 설정 체크리스트 1. OpenSSL 3.0+ (enable-ktls) 2. 커널 CONFIG_TLS=y/m 3. nginx: ssl_conf_command Options KTLS 4. sendfile on; (nginx.conf) 5. modprobe tls 6. strace로 SOL_TLS 확인
# ━━━ nginx kTLS 설정 ━━━

# nginx.conf — kTLS 활성화
http {
    # sendfile 활성화 (kTLS sendfile의 전제 조건)
    sendfile on;

    server {
        listen 443 ssl;
        server_name example.com;

        ssl_certificate     /etc/ssl/cert.pem;
        ssl_certificate_key /etc/ssl/key.pem;

        # OpenSSL kTLS 활성화 (nginx 1.21.4+, OpenSSL 3.0+)
        ssl_conf_command Options KTLS;

        # TLS 1.3 권장 (kTLS와 가장 잘 맞음)
        ssl_protocols TLSv1.2 TLSv1.3;
        ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256;

        location / {
            root /var/www/html;
            # 정적 파일 → sendfile → kTLS sendpage → 제로카피
        }
    }
}

# kTLS 활성화 확인
strace -f -p $(pgrep -f 'nginx: worker') -e setsockopt 2>&1 | grep -E "TCP_ULP|SOL_TLS"

# 커널 통계로 확인
watch -n 1 cat /proc/net/tls_stat

# ━━━ HAProxy kTLS 설정 ━━━

# haproxy.cfg — kTLS 활성화 (HAProxy 2.5+, OpenSSL 3.0+)
global
    # OpenSSL에서 kTLS 활성화
    ssl-default-bind-options ssl-min-ver TLSv1.2
    ssl-engine ktls

frontend https_in
    bind *:443 ssl crt /etc/ssl/haproxy.pem
    default_backend servers

# HAProxy 참고: ssl-engine ktls 설정은 버전에 따라 다를 수 있음
# HAProxy의 kTLS 지원은 주로 TX 방향에서 효과적

# ━━━ kTLS + nginx 벤치마크 예시 ━━━

# wrk로 정적 파일 처리량 비교
wrk -t4 -c100 -d30s https://localhost/1MB.bin
# kTLS OFF: Requests/sec: ~12,000, CPU: ~85%
# kTLS ON:  Requests/sec: ~15,000, CPU: ~65%
# (수치는 환경에 따라 크게 달라짐 — 경향성 참고용)
nginx ssl_sendfile 지시문: nginx 1.25.4+에서는 ssl_sendfile on; 지시문이 추가되어 kTLS sendfile을 명시적으로 제어할 수 있습니다. 이전 버전에서는 ssl_conf_command Options KTLSsendfile on;의 조합으로 활성화합니다.

kTLS 성능 벤치마크 — Userspace vs SW kTLS vs HW Offload

kTLS의 성능 이점은 워크로드 유형에 따라 크게 달라집니다. 아래 비교는 일반적인 경향성을 정리한 것이며, 실제 수치는 하드웨어, 커널 버전, 암호 스위트, 파일 크기, 동시 연결 수 등에 따라 달라집니다.

지표유저스페이스 TLSSW kTLSHW kTLS Offload
sendfile 지원 불가 — read() + SSL_write() 가능 — 커널 내 제로카피 가능 — NIC가 암호화
정적 파일 CPU 사용 기준 (100%) ~70% (복사 제거) ~30% (암호화도 오프로드)
Syscall 횟수 (정적 파일) 2 (read + write) 1 (sendfile) 1 (sendfile)
메모리 복사 횟수 2~3회 0~1회 0회 (DMA 직접)
동적 응답 (API) 기준 개선 제한적 개선 제한적
짧은 메시지 오버헤드 기준 레코드 경계 오버헤드 유사 NIC 컨텍스트 전환 비용 추가
대역폭 (100Gbps NIC) CPU 병목 ~40Gbps CPU 병목 ~60Gbps 라인레이트 ~100Gbps 가능
최대 동시 오프로드 연결 무제한 무제한 NIC 리소스 제한 (수천~수만)
디버깅 난이도 쉬움 (유저스페이스) 보통 (/proc/net/tls_stat) 어려움 (NIC 내부 상태)
벤치마크 주의: 위 수치는 경향성을 보여주기 위한 참고 자료입니다. 실제 성능은 CPU 모델(AES-NI 지원 여부), 메모리 대역폭, NIC 종류, 커널 버전, 암호 스위트(AES-128-GCM vs AES-256-GCM vs ChaCha20), 파일 크기 분포, 동시 연결 수에 따라 크게 달라집니다. 운영 환경에서는 반드시 자체 벤치마크를 수행하세요.
# ━━━ kTLS 성능 비교 벤치마크 스크립트 ━━━

# 1. OpenSSL s_server로 kTLS 성능 테스트

# kTLS OFF
openssl s_server -accept 8443 -cert cert.pem -key key.pem -WWW
openssl s_time -connect 127.0.0.1:8443 -www /1MB.bin -time 10

# kTLS ON
openssl s_server -accept 8443 -cert cert.pem -key key.pem -WWW -ktls -sendfile
openssl s_time -connect 127.0.0.1:8443 -www /1MB.bin -time 10

# 2. CPU 사용률 비교
pidstat -p $(pgrep -f s_server) 1

# 3. perf로 프로파일링
perf stat -e cycles,instructions,cache-misses -p $(pgrep -f nginx) -- sleep 10

# 4. kTLS 통계 변화 모니터링
watch -d -n 0.5 'cat /proc/net/tls_stat'

# 5. 메모리 대역폭 비교 (perf mem)
perf mem record -p $(pgrep -f nginx) -- sleep 5
perf mem report
가장 큰 이득을 보는 케이스:
  • 대용량 정적 파일 서빙 (이미지, 비디오, 다운로드)
  • CDN/프록시에서 캐시(Cache)된 콘텐츠 전달
  • 100Gbps 이상 NIC에서 HW offload와 함께 사용
  • CPU 바운드 TLS 워크로드에서 CPU 여유 확보

ftrace/bpftrace kTLS 추적 — 암호화 지연시간 분석

kTLS의 성능 문제를 진단할 때 ftracebpftrace는 가장 강력한 도구입니다. 커널 내부의 암호화/복호화 지연, 레코드 처리 경로, 비동기 완료 패턴을 실시간(Real-time)으로 추적할 수 있습니다.

kTLS 추적 포인트 (kprobe/bpftrace) TX 경로 추적 tls_sw_sendmsg tls_push_data tls_do_encryption tls_push_record tcp_sendmsg RX 경로 추적 tls_sw_recvmsg tls_strp_msg_ready decrypt_skb skb_copy_datagram_msg 핵심 측정 포인트 tls_do_encryption 지연 TX 암호화 시간 (us) decrypt_skb 지연 RX 복호화 시간 (us) 레코드/초 처리량 지표
# ━━━ bpftrace kTLS 추적 스크립트 모음 ━━━

# 1. TX 암호화 지연시간 히스토그램
bpftrace -e '
kprobe:tls_do_encryption {
    @start[tid] = nsecs;
}
kretprobe:tls_do_encryption /@start[tid]/ {
    @encrypt_us = hist((nsecs - @start[tid]) / 1000);
    delete(@start[tid]);
}
interval:s:10 { exit(); }
'

# 2. RX 복호화 지연시간 히스토그램
bpftrace -e '
kprobe:decrypt_skb {
    @start[tid] = nsecs;
}
kretprobe:decrypt_skb /@start[tid]/ {
    @decrypt_us = hist((nsecs - @start[tid]) / 1000);
    delete(@start[tid]);
}
interval:s:10 { exit(); }
'

# 3. kTLS sendmsg/recvmsg 호출 빈도 및 크기
bpftrace -e '
kprobe:tls_sw_sendmsg {
    @tx_bytes = hist(arg2);
    @tx_count = count();
}
kprobe:tls_sw_recvmsg {
    @rx_bytes = hist(arg2);
    @rx_count = count();
}
interval:s:5 { print(@tx_count); print(@rx_count); }
'

# 4. sendfile vs sendmsg 경로 비교
bpftrace -e '
kprobe:tls_sw_sendpage {
    @sendpage = count();
    @sendpage_comm[comm] = count();
}
kprobe:tls_sw_sendmsg {
    @sendmsg = count();
    @sendmsg_comm[comm] = count();
}
interval:s:5 {
    printf("sendpage(sendfile): %d, sendmsg: %d\n",
           @sendpage, @sendmsg);
}
'

# 5. TLS 레코드 타입별 통계
bpftrace -e '
kprobe:tls_sw_recvmsg {
    @recv_pid[pid, comm] = count();
}
'

# 6. ftrace로 kTLS 함수 호출 체인 추적
echo 'tls_sw_sendmsg' > /sys/kernel/debug/tracing/set_graph_function
echo function_graph > /sys/kernel/debug/tracing/current_tracer
echo 1 > /sys/kernel/debug/tracing/tracing_on
# 부하 발생 후
cat /sys/kernel/debug/tracing/trace
echo 0 > /sys/kernel/debug/tracing/tracing_on

# 7. 비동기 복호화 대기 시간 측정
bpftrace -e '
kprobe:tls_decrypt_async_wait {
    @async_start[tid] = nsecs;
}
kretprobe:tls_decrypt_async_wait /@async_start[tid]/ {
    @async_wait_us = hist((nsecs - @async_start[tid]) / 1000);
    delete(@async_start[tid]);
}
'
perf와 조합: bpftrace로 kTLS 경로를 특정한 후 perf record로 상세 프로파일링(Profiling)을 하면 암호화 함수 내부의 CPU 사이클 분포까지 확인할 수 있습니다. perf stat -e cache-misses로 메모리 접근 패턴도 함께 보면, sendfile 경로의 캐시 효율을 정량화할 수 있습니다.

kTLS 문제 해결 — 커널 버전 호환성과 Cipher Suite 지원

kTLS 도입 시 가장 흔한 문제는 커널 버전 미지원, TLS 모듈 미로드, cipher suite 불일치, 그리고 라이브러리의 kTLS 지원 부족입니다. 이 절에서는 문제 상황별 체계적인 진단 경로를 제시합니다.

kTLS 문제 해결 의사결정 트리 kTLS가 작동하지 않음 lsmod | grep tls ? NO modprobe tls CONFIG_TLS 확인 YES OpenSSL KTLS 빌드 확인? NO OpenSSL 3.0+ enable-ktls 재빌드 YES BIO_get_ktls_send() == 1? NO 원인 확인: - SSL_OP_ENABLE_KTLS? - cipher suite 지원? YES HW offload 사용 여부? SW만 SW kTLS 정상 tls_stat 모니터링 HW ethtool -k | grep tls? ON → 정상 OFF/미지원 NIC/FW 업그레이드 EKEYEXPIRED 오류? TLS 1.3 KeyUpdate 수신 → 라이브러리 rekey 지원 확인
Cipher Suite커널 지원 버전상수비고
AES-128-GCM 4.13+ (TX), 4.17+ (RX) TLS_CIPHER_AES_GCM_128 (51) 가장 널리 사용, HW offload 지원 가장 넓음
AES-256-GCM 4.13+ (TX), 4.17+ (RX) TLS_CIPHER_AES_GCM_256 (52) 높은 보안 요구 환경, 일부 HW offload 지원
AES-128-CCM 5.1+ TLS_CIPHER_AES_CCM_128 (53) IoT/임베디드 환경
ChaCha20-Poly1305 5.0+ TLS_CIPHER_CHACHA20_POLY1305 (54) AES-NI 없는 환경에서 유리, 모바일 클라이언트
SM4-GCM 6.0+ TLS_CIPHER_SM4_GCM (55) 중국 국가 표준 암호
SM4-CCM 6.0+ TLS_CIPHER_SM4_CCM (56) 중국 국가 표준 암호
ARIA-128-GCM 6.2+ TLS_CIPHER_ARIA_GCM_128 (57) 한국 표준 암호
ARIA-256-GCM 6.2+ TLS_CIPHER_ARIA_GCM_256 (58) 한국 표준 암호
문제 증상원인해결 방법
ENOPROTOOPT on setsockopt tls 모듈 미로드 modprobe tls 또는 CONFIG_TLS=y 커널 재빌드
BIO_get_ktls_send() == 0 OpenSSL kTLS 미활성화 SSL_OP_ENABLE_KTLS 설정, OpenSSL enable-ktls 빌드 확인
EKEYEXPIRED on recv() TLS 1.3 KeyUpdate 수신 라이브러리가 새 RX 키를 TLS_RX로 설치해야 함
TlsCurrTxDevice = 0 HW offload 미지원 또는 미활성화 ethtool -K eth0 tls-hw-tx-offload on, NIC/FW 확인
TlsDecryptError 증가 MAC 검증 실패 키 불일치, 패킷 손상, 중간자 공격 의심
TlsDeviceRxResync 빈번 패킷 유실/재정렬 빈번 네트워크 품질 확인, SW fallback 빈도 모니터링
sendfile()이 SSL_write로 fallback kTLS TX 미설치 straceSOL_TLS setsockopt 확인
성능 개선 미미 동적 응답 위주 워크로드 kTLS 이점은 정적 파일/sendfile에 집중 — 워크로드 특성 확인
커널 패닉(Kernel Panic)/oops kTLS 커널 버그 (드물지만 존재) 커널 업그레이드, dmesg 로그 확인, 버그 리포트
# ━━━ 체계적 kTLS 문제 진단 스크립트 ━━━

#!/bin/bash
echo "=== kTLS 진단 시작 ==="

# 1. 커널 모듈 확인
echo "--- 커널 모듈 ---"
lsmod | grep tls || echo "WARNING: tls 모듈 미로드 → modprobe tls"
cat /proc/config.gz 2>/dev/null | zcat | grep CONFIG_TLS || \
    grep CONFIG_TLS /boot/config-$(uname -r) 2>/dev/null || \
    echo "커널 설정 파일 확인 불가"

# 2. OpenSSL 버전 및 kTLS 지원
echo "--- OpenSSL ---"
openssl version
openssl version -a 2>&1 | grep -i ktls && echo "kTLS 빌드 포함" || echo "WARNING: kTLS 미포함"

# 3. 커널 통계
echo "--- kTLS 통계 ---"
cat /proc/net/tls_stat 2>/dev/null || echo "WARNING: /proc/net/tls_stat 없음"

# 4. NIC offload 상태
echo "--- NIC TLS offload ---"
for iface in $(ls /sys/class/net/ | grep -v lo); do
    echo "$iface:"
    ethtool -k $iface 2>/dev/null | grep tls || echo "  tls offload 미지원"
done

# 5. 커널 버전 호환성
echo "--- 커널 버전 ---"
uname -r
echo "kTLS TX: 4.13+, RX: 4.17+, TLS 1.3: 5.1+, KeyUpdate: 6.0+"

echo "=== 진단 완료 ==="
커널 버전별 주요 변경사항:
  • 4.13: kTLS TX 최초 도입 (AES-GCM-128)
  • 4.17: kTLS RX 추가
  • 5.0: ChaCha20-Poly1305 지원
  • 5.1: TLS 1.3 지원, AES-CCM-128
  • 5.2: HW RX offload, resync 메커니즘
  • 5.7: TLS_TX_ZEROCOPY_RO
  • 5.11: TLS_RX_EXPECT_NO_PAD
  • 5.19: TLS_TX_MAX_PAYLOAD_LEN (RFC 8449)
  • 6.0: SM4-GCM/CCM, KeyUpdate RX 정지 메커니즘 개선
  • 6.2: ARIA-GCM-128/256
  • 6.7: TLS Handshake Upcall (CONFIG_TLS_HANDSHAKE), tlshd 연동
  • 6.8: Handshake timeout 개선, PSK identity hint

TLS Handshake Upcall — In-Kernel TLS 핸드셰이크 (6.7+)

kTLS는 본래 유저스페이스에서 TLS 핸드셰이크를 완료한 뒤 setsockopt(SOL_TLS, TLS_TX/TLS_RX)로 세션 키를 커널에 넘기는 구조입니다. 그러나 커널 내부 프로토콜이 직접 TLS를 필요로 하는 사례가 늘었습니다.

이런 커널 서비스들은 유저스페이스 소켓 API를 쓸 수 없으므로, 커널 6.7에서 TLS Handshake Upcall 메커니즘이 도입되었습니다. 커널 모듈(Kernel Module)이 tls_client_hello() 또는 tls_server_hello()를 호출하면, Netlink generic family(HANDSHAKE_GENL_NAME)를 통해 유저스페이스의 tlshd 데몬에게 핸드셰이크를 위임합니다. tlshd가 OpenSSL/GnuTLS로 핸드셰이크를 완료한 뒤 setsockopt(SOL_TLS)를 호출하면, 이후 데이터 경로는 기존 kTLS 오프로드 그대로 동작합니다.

CONFIG_TLS_HANDSHAKE: 이 기능을 사용하려면 커널 설정에서 CONFIG_TLS_HANDSHAKE=y가 필요합니다. CONFIG_TLS와 별도 옵션이며, Netlink generic 인프라(CONFIG_GENERIC_NET_SYSCTLS)도 함께 활성화해야 합니다.

핵심 자료구조: struct tls_handshake_args

/* include/net/handshake.h (커널 6.7+) */
struct tls_handshake_args {
    struct socket       *ta_sock;       /* 대상 소켓 */
    tls_done_func_t      ta_done;       /* 완료 콜백 */
    void                *ta_data;       /* 콜백 인자 */
    const char          *ta_peername;   /* SNI 호스트명 */
    unsigned int         ta_timeout_ms;  /* 핸드셰이크 타임아웃 */
    key_serial_t         ta_keyring;     /* 인증서/PSK 키링 */
    key_serial_t         ta_my_cert;     /* 클라이언트 인증서 키 */
    key_serial_t         ta_my_privkey;  /* 클라이언트 개인키 */
    unsigned int         ta_num_peernames; /* PSK identity 수 */
};

커널 모듈에서 핸드셰이크 요청

/* 커널 모듈 — TLS 클라이언트 핸드셰이크 요청 예시 */
static void my_tls_done(void *data, int status, key_serial_t peerid)
{
    struct my_context *ctx = data;
    if (status == 0)
        pr_info("TLS handshake succeeded, peer key=%d\n", peerid);
    else
        pr_err("TLS handshake failed: %d\n", status);
    complete(&ctx->hs_done);
}

int start_kernel_tls(struct socket *sock, struct my_context *ctx)
{
    struct tls_handshake_args args = {
        .ta_sock      = sock,
        .ta_done      = my_tls_done,
        .ta_data      = ctx,
        .ta_peername   = "nfs-server.example.com",
        .ta_timeout_ms = 10000,   /* 10초 */
    };

    return tls_client_hello_x509(&args, GFP_KERNEL);
}

Upcall 흐름도

TLS Handshake Upcall 흐름도 커널 서비스가 tlshd에게 핸드셰이크를 위임하고, 완료 후 kTLS 데이터 경로를 사용하는 과정 커널 공간 (Kernel Space) 유저 공간 (User Space) NFS / ksmbd NVMe-oF / SUNRPC TLS Handshake net/handshake/ Netlink kTLS ULP net/tls/ NIC HW TLS offload tlshd ktls-utils 패키지 OpenSSL / GnuTLS TLS 라이브러리 원격 피어 upcall ⑤ setsockopt(SOL_TLS) ① tls_client_hello_x509() ② Netlink genl ③ TLS 라이브러리 호출 ④ 핸드셰이크 ⑤ 키 설치 ⑥ 데이터 경로 (kTLS offload)
핵심 포인트: 핸드셰이크 완료 후의 데이터 경로는 기존 kTLS와 완전히 동일합니다. sendfile(), splice(), HW offload, TLS_TX_ZEROCOPY_RO 등 모든 kTLS 최적화가 그대로 적용됩니다. 유일한 차이는 핸드셰이크를 누가 시작하느냐(유저 앱 vs 커널 모듈)입니다.

X.509 vs PSK 모드

항목X.509 인증서 모드PSK (Pre-Shared Key) 모드
API tls_client_hello_x509() tls_client_hello_psk()
인증 방식 CA 인증서 체인 검증 사전 공유 키 (identity + key)
키 저장소 커널 키링 (keyctl) 커널 키링 (keyctl)
주요 사용처 NFS, ksmbd NVMe-oF/TCP, 사전 설정 환경
tlshd 설정 인증서/키 파일 경로 PSK identity/key 설정

tlshd 데몬 — 유저스페이스 핸드셰이크 에이전트

tlshdktls-utils 패키지에 포함된 데몬으로, 커널의 TLS Handshake Upcall 요청을 받아 실제 TLS 핸드셰이크를 수행합니다. Netlink generic family HANDSHAKE_GENL_NAME("handshake")을 감시하며, 커널로부터 소켓 파일 디스크립터(File Descriptor)를 전달받아 핸드셰이크를 완료합니다.

동작 원리

  1. 커널 모듈이 tls_client_hello_x509() 등을 호출
  2. handshake 서브시스템이 Netlink 메시지를 브로드캐스트
  3. tlshd가 Netlink HANDSHAKE_CMD_ACCEPT로 요청 수락
  4. 커널이 소켓 fd를 tlshd 프로세스(Process)로 전달 (SCM_RIGHTS)
  5. tlshd가 GnuTLS (기본) 또는 OpenSSL로 TLS 핸드셰이크 수행
  6. 핸드셰이크 완료 시 setsockopt(SOL_TLS, TLS_TX) + TLS_RX로 세션 키 설치
  7. Netlink HANDSHAKE_CMD_DONE으로 커널에 결과 통보

설치 및 설정

# ktls-utils 설치 (tlshd 포함)
# Fedora/RHEL
dnf install ktls-utils

# 소스에서 빌드
git clone https://github.com/oracle/ktls-utils.git
cd ktls-utils
./autogen.sh && ./configure --with-gnutls
make && make install

tlshd 설정 파일

# /etc/tlshd.conf
[authenticate]
# X.509 인증서/키 경로
x509.certificate = /etc/tlshd/tls.crt
x509.private_key = /etc/tlshd/tls.key
x509.truststore  = /etc/tlshd/ca-bundle.crt

[service]
# 핸드셰이크 타임아웃 (초)
timeout = 30

systemd 서비스

# /etc/systemd/system/tlshd.service
[Unit]
Description=TLS Handshake Daemon
After=network-online.target
Wants=network-online.target

[Service]
Type=notify
ExecStart=/usr/sbin/tlshd
Restart=on-failure
RestartSec=5

# 보안 강화
ProtectSystem=strict
ProtectHome=true
PrivateTmp=true
NoNewPrivileges=true

[Install]
WantedBy=multi-user.target
# 서비스 활성화 및 시작
systemctl daemon-reload
systemctl enable --now tlshd.service

# 상태 확인
systemctl status tlshd
journalctl -u tlshd -f
tlshd 미실행 시 동작: 커널이 핸드셰이크 upcall을 보냈으나 tlshd가 응답하지 않으면, ta_timeout_ms 이후 ta_done 콜백이 에러 상태(-ETIMEDOUT)로 호출됩니다. 커널 6.8에서는 이 타임아웃 처리가 개선되어 소켓 리소스 누수가 방지됩니다.

RPC-over-TLS — NFS와 kTLS 통합

RFC 9289는 ONC RPC 위에 TLS를 적용하는 표준으로, NFS v4.x 트래픽을 전송 계층에서 암호화합니다. 리눅스에서는 TLS Handshake Upcall + kTLS 조합으로 구현되어, 핸드셰이크 이후의 데이터 경로는 커널에서 직접 처리합니다.

아키텍처

RPC-over-TLS는 기존 NFS TCP 연결 위에 STARTTLS 방식으로 TLS를 활성화합니다. AUTH_TLS NULL RPC 호출로 TLS 업그레이드를 협상한 뒤, 커널의 handshake upcall → tlshd → setsockopt(SOL_TLS) 경로를 따릅니다.

계층구성 요소역할
응용 NFS v4.0 / v4.1 / v4.2 파일 시스템 프로토콜
RPC SUNRPC + AUTH_TLS TLS 업그레이드 협상
보안 TLS 1.3 (tlshd) 핸드셰이크 (유저스페이스)
전송 암호화 kTLS ULP 레코드 암/복호화 (커널)
전송 TCP 바이트 스트림

서버측 설정 (nfsd)

# 1. 커널 모듈 확인
modprobe tls
modprobe nfsd
lsmod | grep -E 'tls|nfsd'

# 2. tlshd 인증서 설정
# /etc/tlshd.conf에 서버 인증서 경로 설정 (위 참조)

# 3. NFS export에 xprtsec 옵션 추가
# /etc/exports
/srv/nfs  *(rw,sync,no_subtree_check,xprtsec=tls)

# xprtsec 옵션:
#   none  — TLS 불필요 (기본)
#   tls   — TLS 필수 (서버 인증서만)
#   mtls  — mutual TLS (클라이언트 인증서도 요구)

# 4. export 적용
exportfs -rav

# 5. tlshd & nfsd 시작
systemctl restart tlshd
systemctl restart nfs-server

클라이언트측 마운트(Mount)

# TLS 마운트
mount -t nfs4 -o vers=4.2,xprtsec=tls server:/srv/nfs /mnt/nfs

# mutual TLS 마운트
mount -t nfs4 -o vers=4.2,xprtsec=mtls server:/srv/nfs /mnt/nfs

# 마운트 확인
mount | grep nfs
# server:/srv/nfs on /mnt/nfs type nfs4 (rw,xprtsec=tls,...)

# RPC-over-TLS 연결 상태 확인
cat /proc/net/rpc/xprt_info
# xprt: tcp ... security: tls

ksmbd (커널 SMB 서버)의 TLS 활용

ksmbd는 커널 내 SMB3 서버로, SMB 3.0 이상의 전송 암호화를 지원합니다. ksmbd도 TLS Handshake Upcall을 사용하여 tlshd에게 핸드셰이크를 위임하며, 데이터 전송은 kTLS를 통해 처리합니다.

# ksmbd TLS 설정 (/etc/ksmbd/ksmbd.conf)
[global]
    server signing = mandatory
    smb encryption = required

# tlshd가 ksmbd의 핸드셰이크 요청도 처리
# 동일한 인증서/키를 공유하거나 별도 설정 가능
성능 이점: RPC-over-TLS + kTLS 조합은 IPsec 터널 대비 오버헤드가 낮습니다. kTLS의 sendfile() 제로카피가 NFS 데이터 전송에 그대로 적용되고, HW TLS offload가 있는 NIC에서는 암호화가 NIC에서 수행되어 CPU 부하가 거의 없습니다.

BPF sockmap + kTLS 통합 — 커널 내 L7 프록시 가속

BPF sockmap과 kTLS를 조합하면 유저스페이스 개입 없이 커널 내에서 TLS 복호화 → BPF 프로그램 판단 → 다른 소켓으로 재암호화 전송을 수행할 수 있습니다. 이는 Envoy, Cilium 같은 L7 프록시에서 사이드카 오버헤드를 크게 줄이는 핵심 메커니즘입니다.

BPF sockmap + kTLS: 커널 내 L7 프록시 외부 클라이언트 TLS 연결 기존 방식: 유저스페이스 L7 프록시 (Envoy 등) kTLS RX 유저 복사 ① 프록시 로직 유저 복사 ② kTLS TX → recv() + 유저 판단 + send() = 2회 syscall, 2회 복사, 2회 컨텍스트 스위칭 BPF sockmap + kTLS: 커널 내 직접 전달 kTLS RX 복호화 평문 skb 생성 BPF sk_msg 프로그램 라우팅 판단 (커널 내) sockmap redirect bpf_msg_redirect_map() kTLS TX 암호화 → 백엔드 전송 → syscall 0회, 유저 복사 0회, 컨텍스트 스위칭 0회 (전부 커널 내 처리) 백엔드 서버 TLS 연결 기존: ~2μs 지연, CPU 높음 sockmap: ~0.5μs 지연, CPU 낮음
/* ━━━ BPF sockmap + kTLS 구현 예시 ━━━ */

/* BPF 프로그램 (sk_msg 타입) — 커널 내 L7 라우팅 */
/* bpf/ktls_redirect.bpf.c */

#include <vmlinux.h>
#include <bpf/bpf_helpers.h>

/* sockmap: 소켓 쌍(프론트엔드 ↔ 백엔드) 저장 */
struct {
    __uint(type, BPF_MAP_TYPE_SOCKMAP);
    __uint(max_entries, 65535);
    __type(key, __u32);
    __type(value, __u64);
} sock_map SEC(".maps");

/* sk_msg 프로그램: kTLS가 복호화한 평문 데이터를 받아
 * 목적지 소켓으로 redirect — 유저스페이스 개입 없음 */
SEC("sk_msg")
int ktls_redirect_prog(struct sk_msg_md *msg)
{
    /* 소켓 쿠키로 쌍을 찾는 방식 (실무에서는 해시맵 활용) */
    __u32 key = msg->local_port;

    /* kTLS가 이미 복호화한 평문 데이터가 msg에 담겨 있음
     *
     * 여기서 L7 판단 가능:
     *   - HTTP 요청 파싱 (Host, Path 등)
     *   - 간단한 로드밸런싱 로직
     *   - 접근 제어 (차단 시 SK_DROP 반환)
     */

    /* 목적지 소켓으로 데이터 redirect
     * → 목적지 소켓에 kTLS TX가 설치되어 있으면
     *   커널이 자동으로 재암호화하여 전송 */
    return bpf_msg_redirect_map(msg, &sock_map, key, BPF_F_INGRESS);
}

/* sk_skb 프로그램: 수신 데이터를 sockmap으로 분류 */
SEC("sk_skb/stream_verdict")
int ktls_stream_verdict(struct __sk_buff *skb)
{
    __u32 key = skb->local_port;
    return bpf_sk_redirect_map(skb, &sock_map, key, 0);
}

char LICENSE[] SEC("license") = "GPL";
/* ━━━ 유저스페이스: sockmap에 kTLS 소켓 쌍 등록 ━━━ */

#include <bpf/libbpf.h>
#include <bpf/bpf.h>

static int setup_sockmap_pair(int map_fd,
                              int frontend_fd, int backend_fd,
                              __u32 frontend_key, __u32 backend_key)
{
    int err;

    /* 프론트엔드 소켓 등록 (클라이언트 → 프록시) */
    err = bpf_map_update_elem(map_fd, &frontend_key,
                              &backend_fd, BPF_ANY);
    if (err) return err;

    /* 백엔드 소켓 등록 (프록시 → 백엔드 서버) */
    err = bpf_map_update_elem(map_fd, &backend_key,
                              &frontend_fd, BPF_ANY);
    return err;

    /* 이후 데이터 흐름:
     *
     * 클라이언트 → NIC → TCP → kTLS RX (복호화)
     *   → BPF sk_msg 프로그램 (커널 내 라우팅)
     *     → bpf_msg_redirect_map() → 백엔드 소켓
     *       → kTLS TX (재암호화) → TCP → NIC → 백엔드 서버
     *
     * 유저스페이스 개입 없이 전체 경로가 커널에서 완결 */
}
sockmap + kTLS 실무 활용:
  • Cilium — eBPF 기반 서비스 메시에서 kTLS sockmap을 활용하여 사이드카 프록시 없이 mTLS를 구현합니다.
  • Envoy + BPF — Envoy 프록시의 데이터 플레인(Data Plane)을 커널로 오프로드하여 지연과 CPU 사용을 줄입니다.
  • Katran (Meta) — Meta의 L4 로드밸런서에서 kTLS 소켓 간 데이터를 커널 내에서 직접 전달합니다.
  • 핵심 조건: 양쪽 소켓 모두 kTLS가 활성화되어야 하며, BPF 프로그램이 sk_msg 타입으로 attach되어야 합니다.
제약사항: sockmap redirect는 스트림 데이터(TCP)에서만 동작합니다. kTLS 레코드 경계와 BPF msg 경계가 반드시 일치하지 않을 수 있으므로, L7 파싱이 필요한 경우 여러 msg에 걸쳐 레코드가 분리될 수 있음을 고려해야 합니다. 또한 bpf_msg_redirect_map() 실패 시 데이터가 유실되지 않도록 fallback 경로(유저스페이스 recv/send)를 반드시 준비해야 합니다.

kTLS 보안 고려사항 및 모범 사례

kTLS는 TLS 레코드 계층의 데이터 경로만 최적화하며, 보안 수준 자체를 변경하지 않습니다. 그러나 커널에서 암복호화를 처리하므로, 유저스페이스 TLS와는 다른 보안 표면(Attack Surface)이 존재합니다. 이 절에서는 kTLS 고유의 보안 고려사항과 운영 모범 사례를 정리합니다.

kTLS 보안 경계 — 유저스페이스 vs 커널 책임 유저스페이스 TLS 라이브러리 책임 TLS 핸드셰이크 (ClientHello/ServerHello) 인증서 검증 (CA 체인, OCSP, CRL) 프로토콜 버전/Cipher Suite 협상 ALPN (HTTP/2, h3 등) 협상 키 재료 추출 및 kTLS 설치 설정 오류 → 보안 수준 저하 (kTLS와 무관) 커널 kTLS 책임 AEAD 암복호화 (AES-GCM, ChaCha20) TLS 레코드 프레이밍 (헤더, 태그) 레코드 시퀀스 번호 관리 KeyUpdate RX 정지 메커니즘 HW offload resync (패킷 유실 대응) 커널 버그 → 메모리 노출/MAC 우회 가능성 (드묾)
보안 항목위험 요소모범 사례
키 재료 노출 커널 메모리 덤프(Core Dump)에 대칭키가 포함될 수 있음 kdump 설정에서 /proc/kcore 접근 제한, 키 재료가 있는 slab을 KASAN으로 감시
Nonce 재사용 AES-GCM에서 nonce(IV) 재사용 시 기밀성 완전 파괴 커널이 시퀀스 번호를 자동 증가시키지만, 수동 TLS_TX 재설치 시 rec_seq 값 검증 필수
TLS_RX_EXPECT_NO_PAD 악의적 peer가 패딩을 추가하면 재복호화 비용 증가 (DoS 벡터) 신뢰할 수 있는 peer에만 사용 — 커널 문서가 명시적으로 경고
TLS_TX_ZEROCOPY_RO 전송 중 원본 데이터 변경 시 무결성 오류 (재전송 패킷 불일치) device offload 전용, 전송 중 파일 수정 금지 보장 필요
커널 취약점 표면 kTLS 코드 버그로 인한 메모리 손상, use-after-free 커널 버전 최신 유지, CONFIG_KASAN/CONFIG_UBSAN 활성화, CVE 모니터링
HW offload 투명성 NIC 펌웨어 버그로 암호화 우회, 평문 노출 tcpdump로 와이어 레벨 암호화 검증, NIC 펌웨어 업데이트 정책
KeyUpdate 미처리 라이브러리가 kTLS rekey를 지원하지 않으면 RX 영구 정지 TlsRxRekeyReceived/TlsRxRekeyOk 모니터링, 라이브러리 rekey 지원 확인
핸드셰이크 보안 kTLS는 핸드셰이크에 관여하지 않으므로 다운그레이드 공격 등은 라이브러리 설정에 의존 TLS 1.2 이상 강제, 안전한 cipher suite만 허용, 인증서 검증 활성화

kTLS vs IPsec vs WireGuard 보안 비교

항목kTLSIPsec (xfrm)WireGuard
보호 계층 L7 (애플리케이션 연결별) L3 (IP 패킷 단위) L3 (터널 단위)
암호화 대상 TCP 스트림 내 TLS 레코드 IP 패킷 전체 또는 페이로드 IP 패킷 전체
인증 방식 X.509 인증서, PSK IKEv2, X.509, PSK Curve25519 공개키
sendfile 제로카피 가능 (핵심 이점) 불가 불가
HW offload NIC TLS offload NIC IPsec offload 제한적
연결별 제어 소켓 단위 세밀 제어 SA/SP 정책 기반 피어(Peer) 단위
애플리케이션 수정 필요 (TLS 라이브러리 사용) 불필요 (투명) 불필요 (투명)
적합한 사용처 웹서버, API, 프록시 사이트 간 VPN, 호스트 간 전송 보안 원격 접속 VPN, 사이트 간 VPN
PFS (Perfect Forward Secrecy) TLS 1.3 기본 지원 IKEv2 rekey 내장 (1-RTT handshake)
핵심 정리: kTLS는 애플리케이션 계층의 연결별 암호화 최적화이고, IPsec/WireGuard는 네트워크 계층의 투명한 전송 보안입니다. 용도가 다르므로 직접 대체 관계가 아니라 보완 관계입니다. 예를 들어 NFS-over-TLS(kTLS)와 사이트 간 VPN(WireGuard)을 동시에 사용할 수 있습니다.
# ━━━ kTLS 보안 운영 체크리스트 ━━━

# 1. 커널 보안 옵션 확인
grep -E 'CONFIG_KASAN|CONFIG_UBSAN|CONFIG_LOCKDEP' /boot/config-$(uname -r)

# 2. kTLS 관련 CVE 확인
# https://cve.mitre.org/cgi-bin/cvekey.cgi?keyword=ktls+linux
# net/tls/ 디렉터리의 최근 보안 패치 확인:
git log --oneline --since="6 months ago" -- net/tls/

# 3. HW offload 와이어 레벨 검증
# NIC가 실제로 암호화하는지 tcpdump로 확인
tcpdump -i eth0 -X port 443 | head -50
# TLS 레코드 헤더(0x17 0x03 0x03)와 암호문이 보여야 정상
# 평문이 보이면 offload 또는 kTLS 미활성화

# 4. 복호화 에러 모니터링 (MAC 검증 실패 = 무결성 위반)
watch -n 5 'cat /proc/net/tls_stat | grep -E "DecryptError|NoPadViolation"'

# 5. Cipher Suite 안전성 확인
# AES-128-GCM, AES-256-GCM, ChaCha20-Poly1305 모두 안전
# AES-CCM은 IoT용, 일반 서버에서는 GCM 우선 권장
openssl ciphers -v 'TLS_AES_128_GCM_SHA256:TLS_AES_256_GCM_SHA384:TLS_CHACHA20_POLY1305_SHA256'

# 6. TLS 1.3 필수화 (가능한 경우)
# OpenSSL: SSL_CTX_set_min_proto_version(ctx, TLS1_3_VERSION)
# nginx: ssl_protocols TLSv1.3;

# 7. 키 재료 보호 — core dump 제한
echo 0 > /proc/sys/kernel/core_pattern
# 또는 systemd: Storage=none in /etc/systemd/coredump.conf

kTLS 향후 발전 방향

kTLS는 단순한 TLS 오프로드에서 커널 네트워크 보안의 핵심 인프라로 확장되고 있습니다. 아래는 현재 진행 중이거나 논의 단계의 주요 발전 방향입니다.

io_uring + kTLS sendfile 최적화

io_uring의 비동기 sendfile/splice 경로가 kTLS와 결합되면, 단일 스레드(Thread)에서 수만 개의 TLS 연결을 효율적으로 처리할 수 있습니다. 현재 io_uring_cmd를 통한 kTLS 직접 제어 패치(Patch)가 논의 중이며, 기존 io_uring send/recv는 이미 kTLS 소켓에서 동작합니다.

QUIC 커널 지원 — kTLS ULP 모델 확장

QUIC(RFC 9000)은 UDP 위의 전송 프로토콜이지만, TLS 1.3 핸드셰이크를 내장합니다. net/quic 패치셋은 kTLS의 ULP(Upper Layer Protocol) 모델을 확장하여 QUIC 레코드 암/복호화를 커널에서 처리하려 합니다. 이렇게 되면 kTLS의 HW offload, sendfile, splice 인프라를 QUIC에서도 재사용할 수 있습니다.

/* QUIC ULP 설정 (제안 단계) — kTLS와 유사한 API */
setsockopt(fd, SOL_UDP, UDP_ULP, "quic", 4);

struct quic_crypto_info_aes_gcm_128 crypto;
crypto.version = QUIC_VERSION_1;
/* ... 키/IV 설정 ... */
setsockopt(fd, SOL_QUIC, QUIC_SOCKOPT_CRYPTO_SEND, &crypto, sizeof(crypto));

TLS 1.3 0-RTT (Early Data) 커널 지원

TLS 1.3의 0-RTT 모드는 재연결 시 핸드셰이크 없이 즉시 데이터를 전송합니다. 커널에서 0-RTT 데이터를 인식하고 올바르게 처리하려면 kTLS에 early data 상태 머신을 추가해야 합니다. 현재는 유저스페이스 TLS 라이브러리가 0-RTT를 처리한 뒤 일반 kTLS 경로로 전환하는 방식이 일반적입니다.

6.7 이후 주요 변경사항

버전변경사항관련 설정 / 모듈
6.7 TLS Handshake Upcall 도입 CONFIG_TLS_HANDSHAKE
6.7 tlshd Netlink 인터페이스 handshake genl family
6.7 RPC-over-TLS (NFS) xprtsec=tls|mtls
6.8 Handshake timeout 개선 소켓 리소스 누수 방지
6.8 PSK identity hint 지원 NVMe-oF/TCP 연동 개선
6.9+ ksmbd TLS 통합 강화 SMB 3.x 전송 암호화
커널 빌드 시 필요 옵션 요약:
CONFIG_TLS=m              # kTLS 기본 모듈
CONFIG_TLS_DEVICE=y       # HW offload (선택)
CONFIG_TLS_HANDSHAKE=y    # Handshake Upcall (6.7+)
CONFIG_NFS_V4=y           # NFS v4 (RPC-over-TLS 전제)
CONFIG_SUNRPC_XPRT_TLS=y  # RPC-over-TLS 전송

흔한 실수와 주의사항

kTLS를 처음 도입하거나 운영 환경에 적용할 때 자주 만나는 실수를 정리합니다. 이 항목들은 실무에서 시간을 절약하는 데 도움이 됩니다.

kTLS 도입 전 필수 체크리스트 1. 커널 모듈 로드 modprobe tls CONFIG_TLS=y 또는 m 누락 시: ENOPROTOOPT 에러 2. OpenSSL kTLS 빌드 OpenSSL 3.0+ (enable-ktls) openssl version -a | grep KTLS 누락 시: kTLS 비활성화 (조용히) 3. 애플리케이션 옵션 SSL_OP_ENABLE_KTLS 설정 BIO_get_ktls_send() 확인 누락 시: 유저스페이스 TLS로 동작 4. Cipher Suite 호환 AES-GCM-128/256, ChaCha20 CBC 계열은 kTLS 미지원 불일치 시: SW TLS fallback 5. Fallback 구현 필수 kTLS 미활성화 시 SSL_write() 항상 BIO_get_ktls 확인 후 분기 미구현 시: 환경 변화에 취약 6. 모니터링 설정 /proc/net/tls_stat 주기적 수집 TlsDecryptError 임계값 알림 미설정 시: 장애 감지 지연 가장 흔한 5가지 실수 tls 모듈 미로드 → modprobe tls를 부팅 시 자동 로드 설정 (/etc/modules-load.d/) SSL_sendfile() 호출 전 kTLS TX 활성화 미확인 → BIO_get_ktls_send() == 0이면 ENOTSUP TLS 1.3 KeyUpdate 미처리 → recv()가 EKEYEXPIRED 반환 → 연결이 멈춘 것처럼 보임 HW offload 기대하고 확인 안 함 → ethtool -k로 tls-hw-tx-offload 지원 여부 반드시 확인 동적 응답에서 큰 성능 개선 기대 → kTLS 이점은 정적 파일/sendfile 경로에 집중
실수증상원인해결 방법
tls 모듈 미로드 상태로 배포 setsockopt()에서 ENOPROTOOPT 커널 tls 모듈이 로드되지 않음 /etc/modules-load.d/tls.conftls 추가하여 부팅 시 자동 로드
SSL_sendfile() 실패를 무시 반환값 -1, errno ENOTSUP kTLS TX 미활성화 상태에서 호출 BIO_get_ktls_send() 확인 후 SSL_write() fallback 구현
KeyUpdate 이벤트 미처리 recv()EKEYEXPIRED 반환, 연결 정지 TLS 1.3 상대방이 KeyUpdate 전송 라이브러리의 rekey 콜백 연동 확인, TlsRxRekeyReceived 모니터링
CBC cipher suite로 kTLS 기대 kTLS 비활성화 (조용히 fallback) kTLS는 AEAD만 지원 (AES-GCM, ChaCha20) cipher suite를 AEAD 계열로 설정, ssl_ciphers 확인
sendfile 중 파일 변경 수신 측 TLS 무결성 오류 (bad_record_mac) TLS_TX_ZEROCOPY_RO 모드에서 전송 중 파일 수정 전송 중 원본 파일 불변성 보장, 또는 zerocopy 미사용
kTLS 통계 미모니터링 장애 후에야 kTLS 미동작을 인지 /proc/net/tls_stat 수집 누락 모니터링 시스템에 TlsCurrTxSw, TlsDecryptError 지표 추가
HW offload 가정만 하고 미확인 CPU 사용률 기대만큼 감소하지 않음 NIC이 kTLS offload를 지원하지 않음 ethtool -k eth0 | grep tlsethtool -S로 실제 offload 확인
# ━━━ kTLS 운영 전 빠른 점검 스크립트 ━━━

#!/bin/bash
# kTLS 도입 전 환경 점검을 한 번에 수행합니다.

echo "=== kTLS 사전 점검 ==="

# 1. 커널 모듈
if lsmod | grep -q '^tls '; then
    echo "[OK] tls 모듈 로드됨"
else
    echo "[FAIL] tls 모듈 미로드 → sudo modprobe tls"
fi

# 2. OpenSSL kTLS 지원
if openssl version -a 2>&1 | grep -qi ktls; then
    echo "[OK] OpenSSL kTLS 지원: $(openssl version)"
else
    echo "[WARN] OpenSSL kTLS 미지원 — enable-ktls 빌드 필요"
fi

# 3. 현재 kTLS 연결 수
if [ -f /proc/net/tls_stat ]; then
    echo "[INFO] 현재 kTLS 상태:"
    grep -E "TlsCurr|TlsDecrypt" /proc/net/tls_stat | sed 's/^/  /'
else
    echo "[WARN] /proc/net/tls_stat 없음 — tls 모듈 로드 필요"
fi

# 4. NIC HW offload
for iface in $(ls /sys/class/net/ | grep -v lo); do
    hw=$(ethtool -k "$iface" 2>/dev/null | grep "tls-hw" | head -3)
    if [ -n "$hw" ]; then
        echo "[INFO] $iface TLS offload:"
        echo "$hw" | sed 's/^/  /'
    fi
done

# 5. AES-NI 지원 (SW kTLS 성능에 중요)
if grep -q aes /proc/cpuinfo; then
    echo "[OK] AES-NI 지원됨 (SW kTLS 암호화 가속)"
else
    echo "[WARN] AES-NI 미지원 — ChaCha20-Poly1305 고려"
fi

echo "=== 점검 완료 ==="
운영 환경 배포 전 반드시 확인:
  • kTLS는 조용히 비활성화됩니다: 모듈 미로드, cipher 불일치, OpenSSL 미지원 등의 이유로 kTLS가 활성화되지 않아도 에러가 아닌 유저스페이스 fallback으로 동작합니다. 반드시 BIO_get_ktls_send()/proc/net/tls_stat로 실제 활성화를 확인해야 합니다.
  • 성능 벤치마크를 먼저 수행: kTLS의 이점은 워크로드에 따라 크게 달라집니다. 동적 API 응답 위주라면 개선이 미미할 수 있으므로, 도입 전 자체 워크로드로 벤치마크를 수행하세요.
  • 커널 업그레이드 시 재확인: 커널 업그레이드 후 tls 모듈이 자동 로드되지 않을 수 있습니다. /etc/modules-load.d/tls.conf를 설정해두면 안전합니다.