Kernel TLS (kTLS)

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

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

핵심 요약

  • ULP 부착 — TCP 소켓에 TCP_ULP="tls"를 붙여 kTLS 경로를 활성화합니다.
  • 레코드 계층만 이동 — 인증서 검증, 핸드셰이크, 세션 정책은 여전히 유저스페이스 TLS 라이브러리가 담당합니다.
  • TX/RX 독립 설치TLS_TXTLS_RX는 별도 소켓 옵션이며, 재키잉도 방향별로 처리됩니다.
  • 핵심 이점 — 정적 파일 전송에서 sendfile(), splice(), 페이지 캐시 직결 경로가 가장 큰 이점을 냅니다.
  • 운영 포인트 — 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, 재전송, 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) — 종합 목록은 참고자료 — 표준 & 규격를 참고하세요.

kTLS (Kernel TLS) 심화

kTLS (Kernel TLS)는 TLS 레코드 계층의 암호화/복호화를 커널 소켓 계층에서 수행하는 메커니즘입니다. 전통적으로 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

유저스페이스 TLS vs kTLS 비교

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

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

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가 활성화된 경우에만 동작합니다.

레코드 경계, control message, record_size_limit

kTLS는 바이트 스트림을 TLS 레코드로 잘라 전송합니다. 기본적으로 각 send() 호출은 레코드 경계를 만들고, MSG_MORE를 주면 레코드 생성을 지연하다가 최대 길이(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 전용이며, 송신이 끝날 때까지 데이터가 절대 바뀌지 않는다는 가정이 필요합니다. 전송 중 파일이 바뀌면 원래 패킷과 재전송 패킷의 내용이 달라져 수신 측에서는 TLS 무결성 오류처럼 보일 수 있습니다.

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/펌웨어가 Linux networking stack 일부를 사실상 대체 일부 환경에서는 더 높은 오프로드율 가능 공식 문서 기준 host firewall, QoS, packet scheduling 의존 환경에는 부적합
운영상 중요한 제약: 커널 문서 기준으로 오프로드 모드는 NIC 설정에 따라 자동 선택되며, 현재는 연결별로 명시적 opt-in/opt-out을 세밀하게 제어하는 방식이 아닙니다. 또한 소프트웨어 인터페이스를 거치는 터널·가상화 경로에서는 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
패딩 없음 선택적 패딩 (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 성능 특성

시나리오예상 이점왜 이득이 나는가병목/주의점
정적 파일 + 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 오버헤드가 상대적으로 커집니다. 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 연동과 페이지 핀닝

kTLS에서 sendfile()파일 페이지 캐시를 유저스페이스로 복사하지 않고 커널 내에서 바로 TLS 레코드로 암호화하여 TCP에 전달합니다. 이 경로의 핵심은 페이지 캐시 페이지를 scatterlist에 직접 매핑하는 것으로, 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()로 참조 카운트가 증가된 상태입니다. 전송 중에 원본 파일이 수정되면 (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바이트) + 암호화된 페이로드 + 인증 태그로 구성됩니다. 이 절에서는 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 연결에서도 높은 처리량을 달성할 수 있습니다.

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/프록시에서 캐시된 콘텐츠 전달
  • 100Gbps 이상 NIC에서 HW offload와 함께 사용
  • CPU 바운드 TLS 워크로드에서 CPU 여유 확보

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

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

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로 상세 프로파일링을 하면 암호화 함수 내부의 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에 집중 — 워크로드 특성 확인
커널 패닉/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