WireGuard

Linux WireGuard VPN 구현을 심층 설명합니다. Noise IK 기반 핸드셰이크와 키 갱신, peer·allowed-ips 라우팅 모델, Netlink 기반 설정(wg/wg-quick), roaming·persistent keepalive·NAT 트래버설, MTU/fragmentation 영향, 고속 처리 튜닝과 연결 불안정·핸드셰이크 실패·경로 누락 문제 디버깅 절차까지 실무 기준으로 정리합니다.

전제 조건: 네트워크 스택커널 보안 문서를 먼저 읽으세요. 보안 네트워킹은 정책 결정 경로와 실제 암복호화/필터 경로를 함께 보아야 운영 사고를 줄일 수 있습니다.
일상 비유: 이 주제는 출입 통제와 봉인 검사와 비슷합니다. 패킷을 통과시키기 전에 규칙 검사와 신원 확인을 거치듯이, 정책과 데이터 경로의 결합이 핵심입니다.

핵심 요약

  • 패킷 수명주기 — ingress, 처리, egress 경로를 연결합니다.
  • 큐/버퍼 모델 — sk_buff와 큐 지점의 역할을 분리합니다.
  • 정책/데이터 분리 — 제어 평면과 데이터 평면을 구분합니다.
  • 성능 지표 — PPS, 지연, 드롭 원인을 함께 분석합니다.
  • 오프로딩 경계 — NIC/XDP/DPDK 경계를 명확히 유지합니다.

단계별 이해

  1. 경로 고정
    문제가 발생한 ingress/egress 지점을 먼저 특정합니다.
  2. 큐 관찰
    백로그와 드롭 위치를 계측합니다.
  3. 정책 반영 확인
    라우팅/필터 변경이 데이터 경로에 반영됐는지 봅니다.
  4. 부하 검증
    실제 트래픽 패턴에서 재현성을 확인합니다.
관련 표준: WireGuard Protocol — 현대적인 VPN 프로토콜 표준입니다. 종합 목록은 참고자료 — 표준 & 규격 섹션을 참고하세요.
Peer A Static PubKey Ephemeral Key Peer B Static PubKey Session Key 생성 Handshake Init Handshake Response

WireGuard 심화

WireGuard는 Jason A. Donenfeld가 설계한 차세대 VPN 프로토콜로, Linux 커널 5.6에 공식 통합되었습니다 (drivers/net/wireguard/). IPSec이나 OpenVPN 대비 코드량이 약 4,000줄(vs IPSec 수만 줄)로 극도로 간결하며, 최신 암호화 프리미티브만 사용하여 보안 감사가 용이합니다. 커널 레벨에서 동작하므로 유저스페이스 VPN 대비 높은 성능을 제공합니다.

ℹ️

WireGuard 설계 철학: "Cryptokey Routing" — 각 피어에 공개키와 허용 IP 대역을 매핑하여, 라우팅 테이블처럼 동작합니다. 복잡한 상태 머신이나 협상 과정 없이, 패킷이 도착하면 공개키 기반으로 자동 핸드셰이크를 수행합니다. 설정이 SSH authorized_keys 수준으로 단순합니다.

WireGuard vs IPSec vs OpenVPN 비교

특성WireGuardIPSec (strongSwan)OpenVPN
코드 라인 ~4,000 ~400,000+ ~100,000+
동작 레이어 커널 (L3 netdevice) 커널 (xfrm) 유저스페이스 (tun/tap)
프로토콜 UDP (단일 포트) ESP (IP 프로토콜 50) + IKEv2 TCP/UDP
암호화 협상 없음 (고정 암호 스위트) IKEv2로 협상 TLS 핸드셰이크
핵심 암호 ChaCha20-Poly1305, Curve25519 AES-GCM, RSA/ECDSA AES-GCM, RSA/ECDHE
키 교환 Noise IK (1-RTT) IKEv2 (2-RTT) TLS (2-3 RTT)
연결 상태 Stateless (Cryptokey Routing) Stateful (SA 관리) Stateful (TCP/TLS 세션)
로밍 지원 자동 (소스 IP 변경 감지) MOBIKE 확장 필요 재연결 필요
Throughput (1Gbps NIC) ~950 Mbps ~800 Mbps ~400 Mbps

암호화 프리미티브

WireGuard는 암호 민첩성(cipher agility)을 의도적으로 배제합니다. 단일 고정 암호 스위트만 사용하여 다운그레이드 공격을 원천 차단합니다:

용도알고리즘커널 구현설명
키 교환 (ECDH) Curve25519 lib/crypto/curve25519.c 타원곡선 Diffie-Hellman (X25519)
대칭 암호화 ChaCha20-Poly1305 lib/crypto/chacha20poly1305.c AEAD — 암호화 + 인증 동시 제공
해시 BLAKE2s lib/crypto/blake2s.c SHA 시리즈보다 빠르고 안전
MAC (키 유도) HKDF-BLAKE2s drivers/net/wireguard/noise.c Noise 프레임워크 표준 KDF
💡

ChaCha20-Poly1305 선택 이유: AES-GCM보다 소프트웨어 구현(특히 ARM 같은 비x86 플랫폼)에서 빠릅니다. AES-NI 없는 구형 CPU에서도 고성능이며, 타이밍 공격에 강합니다. Poly1305는 고속 MAC으로, AES-GCM의 GHASH보다 CPU 사이클이 적게 듭니다.

Noise Protocol Framework (IK 패턴)

WireGuard의 핸드셰이크는 Noise_IK_25519_ChaChaPoly_BLAKE2s 프로토콜입니다. Noise 프레임워크는 Signal 프로토콜과 유사한 현대 암호화 프로토콜 설계 도구로, 다양한 핸드셰이크 패턴을 조합할 수 있습니다.

IK 패턴 (Identity Known)

IK는 "개시자(Initiator)가 응답자(Responder)의 정적 공개키를 미리 알고 있음"을 의미합니다. WireGuard에서는 양측 모두가 상대방의 공개키를 설정 파일에 가지고 있습니다.

// Noise_IK 메시지 흐름 (단순화):

// 개시자(A) → 응답자(B) 첫 메시지:
A → B:  e, es, s, ss
        // e:  A의 임시 공개키(ephemeral)
        // es: A_ephemeral과 B_static으로 DH (B의 정적키는 이미 알고 있음)
        // s:  A의 정적 공개키 (암호화됨)
        // ss: A_static과 B_static으로 DH

// 응답자(B) → 개시자(A) 응답:
B → A:  e, ee, se
        // e:  B의 임시 공개키
        // ee: A_ephemeral과 B_ephemeral로 DH
        // se: B_static과 A_ephemeral로 DH

이 과정에서 전방 기밀성(forward secrecy)이 보장됩니다. 임시 키(ephemeral)가 매 핸드셰이크마다 새로 생성되므로, 과거 세션 키 노출이 미래 트래픽에 영향을 주지 않습니다.

Noise 상태 머신 핵심

// drivers/net/wireguard/noise.c

struct noise_handshake {
    enum {
        HANDSHAKE_ZEROED,
        HANDSHAKE_CREATED_INITIATION,
        HANDSHAKE_CONSUMED_INITIATION,
        HANDSHAKE_CREATED_RESPONSE,
        HANDSHAKE_CONSUMED_RESPONSE
    } state;

    u8 hash[NOISE_HASH_LEN];        // 누적 해시 (프로토콜 바인딩)
    u8 chaining_key[NOISE_HASH_LEN]; // HKDF 체이닝 키
    u8 remote_static[NOISE_PUBLIC_KEY_LEN];
    u8 local_ephemeral[NOISE_PUBLIC_KEY_LEN];
    u8 remote_ephemeral[NOISE_PUBLIC_KEY_LEN];
};

// 핸드셰이크 개시 (A → B):
bool wg_noise_handshake_create_initiation(
    struct message_handshake_initiation *dst,
    struct noise_handshake *handshake)
{
    // 1. 새 임시 키 쌍 생성
    curve25519_generate_secret(handshake->ephemeral_private);
    curve25519_generate_public(key, handshake->ephemeral_private);

    // 2. DH(ephemeral, remote_static) — es
    // 3. 정적 공개키 암호화하여 전송 (s)
    // 4. DH(local_static, remote_static) — ss
    // 5. 타임스탬프 암호화 (리플레이 방지)
    // 6. MAC 태그 추가

    dst->message_type = cpu_to_le32(MESSAGE_HANDSHAKE_INITIATION);
    dst->sender_index = wg_index_hashtable_insert(&handshake->entry);
    return true;
}

// 핸드셰이크 응답 (B → A):
bool wg_noise_handshake_consume_initiation(
    struct message_handshake_initiation *src,
    struct wg_peer *peer)
{
    // 1. 타임스탬프 검증 (리플레이 방지)
    // 2. MAC 검증
    // 3. DH 연산으로 공유 비밀 재구성
    // 4. 전송 키(TX/RX 대칭키) 유도

    return handshake->state == HANDSHAKE_CONSUMED_INITIATION;
}

핸드셰이크 상세 과정

1단계: Initiation (개시)

// message_handshake_initiation 구조체 (148 bytes):
struct message_handshake_initiation {
    __le32 message_type;          // 1
    __le32 sender_index;          // 로컬 피어 인덱스
    u8 unencrypted_ephemeral[32]; // e: 임시 공개키
    u8 encrypted_static[NOISE_ENCRYPTED_LEN]; // s: 정적키 (암호화됨)
    u8 encrypted_timestamp[NOISE_TIMESTAMP_LEN]; // 리플레이 방지
    u8 mac1[COOKIE_LEN];          // 피어 인증 MAC
    u8 mac2[COOKIE_LEN];          // 쿠키 (DoS 방어, 선택적)
};

mac1BLAKE2s(LABEL_MAC1, responder_public_key || message)로 계산됩니다. 이를 통해 잘못된 피어로부터의 무작위 패킷을 조기에 거부합니다 (CPU 비용이 큰 DH 연산 전에).

2단계: Response (응답)

// message_handshake_response 구조체 (92 bytes):
struct message_handshake_response {
    __le32 message_type;          // 2
    __le32 sender_index;          // 응답자 인덱스
    __le32 receiver_index;        // 개시자 인덱스 (상대방이 보낸 sender_index)
    u8 unencrypted_ephemeral[32]; // e: 응답자 임시 공개키
    u8 encrypted_nothing[16];     // 빈 페이로드 (인증 태그만)
    u8 mac1[COOKIE_LEN];
    u8 mac2[COOKIE_LEN];
};

이 메시지 후, 양측은 keypair를 생성합니다:

struct noise_keypair {
    u8 sending_key[NOISE_SYMMETRIC_KEY_LEN];   // TX 키
    u8 receiving_key[NOISE_SYMMETRIC_KEY_LEN]; // RX 키
    __le64 sending_counter;    // 리플레이 카운터 (송신)
    __le64 receiving_counter;  // 리플레이 카운터 (수신)
    struct kref refcount;
    u64 birthdate;             // 키 생성 시각 (rekeying 판단)
};

핸드셰이크 후 데이터 패킷

// message_data 구조체 (가변 길이):
struct message_data {
    __le32 message_type;    // 4
    __le32 receiver_index;  // 수신자 피어 인덱스
    __le64 counter;         // Nonce (단조 증가)
    u8 encrypted_data[];   // ChaCha20-Poly1305로 암호화된 IP 패킷
};

// 암호화 과정:
chacha20poly1305_encrypt(
    encrypted_data,           // 출력 버퍼
    plaintext,                // 원본 IP 패킷
    plaintext_len,
    NULL,                     // AAD (추가 인증 데이터, 여기선 없음)
    0,
    counter,                  // Nonce
    keypair->sending_key      // 대칭 키
);

DoS 방어: 쿠키 메커니즘

WireGuard는 MAC2 쿠키로 CPU 비용이 큰 핸드셰이크 DoS를 방어합니다. 공격자가 가짜 소스 IP로 대량 핸드셰이크 요청을 보내면, 응답자는 "쿠키 요청"을 먼저 보냅니다:

// message_handshake_cookie (64 bytes):
struct message_handshake_cookie {
    __le32 message_type;        // 3
    __le32 receiver_index;      // 개시자 인덱스
    u8 nonce[COOKIE_NONCE_LEN];
    u8 encrypted_cookie[COOKIE_ENCRYPTED_SIZE];
};

// 쿠키 계산:
cookie = BLAKE2s(label || peer_public_key || src_ip_port)
// 응답자는 이 쿠키를 암호화하여 전송.
// 개시자는 다음 핸드셰이크 시 mac2 필드에 이 쿠키를 포함해야 함.

쿠키는 소스 IP 기반이므로, IP 스푸핑 공격자는 쿠키를 받을 수 없습니다. 이를 통해 응답자는 "핸드셰이크 속도 제한" 임계값을 초과한 경우에만 쿠키를 요구하여, 정상 트래픽에는 영향을 주지 않습니다.

// drivers/net/wireguard/cookie.c

void wg_cookie_checker_precompute_peer_keys(struct wg_peer *peer)
{
    // 피어별 BLAKE2s 키 사전 계산 (성능 최적화)
    blake2s(peer->latest_cookie.cookie,
            peer->handshake.remote_static,
            NULL, COOKIE_LEN, NOISE_PUBLIC_KEY_LEN, 0);
}

bool wg_cookie_validate_packet(struct wg_device *wg,
                                struct sk_buff *skb)
{
    if (!ratelimit_allow(&peer->handshake_ratelimit))
        return false; // 속도 제한 초과 시 쿠키 요구
    return true;
}

커널 구현 아키텍처

WireGuard는 네트워크 디바이스로 구현됩니다 (wg0, wg1 등). struct net_device를 등록하여, 일반 네트워크 인터페이스처럼 동작합니다.

// drivers/net/wireguard/device.c

static const struct net_device_ops netdev_ops = {
    .ndo_open       = wg_open,
    .ndo_stop       = wg_stop,
    .ndo_start_xmit = wg_xmit,           // 송신 경로
    .ndo_get_stats64 = wg_get_stats64,
};

static void wg_setup(struct net_device *dev)
{
    dev->netdev_ops = &netdev_ops;
    dev->type = ARPHRD_NONE;            // L3 장치 (ARP 없음)
    dev->flags = IFF_POINTOPOINT | IFF_NOARP;
    dev->mtu = ETH_DATA_LEN - MESSAGE_DATA_OVERHEAD; // 기본 MTU
    dev->needed_headroom = DATA_PACKET_HEAD_ROOM;
}

WireGuard 인터페이스는 TUN 장치와 유사하지만, 암호화/복호화가 커널 내부에서 처리됩니다. 유저스페이스로 패킷이 올라가지 않으므로, 컨텍스트 스위칭 오버헤드가 없습니다.

주요 데이터 구조

// drivers/net/wireguard/device.h

struct wg_device {
    struct net_device *dev;
    struct crypt_queue encrypt_queue;     // 암호화 워크큐
    struct crypt_queue decrypt_queue;     // 복호화 워크큐
    struct sock __rcu *sock4;             // UDP 소켓 (IPv4)
    struct sock __rcu *sock6;             // UDP 소켓 (IPv6)
    u16 incoming_port;                    // 리슨 포트
    struct noise_static_identity static_identity; // 로컬 정적키
    struct hlist_head peer_hashtable[PEER_HASHTABLE_SIZE];
    struct allowedips peer_allowedips;    // IP → 피어 매핑
    struct mutex device_update_lock;
};

struct wg_peer {
    struct wg_device *device;
    struct noise_handshake handshake;
    struct noise_keypair *keypairs[3];    // current, previous, next
    struct endpoint endpoint;               // 원격 주소 (동적)
    struct list_head allowedips_list;      // 허용 IP 대역

    // 통계 및 타이머
    u64 rx_bytes, tx_bytes;
    struct timer_list timer_retransmit_handshake;
    struct timer_list timer_new_handshake;
    struct timer_list timer_zero_key_material;
    struct timer_list timer_persistent_keepalive;
};

AllowedIPs: Cryptokey Routing

AllowedIPs는 WireGuard의 핵심 개념으로, IP 주소를 공개키에 매핑합니다. 송신 시에는 목적지 IP로 피어를 검색하고, 수신 시에는 복호화된 패킷의 소스 IP가 허용 대역에 있는지 검증합니다.

// drivers/net/wireguard/allowedips.h

struct allowedips {
    struct allowedips_node __rcu *root4;  // IPv4 트라이
    struct allowedips_node __rcu *root6;  // IPv6 트라이
};

struct allowedips_node {
    struct wg_peer *peer;
    struct allowedips_node __rcu *bit[2]; // 이진 트라이
    u8 cidr, bit_at_a, bit_at_b;
    u8 bits[] __aligned(__alignof(u64));    // IP 주소
};

// 검색 함수 (최장 prefix 매칭):
struct wg_peer *wg_allowedips_lookup_dst(
    struct allowedips *table,
    struct sk_buff *skb)
{
    // IP 헤더에서 목적지 주소 추출
    if (skb->protocol == htons(ETH_P_IP)) {
        struct iphdr *ip = ip_hdr(skb);
        return lookup_ip4(table->root4, &ip->daddr);
    } else if (skb->protocol == htons(ETH_P_IPV6)) {
        struct ipv6hdr *ip = ipv6_hdr(skb);
        return lookup_ip6(table->root6, &ip->daddr);
    }
    return NULL;
}

이진 트라이 구조는 O(log n) 검색 속도를 제공하며, RCU로 동기화하여 락 없이 읽기가 가능합니다 (패킷 처리 경로에서 성능 중요).

💡

보안 속성: 수신 패킷의 소스 IP를 AllowedIPs로 검증하므로, IP 스푸핑이 불가능합니다. 암호화된 패킷을 복호화한 후, 내부 IP 헤더의 소스 주소가 해당 피어의 허용 대역에 없으면 패킷을 폐기합니다. 이는 커널 방화벽 규칙 없이 강력한 접근 제어를 제공합니다.

패킷 처리 흐름

송신 경로 (TX)

// 1. 유저스페이스 → wg0 인터페이스 (IP 패킷):
wg_xmit(struct sk_buff *skb, struct net_device *dev)
{
    struct wg_device *wg = netdev_priv(dev);

    // 2. 목적지 IP로 피어 검색 (AllowedIPs):
    peer = wg_allowedips_lookup_dst(&wg->peer_allowedips, skb);
    if (!peer) {
        kfree_skb(skb);
        return NETDEV_TX_OK; // 라우팅 실패
    }

    // 3. 활성 keypair 확인:
    keypair = READ_ONCE(peer->keypairs[KEYPAIR_CURRENT]);
    if (!keypair || !READ_ONCE(keypair->sending.is_valid)) {
        // 핸드셰이크 필요
        wg_packet_send_queued_handshake_initiation(peer);
        goto err;
    }

    // 4. 암호화 워크큐에 추가:
    wg_packet_encrypt_worker(skb, peer, keypair);

    return NETDEV_TX_OK;
}

// 5. 암호화 워커 (별도 스레드/CPU 코어):
void wg_packet_encrypt_worker(struct work_struct *work)
{
    struct crypt_queue *queue = container_of(work, struct crypt_queue, work);
    struct sk_buff *skb;

    while ((skb = __ptr_ring_consume(&queue->ring)) != NULL) {
        // ChaCha20-Poly1305 암호화
        chacha20poly1305_encrypt(
            message->encrypted_data, skb->data, skb->len,
            NULL, 0, counter, keypair->sending_key
        );

        // 6. UDP 소켓으로 전송:
        udp_tunnel_xmit_skb(peer->endpoint.sock4, skb, ...);
    }
}

수신 경로 (RX)

// 1. UDP 소켓 수신 핸들러:
static int wg_receive(struct sock *sk, struct sk_buff *skb)
{
    struct wg_device *wg = sk->sk_user_data;
    struct message_header *header = (struct message_header *)skb->data;

    switch (header->type) {
    case MESSAGE_HANDSHAKE_INITIATION:
        wg_packet_handshake_receive_worker(wg, skb);
        break;
    case MESSAGE_HANDSHAKE_RESPONSE:
        wg_packet_handshake_receive_worker(wg, skb);
        break;
    case MESSAGE_DATA:
        // 2. receiver_index로 피어 검색:
        struct message_data *msg = (struct message_data *)skb->data;
        struct wg_peer *peer = wg_index_hashtable_lookup(
            wg->index_hashtable, msg->receiver_index
        );

        if (!peer) {
            kfree_skb(skb);
            return 0;
        }

        // 3. 복호화 워크큐에 추가:
        wg_packet_decrypt_worker(skb, peer);
        break;
    }
    return 0;
}

// 4. 복호화 워커:
void wg_packet_decrypt_worker(struct work_struct *work)
{
    struct sk_buff *skb;

    while ((skb = dequeue(&decrypt_queue)) != NULL) {
        // ChaCha20-Poly1305 복호화
        if (!chacha20poly1305_decrypt(
                plaintext, message->encrypted_data, len,
                NULL, 0, counter, keypair->receiving_key))
        {
            kfree_skb(skb);
            continue; // 복호화 실패 (인증 실패)
        }

        // 5. 리플레이 검증:
        if (!wg_replay_counter_validate(&keypair->receiving, counter)) {
            kfree_skb(skb);
            continue;
        }

        // 6. AllowedIPs 검증 (소스 IP 검사):
        struct iphdr *ip = (struct iphdr *)plaintext;
        if (!wg_allowedips_lookup_src(&peer->device->peer_allowedips,
                                         &ip->saddr, peer)) {
            kfree_skb(skb);
            continue; // IP 스푸핑 시도
        }

        // 7. 네트워크 스택으로 전달 (wg0 인터페이스 수신):
        skb->dev = wg->dev;
        skb->protocol = htons(ETH_P_IP); // 또는 ETH_P_IPV6
        netif_rx(skb);
    }
}

병렬 암/복호화와 NAPI

WireGuard는 멀티코어 병렬 처리를 위해 CPU별 워크큐를 사용합니다:

// drivers/net/wireguard/queueing.c

int wg_queue_init(void)
{
    wg_packet_queue_encrypt = alloc_percpu(struct crypt_queue);
    wg_packet_queue_decrypt = alloc_percpu(struct crypt_queue);

    // CPU별 워커 스레드 생성
    for_each_possible_cpu(cpu) {
        struct crypt_queue *queue = per_cpu_ptr(wg_packet_queue_encrypt, cpu);
        INIT_WORK(&queue->work, wg_packet_encrypt_worker);
        ptr_ring_init(&queue->ring, ENCRYPT_QUEUE_SIZE, GFP_KERNEL);
    }

    return 0;
}

// 패킷을 현재 CPU의 워크큐에 추가:
void wg_packet_encrypt_queue(struct sk_buff *skb, struct wg_peer *peer)
{
    int cpu = get_cpu();
    struct crypt_queue *queue = per_cpu_ptr(wg_packet_queue_encrypt, cpu);

    if (ptr_ring_produce_bh(&queue->ring, skb))
        queue_work_on(cpu, system_wq, &queue->work);

    put_cpu();
}

이 설계는 RX-TX 대칭을 보장합니다. 패킷이 도착한 CPU에서 복호화하고, 같은 CPU에서 암호화하여, 캐시 지역성(cache locality)을 최대화합니다.

타이머 메커니즘

WireGuard는 4개의 타이머로 핸드셰이크 재시도, 키 갱신, keepalive를 처리합니다:

// 1. RETRANSMIT_HANDSHAKE_TIMER: 핸드셰이크 응답 대기 (5초)
// 초기 핸드셰이크 후 응답이 없으면 재전송
static void wg_expired_retransmit_handshake(struct timer_list *timer)
{
    struct wg_peer *peer = from_timer(peer, timer, timer_retransmit_handshake);

    if (peer->timer_handshake_attempts > MAX_TIMER_HANDSHAKES) {
        // 최대 재시도 초과, 연결 종료로 간주
        wg_peer_timers_data_sent(peer);
        return;
    }

    peer->timer_handshake_attempts++;
    wg_packet_send_queued_handshake_initiation(peer);
}

// 2. NEW_HANDSHAKE_TIMER: 주기적 키 갱신 (120초)
// 키가 생성된 후 2분 경과 시 새 핸드셰이크 시작
static void wg_expired_new_handshake(struct timer_list *timer)
{
    struct wg_peer *peer = from_timer(peer, timer, timer_new_handshake);

    if (READ_ONCE(peer->keypairs[KEYPAIR_CURRENT])->is_valid)
        wg_packet_send_queued_handshake_initiation(peer);
}

// 3. ZERO_KEY_MATERIAL_TIMER: 키 만료 (180초)
// 키가 생성된 후 3분 경과 시 완전히 무효화
static void wg_expired_zero_key_material(struct timer_list *timer)
{
    struct wg_peer *peer = from_timer(peer, timer, timer_zero_key_material);

    wg_noise_keypair_put(peer->keypairs[KEYPAIR_CURRENT]);
    peer->keypairs[KEYPAIR_CURRENT] = NULL;
}

// 4. PERSISTENT_KEEPALIVE_TIMER: keepalive 패킷 전송 (설정 가능)
// NAT/방화벽 매핑 유지 목적 (기본값: 25초)
static void wg_expired_send_persistent_keepalive(struct timer_list *timer)
{
    struct wg_peer *peer = from_timer(peer, timer, timer_persistent_keepalive);

    if (peer->persistent_keepalive_interval)
        wg_packet_send_keepalive(peer);
}
💡

Keepalive 최적화: WireGuard는 "명시적 keepalive"를 지원합니다. PersistentKeepalive = 25로 설정하면, 25초마다 빈 패킷(handshake initiation 또는 data 패킷)을 전송하여, NAT 뒤의 클라이언트가 서버와 연결을 유지할 수 있습니다. 이는 UDP hole punching의 일종입니다.

리플레이 방지 (Sliding Window)

WireGuard는 64비트 카운터와 슬라이딩 윈도우로 리플레이 공격을 방지합니다:

// drivers/net/wireguard/receive.c

#define COUNTER_WINDOW_SIZE 2048 // 비트맵 크기

struct noise_replay_counter {
    u64 counter;
    unsigned long backtrack[COUNTER_BITS_TOTAL / BITS_PER_LONG];
    spinlock_t lock;
};

bool wg_replay_counter_validate(struct noise_replay_counter *counter, u64 their_counter)
{
    u64 index, index_current, top, i;
    unsigned long *bitmap;

    spin_lock_bh(&counter->lock);

    if (their_counter >= counter->counter) {
        // 새로운 카운터 (정상)
        index = their_counter - counter->counter;
        if (index > COUNTER_WINDOW_SIZE) {
            // 윈도우 크기 초과 → 비트맵 리셋
            memset(counter->backtrack, 0, sizeof(counter->backtrack));
        } else {
            // 비트맵 시프트
            for (i = 1; i <= index; ++i)
                counter->backtrack[(i / BITS_PER_LONG)] >>= 1;
        }
        counter->counter = their_counter;
        set_bit(0, counter->backtrack); // 현재 카운터 마킹
        spin_unlock_bh(&counter->lock);
        return true;
    }

    // 오래된 카운터 (재전송 또는 리플레이)
    index = counter->counter - their_counter;
    if (index >= COUNTER_WINDOW_SIZE) {
        spin_unlock_bh(&counter->lock);
        return false; // 윈도우 밖, 거부
    }

    if (test_and_set_bit(index, counter->backtrack)) {
        spin_unlock_bh(&counter->lock);
        return false; // 이미 수신한 패킷, 리플레이 공격
    }

    spin_unlock_bh(&counter->lock);
    return true;
}

2048 비트맵은 최대 2048개의 이전 패킷을 추적합니다. 패킷 손실·재정렬이 심한 네트워크(위성 링크 등)에서도 충분한 여유가 있습니다.

로밍 (Roaming) 메커니즘

WireGuard는 클라이언트 IP 변경(Wi-Fi ↔ 4G/5G 전환)을 자동으로 처리합니다:

// drivers/net/wireguard/receive.c

void wg_socket_set_peer_endpoint(struct wg_peer *peer, const struct endpoint *endpoint)
{
    // 수신 패킷의 소스 주소를 피어의 엔드포인트로 업데이트
    write_lock_bh(&peer->endpoint_lock);
    if (endpoint->addr.sa_family == AF_INET)
        peer->endpoint.addr4 = endpoint->addr4;
    else
        peer->endpoint.addr6 = endpoint->addr6;
    write_unlock_bh(&peer->endpoint_lock);

    // 다음 송신 패킷부터 새 주소 사용
}

패킷이 유효한 피어로부터 수신되면(복호화 성공), 해당 패킷의 소스 주소를 자동으로 피어의 엔드포인트로 업데이트합니다. 클라이언트가 새 IP로 패킷을 보내면, 서버는 즉시 응답 주소를 변경합니다. 추가 협상이나 재연결이 불필요합니다.

wg(8) 유틸리티는 Generic Netlink로 커널과 통신합니다:

// drivers/net/wireguard/netlink.c

static const struct genl_ops wg_genl_ops[] = {
    {
        .cmd = WG_CMD_GET_DEVICE,
        .doit = wg_get_device,
        .flags = GENL_ADMIN_PERM, // CAP_NET_ADMIN 필요
    },
    {
        .cmd = WG_CMD_SET_DEVICE,
        .doit = wg_set_device,
        .flags = GENL_ADMIN_PERM,
    },
};

static struct genl_family wg_genl_family = {
    .name = WG_GENL_NAME,          // "wireguard"
    .version = WG_GENL_VERSION,
    .maxattr = WGDEVICE_A_MAX,
    .ops = wg_genl_ops,
    .n_ops = ARRAY_SIZE(wg_genl_ops),
};

// wg set wg0 listen-port 51820 → SET_DEVICE 메시지:
int wg_set_device(struct sk_buff *skb, struct genl_info *info)
{
    struct wg_device *wg = lookup_interface(info->attrs[WGDEVICE_A_IFNAME]);

    if (info->attrs[WGDEVICE_A_LISTEN_PORT]) {
        u16 port = nla_get_u16(info->attrs[WGDEVICE_A_LISTEN_PORT]);
        wg_socket_reinit(wg, NULL, port); // UDP 소켓 재생성
    }

    if (info->attrs[WGDEVICE_A_PEERS]) {
        // 피어 추가/수정
        wg_set_device_peers(wg, info->attrs[WGDEVICE_A_PEERS]);
    }

    return 0;
}

설정 예제와 커널 동작

서버 설정 (/etc/wireguard/wg0.conf)

[Interface]
Address = 10.0.0.1/24
ListenPort = 51820
PrivateKey = SERVER_PRIVATE_KEY

[Peer]
PublicKey = CLIENT_PUBLIC_KEY
AllowedIPs = 10.0.0.2/32

위 설정을 wg-quick up wg0로 활성화하면, 다음 커널 동작이 발생합니다:

// 1. wg0 네트워크 장치 생성
ip link add dev wg0 type wireguard

// 2. 로컬 주소 할당
ip address add 10.0.0.1/24 dev wg0

// 3. Private Key 설정 (Netlink):
wg set wg0 private-key /tmp/privatekey

// 4. Listen Port 설정:
wg set wg0 listen-port 51820
// → wg_socket_init(): UDP 소켓 생성, bind(0.0.0.0:51820)

// 5. 피어 추가:
wg set wg0 peer CLIENT_PUBLIC_KEY allowed-ips 10.0.0.2/32
// → wg_peer_create(): struct wg_peer 할당
// → wg_allowedips_insert(): 10.0.0.2/32를 AllowedIPs 트라이에 추가

// 6. 인터페이스 활성화
ip link set wg0 up

클라이언트 설정

[Interface]
Address = 10.0.0.2/32
PrivateKey = CLIENT_PRIVATE_KEY

[Peer]
PublicKey = SERVER_PUBLIC_KEY
Endpoint = server.example.com:51820
AllowedIPs = 0.0.0.0/0             # 모든 트래픽 라우팅
PersistentKeepalive = 25         # NAT 유지

AllowedIPs = 0.0.0.0/0는 모든 IP 트래픽을 WireGuard 터널로 보냅니다. 커널은 다음과 같이 라우팅 테이블을 수정합니다:

# wg-quick이 자동으로 실행:
ip route add 0.0.0.0/0 dev wg0 table 51820
ip rule add not fwmark 51820 table 51820
ip rule add table main suppress_prefixlength 0

# 결과: 기본 게이트웨이를 제외한 모든 트래픽이 wg0로 전송됨

네트워크 네임스페이스와 WireGuard

WireGuard는 네임스페이스를 완전히 지원합니다. 다음은 네임스페이스 간 VPN 예제입니다:

# 네임스페이스 생성
ip netns add ns_client
ip netns add ns_server

# 서버 (ns_server):
ip link add wg_srv type wireguard
ip link set wg_srv netns ns_server
ip netns exec ns_server wg set wg_srv private-key /tmp/srv_priv listen-port 51820
ip netns exec ns_server ip addr add 10.0.0.1/24 dev wg_srv
ip netns exec ns_server ip link set wg_srv up

# 클라이언트 (ns_client):
ip link add wg_cli type wireguard
ip link set wg_cli netns ns_client
ip netns exec ns_client wg set wg_cli private-key /tmp/cli_priv peer <srv_pubkey> \
    endpoint 127.0.0.1:51820 allowed-ips 10.0.0.0/24
ip netns exec ns_client ip addr add 10.0.0.2/32 dev wg_cli
ip netns exec ns_client ip link set wg_cli up

# 테스트:
ip netns exec ns_client ping 10.0.0.1

WireGuard 장치는 생성된 네임스페이스에 고정되지 않습니다. ip link set wg0 netns <name>로 네임스페이스 간 이동이 가능하며, 각 네임스페이스는 독립적인 라우팅 테이블을 갖습니다.

성능 특성과 최적화

Throughput 벤치마크

환경WireGuard (Mbps)IPSec (Mbps)OpenVPN (Mbps)
1Gbps NIC (x86_64 AES-NI) 950 850 400
1Gbps NIC (ARM Cortex-A72) 800 600 250
10Gbps NIC (멀티코어) 9,200 7,500 N/A (병목)

CPU 사용률

WireGuard는 CPU 코어당 약 1Gbps 처리 가능합니다 (ChaCha20-Poly1305 암호화). 10Gbps 트래픽은 약 10개 코어를 사용합니다. 멀티코어 시스템에서 선형 확장(linear scaling)이 가능합니다.

# iperf3 테스트 (멀티스트림):
iperf3 -c 10.0.0.1 -P 8  # 8개 병렬 스트림
# → 각 스트림이 다른 CPU 코어에서 암호화 처리됨

레이턴시

WireGuard는 추가 레이턴시가 0.1ms 미만입니다 (LAN 환경). IPSec/OpenVPN은 0.5~2ms 추가 레이턴시가 있습니다. 이는 WireGuard의 간결한 핸드셰이크와 커널 레벨 처리 덕분입니다.

최적화 팁

💡
  1. MTU 조정: WireGuard 헤더(32바이트) + Poly1305 태그(16바이트) = 48바이트 오버헤드. 1500 MTU 네트워크에서는 wg0의 MTU를 1420으로 설정하여 단편화 방지:
    ip link set wg0 mtu 1420
  2. GSO/GRO 활성화: 대용량 패킷 병합으로 암호화 오버헤드 감소:
    ethtool -K wg0 gso on gro on tso on
  3. CPU 친화도: RPS/RFS로 패킷을 특정 CPU에 고정:
    echo f > /sys/class/net/wg0/queues/rx-0/rps_cpus  # 모든 CPU 사용
  4. 커널 파라미터: UDP 버퍼 크기 증가:
    sysctl -w net.core.rmem_max=26214400
    sysctl -w net.core.wmem_max=26214400

보안 속성 분석

공격 시나리오와 방어

공격방어 메커니즘구현
리플레이 공격 슬라이딩 윈도우 (2048 비트) wg_replay_counter_validate()
타이밍 공격 상수 시간 암호 연산 crypto_memneq(), ChaCha20 구현
DoS (핸드셰이크 폭주) 쿠키 메커니즘 wg_cookie_validate_packet()
중간자 공격 정적 공개키 사전 공유 Noise IK 패턴
IP 스푸핑 AllowedIPs 검증 wg_allowedips_lookup_src()
다운그레이드 공격 단일 고정 암호 스위트 협상 불가 설계

형식 검증 (Formal Verification)

WireGuard 프로토콜은 Tamarin 증명기Coq로 형식 검증되었습니다. 다음 속성이 수학적으로 증명되었습니다:

디버깅과 모니터링

커널 로그

# WireGuard 디버그 로그 활성화 (커널 빌드 옵션):
CONFIG_WIREGUARD_DEBUG=y

# 런타임 로그 확인:
dmesg | grep wireguard
# 예: "wireguard: wg0: Receiving handshake initiation from peer 1 (192.168.1.100:12345)"

상태 조회 (wg show)

# 모든 인터페이스 상태:
wg show all

# 특정 인터페이스:
wg show wg0
# 출력 예:
interface: wg0
  public key: SERVER_PUBLIC_KEY
  private key: (hidden)
  listening port: 51820

peer: CLIENT_PUBLIC_KEY
  endpoint: 192.168.1.100:54321
  allowed ips: 10.0.0.2/32
  latest handshake: 1 minute, 23 seconds ago
  transfer: 1.23 GiB received, 456 MiB sent

트래픽 캡처

# 외부 트래픽 (암호화된 UDP 패킷):
tcpdump -i eth0 udp port 51820 -w wireguard_encrypted.pcap

# 내부 트래픽 (복호화된 IP 패킷):
tcpdump -i wg0 -w wireguard_decrypted.pcap

# Wireshark 분석 시, WireGuard 프로토콜 디섹터는 핸드셰이크만 파싱 가능.
# 데이터 패킷은 암호화되어 있어 내용 확인 불가.

성능 프로파일링

# perf로 암호화 핫스팟 분석:
perf record -a -g -- sleep 10  # 10초간 샘플링
perf report

# 주요 함수:
# - chacha20_simd() — SIMD 최적화 암호화
# - poly1305_simd_blocks() — MAC 계산
# - curve25519_generic() — DH 연산 (핸드셰이크)

sysfs 인터페이스

# 피어 통계 (커널 5.12+):
cat /sys/class/net/wg0/statistics/rx_bytes
cat /sys/class/net/wg0/statistics/tx_bytes

# 핸드셰이크 카운터 (디버그 빌드):
cat /sys/kernel/debug/wireguard/wg0/peers/<peer_pubkey>/handshakes
⚠️

보안 주의사항: WireGuard는 wg show 출력에 개인 키를 표시하지 않습니다 ((hidden)). 설정 파일(/etc/wireguard/*.conf)은 반드시 chmod 600으로 보호하세요. 공개키만으로는 통신할 수 없으므로, 공개키 유출은 문제되지 않습니다. 그러나 개인 키 유출 시, 해당 키로 설정된 모든 피어가 위협받습니다.