Skip to content

Blog

This is a simple blog.

  1. sphinx_docs使用

  2. Toyvpn详解

  3. ubuntu无法拉docker镜像问题处理

  4. wireguard


wireguard实现原理分析

https://github1s.com/WireGuard/wireguard-linux/blob/stable/drivers/net/wireguard/noise.c https://www.wireguard.com/repositories/ https://git.zx2c4.com/wireguard-linux/tree/drivers/net/wireguard/

它有几个特点,一是p2p,二是udp转发,三是完全内核态实现/基于tun网卡实现,四是基于ecc加密。 总的来说,握手包特征比较明显,后续的udp的的特征也比较明显,可以识别出是wg的包。

握手流程是这样的:

握手时序图

仅一次 ECDH 密钥派生 + 两次包交互,握手完成后数据加解密直接复用会话密钥,无额外开销。

https://github.com/WireGuard/wireguard-linux/blob/stable/drivers/net/wireguard/noise.c https://github.com/WireGuard/wireguard-linux/blob/stable/drivers/net/wireguard/noise.h

sequenceDiagram
    participant A as Alice (发起方)
    participant B as Bob (响应方)

    Note over A,B: 阶段0:全局初始化 + 静态密钥配置
    A->>A: 1. 初始化 Blake2s 哈希上下文<br/>wg_noise_init() → 生成 handshake_init_hash/chaining_key
    A->>A: 2. 设置本地静态私钥并生成公钥<br/>wg_noise_set_static_identity_private_key(&static, private_key)
    A->>A: 3. 初始化握手结构并预计算静态-静态 ECDH<br/>wg_noise_handshake_init(&handshake, &static, peer_public, psk, peer)
    B->>B: 1. 同Alice:wg_noise_init() 初始化 Blake2s
    B->>B: 2. 同Alice:wg_noise_set_static_identity_private_key 设置静态密钥
    B->>B: 3. 同Alice:wg_noise_handshake_init 初始化握手结构

    Note over A,B: 阶段1:Alice 发起 Type 1 握手包(Initiation)
    A->>A: 1. 等待随机数源就绪<br/>wait_for_random_bytes()
    A->>A: 2. 生成临时密钥对并构建 Type 1 包<br/>wg_noise_handshake_create_initiation(dst, &handshake)
    A->>B: 发送 Type 1 包(UDP)<br/>包结构:魔数(4) + 类型(1) + 临时公钥(32) + 加密静态(48) + 加密时间戳(24) + 发送者索引(4)

    Note over A,B: 阶段2:Bob 处理 Type 1 并回复 Type 2(Response)
    B->>B: 1. 解析 Type 1 包并查找对等体<br/>wg_noise_handshake_consume_initiation(src, wg)
    B->>B: 2. 验证时间戳防重放、验证速率防洪水攻击
    B->>B: 3. 生成临时密钥对并构建 Type 2 包<br/>wg_noise_handshake_create_response(dst, &handshake)
    B->>A: 发送 Type 2 包(UDP)<br/>包结构:魔数(4) + 类型(2) + 临时公钥(32) + 加密空数据(16) + 接收者索引(4) + 发送者索引(4)

    Note over A,B: 阶段3:Alice 处理 Type 2 并建立会话
    A->>A: 1. 解析 Type 2 包并验证<br/>wg_noise_handshake_consume_response(src, wg)
    A->>A: 2. 派生双向会话密钥并开始会话<br/>wg_noise_handshake_begin_session(&handshake, &keypairs)
    A->>B: 双向加密数据传输<br/>发送:使用 keypairs->>sending 密钥加密<br/>接收:使用 keypairs->>receiving 密钥解密并验证
     B->>A: 双向加密数据传输

1. 阶段0:wg_noise_init()

// 全局 Blake2s 哈希初始化(WireGuard 模块加载时执行)
static const u8 handshake_name[37] __nonstring = "Noise_IKpsk2_25519_ChaChaPoly_BLAKE2s";
static const u8 identifier_name[34] __nonstring = "WireGuard v1 zx2c4 Jason@zx2c4.com";
static u8 handshake_init_hash[NOISE_HASH_LEN] __ro_after_init;
static u8 handshake_init_chaining_key[NOISE_HASH_LEN] __ro_after_init;

void __init wg_noise_init(void)
{
    struct blake2s_state blake;

    // 1. 生成初始 chaining key:基于 Noise 协议名称
    blake2s(handshake_init_chaining_key, handshake_name, NULL,
            NOISE_HASH_LEN, sizeof(handshake_name), 0);
    // 2. 生成初始 hash:混合 chaining key + WireGuard 标识符
    blake2s_init(&blake, NOISE_HASH_LEN);
    blake2s_update(&blake, handshake_init_chaining_key, NOISE_HASH_LEN);
    blake2s_update(&blake, identifier_name, sizeof(identifier_name));
    blake2s_final(&blake, handshake_init_hash);
}

初始化 Noise 协议的哈希上下文,为后续密钥派生提供初始熵;

2. 阶段0:wg_noise_set_static_identity_private_key()

// 设置静态私钥并生成公钥(P2P 身份核心)
void wg_noise_set_static_identity_private_key(
    struct noise_static_identity *static_identity,
    const u8 private_key[NOISE_PUBLIC_KEY_LEN])
{
    // 1. 复制私钥
    memcpy(static_identity->static_private, private_key, NOISE_PUBLIC_KEY_LEN);
    // 2. Curve25519 私钥钳制(符合规范)
    curve25519_clamp_secret(static_identity->static_private);
    // 3. 私钥生成公钥
    static_identity->has_identity = curve25519_generate_public(
        static_identity->static_public, private_key);
}

将用户配置的静态私钥转换为公钥,作为 P2P 对等体的唯一身份标识;

3. 阶段0:wg_noise_handshake_init()

// 初始化握手结构并预计算静态-静态 ECDH
void wg_noise_handshake_init(struct noise_handshake *handshake,
                              struct noise_static_identity *static_identity,
                              const u8 peer_public_key[NOISE_PUBLIC_KEY_LEN],
                              const u8 peer_preshared_key[NOISE_SYMMETRIC_KEY_LEN],
                              struct wg_peer *peer)
{
    // 1. 清零握手结构
    memset(handshake, 0, sizeof(*handshake));
    // 2. 初始化读写锁
    init_rwsem(&handshake->lock);
    // 3. 设置类型和对等体指针
    handshake->entry.type = INDEX_HASHTABLE_HANDSHAKE;
    handshake->entry.peer = peer;
    // 4. 复制对端静态公钥
    memcpy(handshake->remote_static, peer_public_key, NOISE_PUBLIC_KEY_LEN);
    // 5. 复制预共享密钥(可选)
    if (peer_preshared_key)
        memcpy(handshake->preshared_key, peer_preshared_key, NOISE_SYMMETRIC_KEY_LEN);
    // 6. 设置静态身份指针
    handshake->static_identity = static_identity;
    // 7. 初始状态:ZEROED
    handshake->state = HANDSHAKE_ZEROED;
    // 8. 预计算静态-静态 ECDH(性能优化)
    wg_noise_precompute_static_static(peer);
}

初始化握手状态,预计算静态-静态 ECDH 以提升握手速度。

4. 阶段1:wg_noise_handshake_create_initiation()

// 创建 Type 1 握手包(Alice 发给 Bob)
bool wg_noise_handshake_create_initiation(struct message_handshake_initiation *dst,
                                            struct noise_handshake *handshake)
{
    u8 timestamp[NOISE_TIMESTAMP_LEN];
    u8 key[NOISE_SYMMETRIC_KEY_LEN];
    bool ret = false;

    // 1. 等待随机数源就绪(Curve25519 需要)
    wait_for_random_bytes();
    // 2. 加锁
    down_read(&handshake->static_identity->lock);
    down_write(&handshake->lock);

    // 3. 检查静态身份是否有效
    if (unlikely(!handshake->static_identity->has_identity))
        goto out;

    // 4. 设置包类型
    dst->header.type = cpu_to_le32(MESSAGE_HANDSHAKE_INITIATION);
    // 5. 初始化 chaining key 和 hash
    handshake_init(handshake->chaining_key, handshake->hash, handshake->remote_static);

    // 6. 生成临时密钥对(e)
    curve25519_generate_secret(handshake->ephemeral_private);
    if (!curve25519_generate_public(dst->unencrypted_ephemeral, handshake->ephemeral_private))
        goto out;
    message_ephemeral(dst->unencrypted_ephemeral, dst->unencrypted_ephemeral,
                      handshake->chaining_key, handshake->hash);

    // 7. ECDH:临时私钥 + 对端静态公钥(es)
    if (!mix_dh(handshake->chaining_key, key, handshake->ephemeral_private,
                handshake->remote_static))
        goto out;

    // 8. 加密本地静态公钥(s)
    message_encrypt(dst->encrypted_static, handshake->static_identity->static_public,
                    NOISE_PUBLIC_KEY_LEN, key, handshake->hash);

    // 9. ECDH:静态私钥 + 对端静态公钥(ss,预计算)
    if (!mix_precomputed_dh(handshake->chaining_key, key,
                             handshake->precomputed_static_static))
        goto out;

    // 10. 生成并加密时间戳({t})
    tai64n_now(timestamp);
    message_encrypt(dst->encrypted_timestamp, timestamp, NOISE_TIMESTAMP_LEN,
                    key, handshake->hash);

    // 11. 插入索引哈希表
    dst->sender_index = wg_index_hashtable_insert(
        handshake->entry.peer->device->index_hashtable, &handshake->entry);
    // 12. 更新状态
    handshake->state = HANDSHAKE_CREATED_INITIATION;
    ret = true;

out:
    up_write(&handshake->lock);
    up_read(&handshake->static_identity->lock);
    memzero_explicit(key, NOISE_SYMMETRIC_KEY_LEN);
    return ret;
}

按照 Noise IKpsk2 协议构建 Type 1 握手包,包含临时公钥、加密的静态公钥和时间戳。

5. 阶段2:wg_noise_handshake_consume_initiation()

// 处理 Type 1 握手包(Bob 接收 Alice 的包)
struct wg_peer *wg_noise_handshake_consume_initiation(struct message_handshake_initiation *src,
                                                         struct wg_device *wg)
{
    struct wg_peer *peer = NULL, *ret_peer = NULL;
    struct noise_handshake *handshake;
    bool replay_attack, flood_attack;
    u8 key[NOISE_SYMMETRIC_KEY_LEN];
    u8 chaining_key[NOISE_HASH_LEN];
    u8 hash[NOISE_HASH_LEN];
    u8 s[NOISE_PUBLIC_KEY_LEN];
    u8 e[NOISE_PUBLIC_KEY_LEN];
    u8 t[NOISE_TIMESTAMP_LEN];
    u64 initiation_consumption;

    // 1. 加锁读取静态身份
    down_read(&wg->static_identity.lock);
    if (unlikely(!wg->static_identity.has_identity))
        goto out;

    // 2. 初始化 chaining key 和 hash
    handshake_init(chaining_key, hash, wg->static_identity.static_public);

    // 3. 处理临时公钥(e)
    message_ephemeral(e, src->unencrypted_ephemeral, chaining_key, hash);

    // 4. ECDH:静态私钥 + 对端临时公钥(es)
    if (!mix_dh(chaining_key, key, wg->static_identity.static_private, e))
        goto out;

    // 5. 解密对端静态公钥(s)
    if (!message_decrypt(s, src->encrypted_static, sizeof(src->encrypted_static),
                         key, hash))
        goto out;

    // 6. 查找对等体(通过静态公钥)
    peer = wg_pubkey_hashtable_lookup(wg->peer_hashtable, s);
    if (!peer)
        goto out;
    handshake = &peer->handshake;

    // 7. ECDH:静态私钥 + 对端静态公钥(ss,预计算)
    if (!mix_precomputed_dh(chaining_key, key, handshake->precomputed_static_static))
        goto out;

    // 8. 解密时间戳({t})
    if (!message_decrypt(t, src->encrypted_timestamp, sizeof(src->encrypted_timestamp),
                         key, hash))
        goto out;

    // 9. 检查重放攻击和洪水攻击
    down_read(&handshake->lock);
    replay_attack = memcmp(t, handshake->latest_timestamp, NOISE_TIMESTAMP_LEN) <= 0;
    flood_attack = (s64)handshake->last_initiation_consumption + NSEC_PER_SEC / INITIATIONS_PER_SECOND > (s64)ktime_get_coarse_boottime_ns();
    up_read(&handshake->lock);
    if (replay_attack || flood_attack)
        goto out;

    // 10. 成功!复制所有信息到对等体
    down_write(&handshake->lock);
    memcpy(handshake->remote_ephemeral, e, NOISE_PUBLIC_KEY_LEN);
    if (memcmp(t, handshake->latest_timestamp, NOISE_TIMESTAMP_LEN) > 0)
        memcpy(handshake->latest_timestamp, t, NOISE_TIMESTAMP_LEN);
    memcpy(handshake->hash, hash, NOISE_HASH_LEN);
    memcpy(handshake->chaining_key, chaining_key, NOISE_HASH_LEN);
    handshake->remote_index = src->sender_index;
    initiation_consumption = ktime_get_coarse_boottime_ns();
    if ((s64)(handshake->last_initiation_consumption - initiation_consumption) < 0)
        handshake->last_initiation_consumption = initiation_consumption;
    handshake->state = HANDSHAKE_CONSUMED_INITIATION;
    up_write(&handshake->lock);
    ret_peer = peer;

out:
    memzero_explicit(key, NOISE_SYMMETRIC_KEY_LEN);
    memzero_explicit(hash, NOISE_HASH_LEN);
    memzero_explicit(chaining_key, NOISE_HASH_LEN);
    up_read(&wg->static_identity.lock);
    if (!ret_peer)
        wg_peer_put(peer);
    return ret_peer;
}

解析 Type 1 包,验证身份,防重放/洪水攻击,更新握手状态。

6. 阶段2:wg_noise_handshake_create_response()

// 创建 Type 2 握手包(Bob 发给 Alice)
bool wg_noise_handshake_create_response(struct message_handshake_response *dst,
                                          struct noise_handshake *handshake)
{
    u8 key[NOISE_SYMMETRIC_KEY_LEN];
    bool ret = false;

    // 1. 等待随机数源就绪
    wait_for_random_bytes();
    // 2. 加锁
    down_read(&handshake->static_identity->lock);
    down_write(&handshake->lock);

    // 3. 检查状态是否正确
    if (handshake->state != HANDSHAKE_CONSUMED_INITIATION)
        goto out;

    // 4. 设置包类型和接收者索引
    dst->header.type = cpu_to_le32(MESSAGE_HANDSHAKE_RESPONSE);
    dst->receiver_index = handshake->remote_index;

    // 5. 生成临时密钥对(e)
    curve25519_generate_secret(handshake->ephemeral_private);
    if (!curve25519_generate_public(dst->unencrypted_ephemeral, handshake->ephemeral_private))
        goto out;
    message_ephemeral(dst->unencrypted_ephemeral, dst->unencrypted_ephemeral,
                      handshake->chaining_key, handshake->hash);

    // 6. ECDH:临时私钥 + 对端临时公钥(ee)
    if (!mix_dh(handshake->chaining_key, NULL, handshake->ephemeral_private,
                handshake->remote_ephemeral))
        goto out;

    // 7. ECDH:临时私钥 + 对端静态公钥(se)
    if (!mix_dh(handshake->chaining_key, NULL, handshake->ephemeral_private,
                handshake->remote_static))
        goto out;

    // 8. 混合预共享密钥(psk)
    mix_psk(handshake->chaining_key, handshake->hash, key, handshake->preshared_key);

    // 9. 加密空数据({})
    message_encrypt(dst->encrypted_nothing, NULL, 0, key, handshake->hash);

    // 10. 插入索引哈希表
    dst->sender_index = wg_index_hashtable_insert(
        handshake->entry.peer->device->index_hashtable, &handshake->entry);
    // 11. 更新状态
    handshake->state = HANDSHAKE_CREATED_RESPONSE;
    ret = true;

out:
    up_write(&handshake->lock);
    up_read(&handshake->static_identity->lock);
    memzero_explicit(key, NOISE_SYMMETRIC_KEY_LEN);
    return ret;
}
  • 核心作用:按照 Noise IKpsk2 协议构建 Type 2 握手包,包含临时公钥和加密的空数据。

7. 阶段3:wg_noise_handshake_consume_response()

// 处理 Type 2 握手包(Alice 接收 Bob 的包)
struct wg_peer *wg_noise_handshake_consume_response(struct message_handshake_response *src,
                                                       struct wg_device *wg)
{
    enum noise_handshake_state state = HANDSHAKE_ZEROED;
    struct wg_peer *peer = NULL, *ret_peer = NULL;
    struct noise_handshake *handshake;
    u8 key[NOISE_SYMMETRIC_KEY_LEN];
    u8 hash[NOISE_HASH_LEN];
    u8 chaining_key[NOISE_HASH_LEN];
    u8 e[NOISE_PUBLIC_KEY_LEN];
    u8 ephemeral_private[NOISE_PUBLIC_KEY_LEN];
    u8 preshared_key[NOISE_SYMMETRIC_KEY_LEN];

    // 1. 加锁读取静态身份
    down_read(&wg->static_identity.lock);
    if (unlikely(!wg->static_identity.has_identity))
        goto out;

    // 2. 查找握手结构(通过接收者索引)
    handshake = (struct noise_handshake *)wg_index_hashtable_lookup(
        wg->index_hashtable, INDEX_HASHTABLE_HANDSHAKE, src->receiver_index, &peer);
    if (unlikely(!handshake))
        goto out;

    // 3. 读取握手状态和临时数据
    down_read(&handshake->lock);
    state = handshake->state;
    memcpy(hash, handshake->hash, NOISE_HASH_LEN);
    memcpy(chaining_key, handshake->chaining_key, NOISE_HASH_LEN);
    memcpy(ephemeral_private, handshake->ephemeral_private, NOISE_PUBLIC_KEY_LEN);
    memcpy(preshared_key, handshake->preshared_key, NOISE_SYMMETRIC_KEY_LEN);
    up_read(&handshake->lock);

    // 4. 检查状态是否正确
    if (state != HANDSHAKE_CREATED_INITIATION)
        goto fail;

    // 5. 处理临时公钥(e)
    message_ephemeral(e, src->unencrypted_ephemeral, chaining_key, hash);

    // 6. ECDH:临时私钥 + 对端临时公钥(ee)
    if (!mix_dh(chaining_key, NULL, ephemeral_private, e))
        goto fail;

    // 7. ECDH:静态私钥 + 对端临时公钥(se)
    if (!mix_dh(chaining_key, NULL, wg->static_identity.static_private, e))
        goto fail;

    // 8. 混合预共享密钥(psk)
    mix_psk(chaining_key, hash, key, preshared_key);

    // 9. 解密空数据({})
    if (!message_decrypt(NULL, src->encrypted_nothing, sizeof(src->encrypted_nothing),
                         key, hash))
        goto fail;

    // 10. 成功!复制所有信息到对等体
    down_write(&handshake->lock);
    // 检查状态是否仍然正确(加锁后)
    if (handshake->state != state) {
        up_write(&handshake->lock);
        goto fail;
    }
    memcpy(handshake->remote_ephemeral, e, NOISE_PUBLIC_KEY_LEN);
    memcpy(handshake->hash, hash, NOISE_HASH_LEN);
    memcpy(handshake->chaining_key, chaining_key, NOISE_HASH_LEN);
    handshake->remote_index = src->sender_index;
    handshake->state = HANDSHAKE_CONSUMED_RESPONSE;
    up_write(&handshake->lock);
    ret_peer = peer;
    goto out;

fail:
    wg_peer_put(peer);
out:
    memzero_explicit(key, NOISE_SYMMETRIC_KEY_LEN);
    memzero_explicit(hash, NOISE_HASH_LEN);
    memzero_explicit(chaining_key, NOISE_HASH_LEN);
    memzero_explicit(ephemeral_private, NOISE_PUBLIC_KEY_LEN);
    memzero_explicit(preshared_key, NOISE_SYMMETRIC_KEY_LEN);
    up_read(&wg->static_identity.lock);
    return ret_peer;
}
  • 核心作用:解析 Type 2 包,验证身份,更新握手状态。

8. 阶段3:wg_noise_handshake_begin_session()

// 开始会话,派生双向密钥
bool wg_noise_handshake_begin_session(struct noise_handshake *handshake,
                                        struct noise_keypairs *keypairs)
{
    struct noise_keypair *new_keypair;
    bool ret = false;

    // 1. 加锁
    down_write(&handshake->lock);

    // 2. 检查状态是否正确
    if (handshake->state != HANDSHAKE_CREATED_RESPONSE &&
        handshake->state != HANDSHAKE_CONSUMED_RESPONSE)
        goto out;

    // 3. 创建新的密钥对
    new_keypair = keypair_create(handshake->entry.peer);
    if (!new_keypair)
        goto out;

    // 4. 设置发起方标志和远程索引
    new_keypair->i_am_the_initiator = handshake->state == HANDSHAKE_CONSUMED_RESPONSE;
    new_keypair->remote_index = handshake->remote_index;

    // 5. 派生双向密钥
    if (new_keypair->i_am_the_initiator)
        derive_keys(&new_keypair->sending, &new_keypair->receiving, handshake->chaining_key);
    else
        derive_keys(&new_keypair->receiving, &new_keypair->sending, handshake->chaining_key);

    // 6. 清零握手结构
    handshake_zero(handshake);

    // 7. 检查对等体是否已死亡
    rcu_read_lock_bh();
    if (likely(!READ_ONCE(container_of(handshake, struct wg_peer, handshake)->is_dead))) {
        // 8. 添加新密钥对
        add_new_keypair(keypairs, new_keypair);
        net_dbg_ratelimited("%s: Keypair %llu created for peer %llu\n",
                            handshake->entry.peer->device->dev->name,
                            new_keypair->internal_id,
                            handshake->entry.peer->internal_id);
        // 9. 替换索引哈希表中的条目
        ret = wg_index_hashtable_replace(
            handshake->entry.peer->device->index_hashtable,
            &handshake->entry, &new_keypair->entry);
    } else {
        kfree_sensitive(new_keypair);
    }
    rcu_read_unlock_bh();

out:
    up_write(&handshake->lock);
    return ret;
}

派生双向会话密钥,创建密钥对结构,开始加密通信。

握手完成后的UDP数据处理

握手完成后,WireGuard 隧道的所有 UDP 传输包(除极特殊的防 DDoS Cookie 应答包外),均采用 ChaCha20-Poly1305 AEAD 算法进行全量加密+完整性认证,不存在明文传输的业务数据、控制包或隧道元数据,仅保留3个必要的明文字段用于快速路由和密钥匹配,且明文字段会被同步认证防篡改。

握手完成后,WireGuard 不再发送 Type1/2 握手包,所有隧道流量仅使用 Type3 数据报文(对应messages.h中定义的MESSAGE_DATA类型),包结构严格遵循内核源码定义,分为明文头部加密载荷+认证标签两部分:

字段 长度 明文/密文 作用
魔数(Magic) 4字节 明文 WireGuard 包标识,固定值,用于快速过滤非 WireGuard 包
包类型 4字节 明文 固定为3,标识为数据传输包
接收者索引 4字节 明文 对应对等体密钥对的唯一ID,接收方无需解密即可快速定位密钥
加密载荷 可变长度 密文 原始三层IP包(用户的UDP/TCP/ICMP等所有业务流量)
Poly1305 认证标签 16字节 密文附属 AEAD 算法生成的完整性校验值,防篡改、防伪造

发送侧:从TUN包到加密UDP包的全流程(对应内核源码)

https://github.com/WireGuard/wireguard-linux/blob/stable/drivers/net/wireguard/send.c

整个流程完全在内核态完成,无用户态-内核态切换,核心逻辑对应send.c+noise.c源码,步骤如下:

1. 流量捕获与对等体匹配

  • 内核将发往wg0虚拟网卡的原始IP包(无论UDP/TCP/ICMP),通过ndo_start_xmit钩子送入 WireGuard 驱动;
  • 通过allowedips.c的二叉特里树,O(1)匹配到目标对等体,获取对等体当前有效的会话密钥对struct noise_keypair,来自握手完成后wg_noise_handshake_begin_session派生的结果)。

2. 加密核心准备(对应noise.h密钥对结构)

// 你提供的noise.h中定义的密钥对核心结构
struct noise_keypair {
    struct noise_symmetric_key sending;    // 发送用对称密钥(握手派生)
    atomic64_t sending_counter;            // 64位原子递增计数器(nonce)
    struct noise_symmetric_key receiving;  // 接收用对称密钥
    struct noise_replay_counter receiving_counter; // 接收端重放保护窗口
    __le32 remote_index;                   // 对端索引(明文头部的接收者索引)
    bool i_am_the_initiator;               // 标记是否为握手发起方
};
  • 原子递增sending_counter,生成全局唯一的96位nonce(WireGuard 固定用64位计数器+32位0填充,符合ChaCha20-Poly1305规范),彻底避免nonce重用导致的加密安全问题;
  • 校验密钥有效性:若密钥已过期,触发自动重协商,拒绝发送明文包。

3. AEAD 全量加密(对应noise.c底层算法)

  • 调用内核chacha20poly1305_encrypt接口(noise.cmessage_encrypt/message_decrypt复用的同一套算法),执行加密:
  • 用ChaCha20流加密算法,基于发送密钥+唯一nonce,加密原始IP包载荷;
  • 用Poly1305算法,基于同一密钥,对「明文头部+加密载荷」生成16字节认证标签;
  • 封装为完整的Type3包,填入明文头部、加密载荷、认证标签。

4. UDP发送

  • 直接通过内核UDP套接字,将加密后的Type3包发送到对等体的公网端点(自动更新的NAT穿透地址);
  • 全程无内存拷贝,直接操作内核sk_buff缓冲区,极致优化转发性能。

接收侧:从UDP包到TUN注入的全流程(对应内核源码)

https://github.com/WireGuard/wireguard-linux/blob/stable/drivers/net/wireguard/receive.c

核心逻辑对应receive.c+noise.c源码,全程在内核态完成,先验签、后解密,验签失败直接丢弃,绝不处理非法包,步骤如下:

1. 包接收与快速路由

  • 从内核UDP套接字收到包,先校验魔数和包类型,非Type3包直接分流到握手/防DDoS逻辑;
  • 通过明文的「接收者索引」,从index_hashtable中O(1)查找到对应的对等体和密钥对,无需解密即可完成路由,性能极致。

2. 防重放校验(对应noise.h重放计数器结构)

// 你提供的noise.h中定义的重放保护结构
struct noise_replay_counter {
    u64 counter;
    spinlock_t lock;
    unsigned long backtrack[COUNTER_BITS_TOTAL / BITS_PER_LONG]; // 滑动窗口bitmap
};
  • 校验包中的计数器:若计数器小于窗口最小值,或已在bitmap中标记为已接收,直接判定为重放攻击,丢弃包;
  • 校验通过后,更新滑动窗口bitmap,标记该计数器为已接收,彻底杜绝重放攻击。

3. AEAD 解密与完整性校验

  • 调用内核chacha20poly1305_decrypt接口,先执行Poly1305验签
  • 基于接收密钥+包中nonce,对「明文头部+加密载荷」重新生成认证标签,与包中标签比对;
  • 验签失败直接丢弃包,绝不执行解密操作,抵御无效包的CPU消耗攻击;
  • 验签通过后,用ChaCha20算法解密载荷,得到原始的三层IP包。

4. 流量注入与密钥状态更新

  • 对解密后的IP包做合法性校验,通过后注入TUN虚拟网卡,交由内核网络栈后续处理;
  • 若该包是next_keypair的第一个有效包,调用wg_noise_received_with_keypair(你提供的noise.c原生函数),将新密钥升级为current_keypair,完成平滑密钥轮换。

特殊场景的加密处理

1. NAT保活包/空包

WireGuard 为维持NAT会话发送的空保活包,同样会经过完整的ChaCha20-Poly1305 AEAD加密,哪怕载荷长度为0,也会生成16字节的认证标签,不存在明文保活包,不会泄露隧道状态。

2. 防DDoS Cookie应答包(Type4)

这是握手完成后唯一的非Type3包,仅在受到UDP反射/DDoS攻击时触发: - 包中仅带防DDoS的Cookie认证信息,不带任何隧道业务数据、密钥信息或敏感内容; - 不加密,但带BLAKE2s强认证,仅用于验证发送方合法性,不会泄露任何隧道相关信息; - 正常通信场景下绝不会发送该类型包。

3. 密钥自动重协商

WireGuard 会定期(默认每2分钟)触发密钥重协商,重协商的Type1/2握手包,与初始握手逻辑完全一致:静态公钥、时间戳等敏感信息全程加密,无明文泄露。

WireGuard 加密算法体系详解

WireGuard 固定使用5类核心密码学原语,无任何可协商的算法选项,彻底杜绝算法降级攻击、错误配置导致的安全漏洞: | 算法类型 | 核心算法 | 安全强度 | 核心用途 | |----------|----------|----------|----------| | 非对称密钥交换 | Curve25519(X25519) | 128位 | 身份认证、ECDH密钥协商、前向保密 | | 对称加密+认证 | ChaCha20-Poly1305 AEAD | 256位密钥/128位安全 | 全量流量加密、完整性校验、防篡改 | | 加密哈希 | BLAKE2s | 256位输出/128位抗碰撞 | 哈希链、HMAC、密钥派生、防DDoS | | 密钥派生 | HKDF(基于BLAKE2s) | 128位 | 会话密钥派生、多轮熵混合 | | 后量子加固 | 可选256位PSK预共享密钥 | 256位 | 抗量子攻击、双重身份认证 |


1. Curve25519(X25519):非对称密钥交换与身份核心

Curve25519 是 Daniel J. Bernstein 设计的椭圆曲线 Diffie-Hellman(ECDH)密钥交换算法,RFC7748 标准化,是 WireGuard 整个加密体系的信任根。

源码对应位置(noise.c
  • 静态密钥生成:wg_noise_set_static_identity_private_key() 中通过 curve25519_generate_public() 从私钥派生公钥;
  • 临时密钥生成:wg_noise_handshake_create_initiation()/wg_noise_handshake_create_response()curve25519_generate_secret() 生成单次握手的临时密钥;
  • ECDH 密钥协商:mix_dh()/wg_noise_precompute_static_static()curve25519() 函数完成核心的密钥交换计算。
核心用途
  1. 身份标识:每个对等体的唯一身份是 32 字节的 Curve25519 静态公钥,替代传统 VPN 的证书体系,配置极简、无证书过期/吊销问题;
  2. 密钥协商:握手阶段通过多轮 ECDH 计算(静态-静态、临时-静态、临时-临时),生成会话密钥的种子;
  3. 前向保密:每次握手都会生成全新的临时 Curve25519 密钥对,用完即销毁;即使长期静态密钥泄露,历史会话流量也无法解密。
设计优势(对比 RSA/NIST P-256)
  • 极致性能:同等安全强度下,计算量仅为 RSA2048 的 1/100,无大整数运算开销,嵌入式路由器/手机等弱算力设备也能轻松跑满带宽;
  • 抗侧信道攻击:全程常数时间实现,无分支判断,彻底规避时序攻击、缓存攻击等侧信道风险;
  • 无潜在后门:曲线参数完全公开、可验证,规避了 NIST 系列曲线的参数不透明问题;
  • 极简轻量:32 字节公钥/私钥,仅为 RSA2048 公钥长度的 1/8,包体积更小、配置更简单。

2. ChaCha20-Poly1305 AEAD:对称加密与完整性认证核心

ChaCha20-Poly1305 是 RFC8439 标准化的带关联数据的认证加密(AEAD) 算法,由 ChaCha20 流加密算法 + Poly1305 消息认证码组合而成,是 TLS 1.3 的标配算法,也是 WireGuard 唯一的对称加密算法。

源码对应位置(noise.c
  • 握手阶段加密:message_encrypt()/message_decrypt() 中完成静态公钥、时间戳的加解密与认证;
  • 数据流量加密:send.c/receive.c 中复用同一套算法,完成所有 Type3 数据报文的加解密;
  • 底层实现:直接调用 Linux 内核的 chacha20poly1305_encrypt()/chacha20poly1305_decrypt() 接口。
核心用途
  1. 全量流量加密:握手完成后,所有隧道内的 IP 包(UDP/TCP/ICMP 等)全部通过 ChaCha20 加密,无任何明文业务数据;
  2. 完整性与真实性认证:Poly1305 为每个包生成 16 字节的认证标签,先验签、后解密,验签失败的包直接丢弃,彻底杜绝篡改、伪造、重放攻击;
  3. 明文头部保护:包的明文字段(魔数、类型、接收者索引)会被纳入 AEAD 的「关联数据(AD)」进行认证,即使明文字段被篡改,也会直接验签失败。
设计优势(对比 AES-GCM)
  • 无硬件依赖,全场景高性能:纯软件实现,在无 AES-NI 硬件加速的设备(嵌入式路由器、手机、低功耗开发板)上,性能比 AES-GCM 高 3-5 倍;有硬件加速的场景下,性能也与 AES-GCM 持平;
  • 抗侧信道攻击:流加密设计,无 S 盒查表操作,全程常数时间实现,规避了 AES 常见的缓存时序攻击;
  • AEAD 一体设计:加密与认证在一个原子步骤完成,彻底避免了「先加密后认证」的经典安全错误(如 OpenSSL 历史漏洞);
  • Nonce 安全设计:WireGuard 固定使用 64 位原子递增计数器作为 Nonce 核心,保证每个包的 Nonce 全局唯一,彻底杜绝 Nonce 重用导致的加密密钥泄露风险。

3. BLAKE2s:加密哈希函数核心

BLAKE2s 是 Daniel J. Bernstein 设计的加密哈希函数,RFC7693 标准化,是 NIST SHA-3 竞赛的决赛入围算法,256 位输出,提供 128 位抗碰撞安全强度。

源码对应位置(noise.c
  • 全局初始化:wg_noise_init() 中通过 BLAKE2s 生成 Noise 协议的初始哈希链与 chaining key;
  • 哈希链更新:mix_hash() 函数完成握手阶段的哈希状态迭代;
  • 底层支撑:hmac()/kdf() 函数的核心均基于 BLAKE2s 实现;
  • 防DDoS:Cookie 生成与验证的核心哈希算法。
核心用途
  1. Noise 协议哈希链:维护握手过程中的哈希状态,保证握手消息的完整性,防止握手包被篡改;
  2. HMAC 实现:作为 HKDF 密钥派生、Cookie 认证的底层哈希函数;
  3. 协议绑定:初始哈希值绑定了 WireGuard 协议标识与 Noise 协议名称,防止跨协议攻击。
设计优势(对比 SHA-256/SHA-3)
  • 极致性能:在 32 位/64 位设备上,软件实现性能均远超 SHA-256,尤其适合 Linux 内核态的轻量实现;
  • 安全冗余:通过了 NIST 严格的密码学分析,无任何已知安全漏洞,抗碰撞、抗第二原像攻击能力与 SHA-256 完全对齐;
  • 功能灵活:原生支持密钥化哈希,无需额外封装 HMAC,代码量更小、实现更简洁。

4. HKDF:密钥派生函数

HKDF(HMAC-based Extract-and-Expand Key Derivation Function)是 RFC5869 标准化的密钥派生函数,WireGuard 基于 BLAKE2s-HMAC 实现,是连接 ECDH 密钥协商与对称加密的核心桥梁。

源码对应位置(noise.c
  • 核心实现:kdf() 函数完整实现了 HKDF 的 Extract 与 Expand 阶段;
  • 密钥混合:mix_dh() 中通过 HKDF 混合 ECDH 结果,更新 chaining key;
  • 会话密钥派生:derive_keys() 中通过 HKDF 从最终 chaining key 派生出双向独立的发送/接收密钥。
核心用途
  1. 熵提取:从多轮 ECDH 计算的结果中提取均匀的密码学熵,即使 ECDH 结果存在少量熵不足,也能生成安全的密钥种子;
  2. 密钥分离:从单一的 chaining key 派生出完全独立的发送密钥、接收密钥,保证双向流量的加密密钥互不影响,单向密钥泄露不会导致全量流量解密;
  3. 密钥扩展:按需扩展出符合加密算法要求的密钥长度,避免密钥复用导致的安全风险。
设计优势
  • 标准化实现:采用业界通用的密钥派生标准,彻底规避了自定义密钥派生的常见安全漏洞;
  • 分层安全:Extract 与 Expand 两个阶段分离,先保证熵的安全性,再扩展出可用密钥,密码学设计严谨;
  • 轻量高效:基于 BLAKE2s 实现,代码量不足 50 行,内核态运行开销极低。

5. 可选 PSK 预共享密钥:后量子安全加固

WireGuard 支持可选配置 256 位预共享密钥(PSK),对应 Noise_IKpsk2 协议中的 psk 环节,是针对量子计算攻击的额外加固层。

源码对应位置(noise.c
  • 初始化:wg_noise_handshake_init() 中复制对等体的 PSK 到握手结构;
  • 密钥混合:mix_psk() 函数完成 PSK 与 chaining key 的混合,派生最终的加密密钥;
  • 握手流程:wg_noise_handshake_create_response()/wg_noise_handshake_consume_response() 中调用 PSK 混合逻辑。
核心用途
  1. 后量子安全:即使未来 Curve25519 椭圆曲线被量子计算机破解,只要 PSK 不泄露,历史会话与新建会话的流量都无法解密;
  2. 双重身份认证:在静态公钥的基础上,额外增加一层对称密钥认证,只有同时持有正确公钥和 PSK 的对等体才能完成握手。

三、辅助安全机制(配套算法实现)

  1. TAI64N 时间戳noise.ctai64n_now() 函数生成,握手阶段加密传输,用于防止握手包的重放攻击,保证每个握手包的唯一性;
  2. 滑动窗口重放保护noise.hnoise_replay_counter 结构实现,基于 bitmap 滑动窗口,防止数据报文的重放攻击,64 位计数器保证每个包的序号唯一,无重复、无遗漏。

ubuntu无法拉docker镜像问题处理

ubuntu24

几次尝试:

  1. 第一次报错信息如下:

yeshen in ~ λ sudo docker pull budtmo/docker-android:emulator_9.0 Error response from daemon: Get "https://registry-1.docker.io/v2/": read tcp 10.249.51.62:53240->98.85.153.80:443: read: connection reset by peer

参考这个问答 https://stackoverflow.com/a/60224720/8305531

dig registry-1.docker.io +short
sudo vim  /etc/hosts

增加一行配置:3.94.224.37 registry-1.docker.io

  1. 报错信息变了,变成如下

yeshen in ~ λ docker pull budtmo/docker-android:emulator_9.0 Error response from daemon: Head "https://registry-1.docker.io/v2/budtmo/docker-android/manifests/emulator_9.0": Get "https://auth.docker.io/token?account=xxx&scope=repository%3Abudtmo%2Fdocker-android%3Apull&service=registry.docker.io": read tcp 10.249.51.62:40916->98.85.153.80:443: read: connection reset by peer

dig auth.docker.io +short
sudo vim  /etc/hosts

增加一行配置:3.94.224.37 auth.docker.io

  1. ok了
yeshen in ~ λ  docker pull budtmo/docker-android:emulator_9.0
emulator_9.0: Pulling from budtmo/docker-android
32f112e3802c: Pull complete 
fd0320870767: Pull complete 
925dd4312eba: Pull complete 
a631c95f4102: Pull complete 
4f4fb700ef54: Pull complete 
9e03ab21c06c: Pull complete 
cedb1a91467d: Pull complete 
a7e5b4ee501c: Pull complete 
5611da61254c: Pull complete 
96c174eaf3cf: Pull complete 
eda8c3879a49: Pull complete 
a1d0aa39e0aa: Pull complete 
e8b559d6233a: Pull complete 
94e4b2ac8c29: Pull complete 
83d204066bc6: Pull complete 
45e889d79f06: Pull complete 
8becf497a696: Pull complete 
142471240580: Pull complete 
6c36f43ca238: Pull complete 
77e34cc9b342: Pull complete 
6d2d0b59e62a: Pull complete 
486f942bc895: Pull complete 
104f2cab59f8: Pull complete 
19a38a23cdc8: Pull complete 
e9a752c6462b: Pull complete 
6f259ed1c92a: Pull complete 
Digest: sha256:9eab37ade5b13b59494043e7720216358515459fe7e11c9e4d75bc3f1707dc9b
Status: Downloaded newer image for budtmo/docker-android:emulator_9.0
docker.io/budtmo/docker-android:emulator_9.0

原因分析:

  1. DNS 污染/劫持
  2. 当执行 docker pull 时,客户端会向 DNS 服务器查询 registry-1.docker.io 的 IP 地址。
  3. 如果本地 DNS 服务器(或网络中的中间设备)返回了错误的 IP 地址(例如被污染的地址),Docker 客户端会尝试连接到一个无效的服务器。
  4. 目标服务器可能直接拒绝连接或重置(connection reset by peer),导致拉取失败。

  5. 修改 hosts 为何有效?

  6. 修改 hosts 文件是手动将域名 registry-1.docker.io 绑定到正确的 IP 地址,绕过了 DNS 查询步骤。
  7. 这证明原始 DNS 解析结果有问题,而直接指定 IP 后连接成功,进一步确认是 DNS 问题。

解决方案:

1. 永久修复 DNS 问题
  • 更换公共 DNS 服务(推荐): ```bash # 临时测试(Linux/Mac) sudo echo "nameserver 8.8.8.8" >> /etc/resolv.conf

    # 永久生效(根据系统配置网络) # 例如 Ubuntu:修改 /etc/systemd/resolved.conf 或 netplan 配置 `` 推荐 DNS 服务器: -8.8.8.8(Google DNS) -1.1.1.1(Cloudflare DNS) -208.67.222.222`(OpenDNS)

  • 刷新 DNS 缓存: ```bash # Linux sudo systemd-resolve --flush-caches

    # MacOS sudo killall -HUP mDNSResponder

    # Windows ipconfig /flushdns ```

2. 检查 hosts 文件
  • 确保 hosts 中指定的 IP 是 Docker Hub 的最新有效 IP(IP 可能变化): bash # 获取 registry-1.docker.io 的最新 IP nslookup registry-1.docker.io 8.8.8.8 将输出中的正确 IP 写入 /etc/hosts(Linux/Mac)或 C:\Windows\System32\drivers\etc\hosts(Windows): 54.192.130.39 registry-1.docker.io
3. 检查网络环境
  • 关闭 VPN/代理:某些 VPN 或代理可能导致 TLS 连接被重置。
  • 防火墙规则:确保允许出站 443 端口(HTTPS)。
  • 企业网络限制:联系 IT 部门确认是否拦截了 Docker Hub。
4. 配置 Docker 使用 HTTP 代理

如果问题由中间网络设备引起,可通过代理访问: bash # 编辑 Docker 服务配置 sudo mkdir -p /etc/systemd/system/docker.service.d sudo nano /etc/systemd/system/docker.service.d/http-proxy.conf 添加: ini [Service] Environment="HTTP_PROXY=http://proxy-ip:port" Environment="HTTPS_PROXY=http://proxy-ip:port" 重启 Docker: bash sudo systemctl daemon-reload sudo systemctl restart docker


总结

  • 根本原因:DNS 污染/劫持导致 registry-1.docker.io 解析到错误 IP,触发连接重置。
  • 解决优先级
    更换 DNS → 检查 hosts → 关闭代理 → 配置 HTTP 代理
    建议优先使用公共 DNS(如 8.8.8.8),这是最稳定的解决方案。

Toyvpn详解

Android中可以使用提供了vpn服务,通过VPN builder,可以创建一个网络接口(https://developer.android.google.cn/reference/android/net/VpnService.Builder#establish()),拦截特定或者部分app的流量。

上面的文档提供了比较详细的说明,还有一份服务端+客户端的demo代码。

https://developer.android.google.cn/develop/connectivity/vpn?hl=zh-cn#service https://android.googlesource.com/platform/development/+/master/samples/ToyVpn

服务端部分

There are several ways to play with this program. Here we just give an example for the simplest scenario. Let us say that a Linux box has a public IPv4 address on eth0. Please try the following steps and adjust the parameters when necessary.

Enable IP forwarding

echo 1 > /proc/sys/net/ipv4/ip_forward

作用:允许Linux系统转发IP数据包(作为路由器功能)。 原理:将内核参数 net.ipv4.ip_forward 设为 1,使流量能在不同网络接口间传递。

Pick a range of private addresses and perform NAT over eth0.

iptables -t nat -A POSTROUTING -s 10.0.0.0/8 -o eth0 -j MASQUERADE

作用:将来自私有网络 10.0.0.0/8 的流量通过公网接口 eth0 进行NAT转换(隐藏客户端真实IP)。 原理:MASQUERADE 规则会动态修改数据包源地址为 eth0 的公网IP,使返回流量能正确路由。

Create a TUN interface.

ip tuntap add dev tun0 mode tun

作用:创建一个名为 tun0 的TUN模式虚拟接口,用于处理VPN隧道的三层(IP层)数据包。 TUN特点:传输原始IP数据包,需用户态程序(如 ToyVpnServer)处理封包逻辑。

Set the addresses and bring up the interface.

ifconfig tun0 10.0.0.1 dstaddr 10.0.0.2 up

作用: 为 tun0 设置本地IP 10.0.0.1,并指定对端(客户端)IP为 10.0.0.2。 up 表示启用接口。 关键参数: dstaddr 定义隧道的目标地址(客户端IP),需与后续服务器命令中的 -a 参数一致。

Create a server on port 8000 with shared secret "test".

./ToyVpnServer tun0 8000 test -m 1400 -a 10.0.0.2 32 -d 8.8.8.8 -r 0.0.0.0 0

参数解析: tun0:绑定到TUN接口。 8000:监听UDP端口8000。 test:共享密钥(明文,无加密)。 -m 1400:设置MTU为1400字节(避免分片)。 -a 10.0.0.2 32:为客户端分配IP 10.0.0.2,子网掩码32位(单地址)。 -d 8.8.8.8:指定客户端DNS服务器为Google DNS。 -r 0.0.0.0 0:添加默认路由(0.0.0.0/0),将所有流量导向VPN隧道。

This program only handles a session at a time. To allow multiple sessions, multiple servers can be created on the same port, but each of them requires its own TUN interface. A short shell script will be sufficient. Since this program is designed for demonstration purpose, it performs neither strong authentication nor encryption. DO NOT USE IT IN PRODUCTION!

该程序一次只能处理一个连接。若需多会话,需为每个客户端创建独立的TUN接口(如 tun1, tun2)并启动多个服务器进程。 无加密:共享密钥 test 以明文传输,易被窃听或篡改。 无身份验证:仅依赖简单密钥,无法防止中间人攻击。 仅限测试:注释明确警告不可用于实际生产环境。

相关代码

// int tun_fd = get_interface("tun0");
// 执行后,系统中会出现 tun0 接口(可通过 ip addr show tun0 查看)。
static int get_interface(char *name)
{

    // **/dev/net/tun**:Linux内核提供的TUN/TAP设备文件,用于创建虚拟网络接口。
    // **O_RDWR**:以读写模式打开设备。
    // **O_NONBLOCK**:设为非阻塞模式(后续读写操作不会阻塞进程)。
    // 返回值:文件描述符 interface,用于后续操作(如读写数据包)。
    // 初始化接口请求结构体 
    int interface = open("/dev/net/tun", O_RDWR | O_NONBLOCK);

    // **ifreq**:用于配置网络接口参数的结构体(定义在 <net/if.h>)
    ifreq ifr;
    memset(&ifr, 0, sizeof(ifr));

    // **IFF_TUN**:创建TUN设备(处理IP层数据包,对应三层网络层)。
    // **IFF_NO_PI**:禁用包头信息(Packet Information),直接传输原始IP数据包。
    // 若未设置此标志,数据包会附加4字节的包头(如协议类型)。
    ifr.ifr_flags = IFF_TUN | IFF_NO_PI;

    // 将用户传入的接口名称(如 "tun0")复制到 ifr.ifr_name。
    // 注意:strncpy 不会自动补全终止符 \0,需确保 name 长度不超过 IFNAMSIZ(通常为16字节)。
    strncpy(ifr.ifr_name, name, sizeof(ifr.ifr_name));

    // **ioctl**:通过 TUNSETIFF 命令将配置应用到TUN设备。
    // 成功:内核会创建名为 name 的TUN接口(如 tun0)。
    // 失败:输出错误信息并终止程序(如权限不足或名称冲突)。
    // 返回文件描述符  
    if (ioctl(interface, TUNSETIFF, &ifr)) {
        perror("Cannot get TUN interface");
        exit(1);
    }

    return interface;
}
// 创建一个UDP服务器,监听指定端口,接收数据包直到收到一个符合特定格式(首字节为0,后面是正确密钥)的数据包,
// 然后与该客户端建立连接,返回socket描述符以供后续通信。
// 这个函数用于在VPN服务器中建立初始的隧道连接,确保客户端提供正确的密钥,并只处理一个客户端。
// port:监听的UDP端口。
// secret:预共享密钥,用于验证客户端。
// 返回值:成功返回连接后的socket文件描述符,失败返回 -1。
static int get_tunnel(char *port, char *secret)
{
    // We use an IPv6 socket to cover both IPv4 and IPv6.
    // 使用 IPv6协议族 (AF_INET6) 创建UDP Socket (SOCK_DGRAM)。
    // 目的:通过IPv6双栈支持,同时监听IPv4和IPv6客户端(需系统支持)。
    // IPv4兼容性依赖系统配置,需确保系统支持IPv4映射地址(sysctl net.ipv6.bindv6only=0)。
    int tunnel = socket(AF_INET6, SOCK_DGRAM, 0);
    int flag = 1;
    // **SO_REUSEADDR**:允许绑定处于 TIME_WAIT 状态的地址,快速重启服务。
    // **IPV6_V6ONLY=0**:允许Socket接收IPv4映射的IPv6地址(如 ::ffff:192.168.1.1)。
    setsockopt(tunnel, SOL_SOCKET, SO_REUSEADDR, &flag, sizeof(flag));
    flag = 0;
    setsockopt(tunnel, IPPROTO_IPV6, IPV6_V6ONLY, &flag, sizeof(flag));

    // Accept packets received on any local address.
    sockaddr_in6 addr;
    memset(&addr, 0, sizeof(addr));
    addr.sin6_family = AF_INET6;
    addr.sin6_port = htons(atoi(port));

    // Call bind(2) in a loop since Linux does not have SO_REUSEPORT.
    // 循环绑定:若端口被占用 (EADDRINUSE),等待0.1秒后重试,直至成功。
    while (bind(tunnel, (sockaddr *)&addr, sizeof(addr))) {
        if (errno != EADDRINUSE) {
            return -1;
        }
        usleep(100000);
    }

    // Receive packets till the secret matches.
    char packet[1024];
    socklen_t addrlen;
    // 数据包格式:首个字节为 0,后续为密钥字符串(如 \0test)。
    // 验证逻辑:
    // 若首字节非 0 或密钥不匹配,继续接收新数据包。
    // 密钥匹配后退出循环。
    do {
        addrlen = sizeof(addr);
        int n = recvfrom(tunnel, packet, sizeof(packet), 0,
                (sockaddr *)&addr, &addrlen);
        if (n <= 0) {
            return -1;
        }
        packet[n] = 0;
    } while (packet[0] != 0 || strcmp(secret, &packet[1]));

    // Connect to the client as we only handle one client at a time.
    // 目的:将UDP Socket与客户端地址绑定,后续通信可使用 send/recv 替代 sendto/recvfrom。
    // 影响:该Socket仅处理来自此客户端的流量,实现单会话独占。
    connect(tunnel, (sockaddr *)&addr, addrlen);
    return tunnel;
}

// Get TUN interface.
// TUN设备用于与内核网络栈交互(如接收/发送IP层数据包)
int interface = get_interface(argv[1]);

// Wait for a tunnel.
int tunnel;
// get_tunnel 创建UDP服务端,监听指定端口,验证客户端密钥,返回已连接的socket描述符 tunnel。
// 单客户端限制:每个循环仅处理一个客户端,断开后才接受下一个连接。
while ((tunnel = get_tunnel(argv[2], argv[3])) != -1) {
    printf("%s: Here comes a new tunnel\n", argv[1]);

    // On UN*X, there are many ways to deal with multiple file
    // descriptors, such as poll(2), select(2), epoll(7) on Linux,
    // kqueue(2) on FreeBSD, pthread(3), or even fork(2). Here we
    // mimic everything from the client, so their source code can
    // be easily compared side by side.

    // Put the tunnel into non-blocking mode.
    // 确保 send/recv 不会阻塞进程,允许轮询多个文件描述符(尽管此处未实现多路复用)。
    fcntl(tunnel, F_SETFL, O_NONBLOCK);

    // Send the parameters several times in case of packet loss.
    // 向客户端重复发送3次 parameters。
    // 目的:应对UDP丢包,提高参数传递可靠性。
    for (int i = 0; i < 3; ++i) {
        send(tunnel, parameters, sizeof(parameters), MSG_NOSIGNAL);
    }

    // Allocate the buffer for a single packet.
    // // 缓冲区(最大支持32KB数据包)
    char packet[32767];

    // We use a timer to determine the status of the tunnel. It
    // works on both sides. A positive value means sending, and
    // any other means receiving. We start with receiving.
     // 状态计时器(正=发送,负=接收)
    int timer = 0;

    // We keep forwarding packets till something goes wrong.
    // 优势:单线程处理双向流量,代码简洁。
    // 劣势:高负载下频繁休眠可能导致延迟;未使用 poll/epoll 等多路复用机制,效率较低。
    while (true) {
        // Assume that we did not make any progress in this iteration.
        bool idle = true;

        // Read the outgoing packet from the input stream.
        // TUN → 隧道:读取内核发出的IP数据包(如来自客户端的流量),通过UDP隧道发送给客户端。
        int length = read(interface, packet, sizeof(packet));
        if (length > 0) {
            // Write the outgoing packet to the tunnel.
            send(tunnel, packet, length, MSG_NOSIGNAL);

            // There might be more outgoing packets.
            idle = false;

            // If we were receiving, switch to sending.
            if (timer < 1) {
                timer = 1;
            }
        }

        // Read the incoming packet from the tunnel.
        // 隧道 → TUN:接收客户端发来的数据包,写入TUN接口(最终进入内核网络栈)。
        length = recv(tunnel, packet, sizeof(packet), 0);
        if (length == 0) {
            break;
        }
        if (length > 0) {
            // Ignore control messages, which start with zero.
            if (packet[0] != 0) {
                // Write the incoming packet to the output stream.
                write(interface, packet, length);
            }

            // There might be more incoming packets.
            idle = false;

            // If we were sending, switch to receiving.
            if (timer > 0) {
                timer = 0;
            }
        }

        // If we are idle or waiting for the network, sleep for a
        // fraction of time to avoid busy looping.
        if (idle) {
            usleep(100000);  // 休眠100ms避免忙等待

            // Increase the timer. This is inaccurate but good enough,
            // since everything is operated in non-blocking mode.
            timer += (timer > 0) ? 100 : -100;

            // We are receiving for a long time but not sending.
            // Can you figure out why we use a different value? :)
            // 长时间未收到响应(接收超时)
            // 心跳机制:接收超时后发送控制包(首字节 0),维持NAT映射或检测存活。
            // 超时阈值:-16000(约16秒)和 20000(约20秒)基于累计的休眠时间估算。

            // 发送状态(timer > 0):累计正值,超时断开。
            // 接收状态(timer ≤ 0):累计负值,超时发送心跳包。
            // 目的:检测单向通信故障(如客户端掉线但未发送FIN)。
            if (timer < -16000) {
                // Send empty control messages.
                packet[0] = 0;
                for (int i = 0; i < 3; ++i) {
                     // 发送空控制包(心跳)
                    send(tunnel, packet, 1, MSG_NOSIGNAL);
                }

                // Switch to sending.
                timer = 1;
            }

            // We are sending for a long time but not receiving.
            if (timer > 20000) {
                break;
            }
        }
    }
    printf("%s: The tunnel is broken\n", argv[1]);
    close(tunnel);
}

客户端部分

https://developer.android.google.cn/develop/connectivity/vpn?hl=zh-cn#service

VpnService.Builder.establish

在Android开发中,VpnService.Builder.establish() 方法返回的文件描述符(File Descriptor, fd)是VPN功能的核心,它允许应用拦截和处理设备的网络流量。该fd代表一个TUN虚拟网络接口,用于在系统和应用之间传输原始IP数据包。当VPN激活时,系统将符合路由规则的流量重定向到此接口,应用通过读取fd获取设备发出的数据包,处理后转发到目标服务器;同时将服务器返回的数据写入fd,系统会将其传递给接收应用。

出站流量:应用从fd读取的是设备发送的原始IP包(如TCP/UDP/ICMP)。 入站流量:应用构造响应IP包并写入fd,系统会将其视为来自外部网络的数据。

VpnService 的 fd 操作的是 IP 层 的原始数据包(基于 TUN 设备),而非传输层(TCP/UDP)。这意味着:

读取时:从 fd 读取的是一个完整的 IP 数据包(IPv4 或 IPv6),其载荷可能包含 TCP、UDP、ICMP 或其他协议的数据。 写入时:必须构造一个完整的 IP 数据包(包含正确的 IP 头和校验和),系统才会将其视为合法的入站流量。

IP 头部:包含协议类型(Protocol 字段,如 6 表示 TCP,17 表示 UDP)。 传输层数据:在 IP 头部之后,例如 TCP 头部及其载荷。

读取流程:

从 fd 读取二进制数据,得到一个原始 IP 数据包。 解析 IP 头部的 Protocol 字段,确定是 TCP(6)、UDP(17)还是其他协议。 根据协议类型,继续解析后续的 TCP/UDP 头部(如端口号、校验和等)。

写入流程:

构造一个完整的 IP 数据包(需手动设置 IP 头部字段)。 填充传输层数据(如 TCP/UDP 头部及其载荷)。 计算 IP 头部和传输层头部的校验和。 将二进制数据写入 fd。

addAllowedApplication

VpnService.Builder builder = new VpnService.Builder();
builder.addAllowedApplication("com.example.app1"); // 指定生效的包名
builder.addAllowedApplication("com.example.app2");
ParcelFileDescriptor fd = builder.establish();

生效规则为白名单机制:调用 addAllowedApplication 后,只有指定的应用流量会经过 VPN。若不调用此方法,默认所有应用的流量都会经过 VPN(全局 VPN)。

Android 系统为每个应用分配唯一的 UID。addAllowedApplication 会根据包名查询应用的 UID,并在内核网络规则中标记这些 UID 的流量。流量重定向通过 iptables 规则实现(如 fwmark 标记)。

即使未调用 addAllowedApplication,也可以通过 addRoute 控制哪些 IP 范围的流量经过 VPN。但 addAllowedApplication 优先级更高:如果指定了应用白名单,即使 IP 符合路由规则,非白名单应用的流量也不会经过 VPN。

注意事项

必须在 establish() 前调用:配置需在建立 VPN 连接前完成。 系统应用限制:无法控制系统级应用(如 android 包名)的流量。 动态生效:若应用在 VPN 连接后安装,需重新建立 VPN 连接才能生效。

相关代码

// 优化点:
// 多线程处理:分离读写线程以提高吞吐量。
// 缓冲区池:复用 ByteBuffer 减少内存分配开销。
// Create a DatagramChannel as the VPN tunnel.
try (DatagramChannel tunnel = DatagramChannel.open()) {

    // Protect the tunnel before connecting to avoid loopback.
    // **mService.protect()**:确保 UDP socket 绕过 VPN 自身,避免数据循环。
    if (!mService.protect(tunnel.socket())) {
        throw new IllegalStateException("Cannot protect the tunnel");
    }

    // Connect to the server.
    // **tunnel.connect()**:连接到 VPN 服务器的 UDP 端口。
    tunnel.connect(server);

    // For simplicity, we use the same thread for both reading and
    // writing. Here we put the tunnel into non-blocking mode.
    tunnel.configureBlocking(false);

    // Authenticate and configure the virtual network interface.
    iface = handshake(tunnel);

    // Now we are connected. Set the flag.
    connected = true;

    // **FileInputStream**:从虚拟接口读取本地应用待发送的数据包。
    // **FileOutputStream**:将接收到的数据包写入虚拟接口,供本地应用处理。
    // **ByteBuffer**:缓冲区,用于临时存储单个数据包。
    // Packets to be sent are queued in this input stream.
    FileInputStream in = new FileInputStream(iface.getFileDescriptor());

    // Packets received need to be written to this output stream.
    FileOutputStream out = new FileOutputStream(iface.getFileDescriptor());

    // Allocate the buffer for a single packet.
    ByteBuffer packet = ByteBuffer.allocate(MAX_PACKET_SIZE);

    // Timeouts:
    //   - when data has not been sent in a while, send empty keepalive messages.
    //   - when data has not been received in a while, assume the connection is broken.
    long lastSendTime = System.currentTimeMillis();
    long lastReceiveTime = System.currentTimeMillis();

    // We keep forwarding packets till something goes wrong.
    while (true) {
        // Assume that we did not make any progress in this iteration.
        boolean idle = true;

        // Read the outgoing packet from the input stream.
        // // 处理发送数据(本地应用 -> 服务器)
        int length = in.read(packet.array());
        if (length > 0) {
            // Write the outgoing packet to the tunnel.
            packet.limit(length);
            tunnel.write(packet);
            packet.clear();

            // There might be more outgoing packets.
            idle = false;
            lastReceiveTime = System.currentTimeMillis();
        }

        // Read the incoming packet from the tunnel.
        // 处理接收数据(服务器 -> 本地应用)
        length = tunnel.read(packet);
        if (length > 0) {
            // Ignore control messages, which start with zero.
            // 忽略控制消息(首字节为0)
            if (packet.get(0) != 0) {
                // Write the incoming packet to the output stream.
                // 转发至本地应用
                out.write(packet.array(), 0, length);
            }
            packet.clear();

            // There might be more incoming packets.
            idle = false;
            lastSendTime = System.currentTimeMillis();
        }

        // If we are idle or waiting for the network, sleep for a
        // fraction of time to avoid busy looping.
        if (idle) {
            Thread.sleep(IDLE_INTERVAL_MS);
            final long timeNow = System.currentTimeMillis();

            // 保活包:定期发送首字节为 0 的空包,防止 NAT 超时断开。
            // 超时断开:超过 RECEIVE_TIMEOUT_MS 未收到数据,认为连接已中断。
            if (lastSendTime + KEEPALIVE_INTERVAL_MS <= timeNow) {
                // We are receiving for a long time but not sending.
                // Send empty control messages.
                // 长时间未发送数据:发送空保活包
                packet.put((byte) 0).limit(1);
                for (int i = 0; i < 3; ++i) {
                    packet.position(0);
                    tunnel.write(packet);
                }
                packet.clear();
                lastSendTime = timeNow;
            // 长时间未接收数据:断开连接
            } else if (lastReceiveTime + RECEIVE_TIMEOUT_MS <= timeNow) {
                // We are sending for a long time but not receiving.
                throw new IllegalStateException("Timed out");
            }
        }
    }
} catch (SocketException e) {
    Log.e(getTag(), "Cannot use socket", e);
} finally {
    if (iface != null) {
        try {
            iface.close();
        } catch (IOException e) {
            Log.e(getTag(), "Unable to close interface", e);
        }
    }
}

小结

总的来说,linux和android上都有接口,创建/使用虚拟网卡,提供了拦截/发送 IP数据包的能力。toyvpn中基于udp数据包,把android中的IP数据包转发到linux上。

实际体验下来,很不稳定,终归是toy。

Sphinx doc

上一篇博客:用sphinx-doc整理文档
回头看,上一篇博客已经是18年的事情了。最近我又开始维护起18年的项目了。最近策划同事提了一些需求。我又改进了一波,所以有本文。

sphinx支持导出pdf

sphinx本身是支持导出pdf的,命令如下:

make clean latexpdf

支持中文的话,需要在 conf.py 增加如下信息:

language = 'zh_CN'
# https://sphinx-rtd-trial.readthedocs.io/en/1.0.8/config.html
# support chinese
latex_elements = {
    'preamble': r'''
    \hypersetup{unicode=true}
    \usepackage{CJKutf8}
    \DeclareUnicodeCharacter{00A0}{\nobreakspace}
    \DeclareUnicodeCharacter{2203}{\ensuremath{\exists}}
    \DeclareUnicodeCharacter{2200}{\ensuremath{\forall}}
    \DeclareUnicodeCharacter{2286}{\ensuremath{\subseteq}}
    \DeclareUnicodeCharacter{2713}{x}
    \DeclareUnicodeCharacter{27FA}{\ensuremath{\Longleftrightarrow}}
    \DeclareUnicodeCharacter{221A}{\ensuremath{\sqrt{}}}
    \DeclareUnicodeCharacter{221B}{\ensuremath{\sqrt[3]{}}}
    \DeclareUnicodeCharacter{2295}{\ensuremath{\oplus}}
    \DeclareUnicodeCharacter{2297}{\ensuremath{\otimes}}
    \begin{CJK}{UTF8}{gbsn}
    \AtEndDocument{\end{CJK}}
    ''',
    'extraclassoptions': 'openany,oneside',
}

需要安装如下几个软件:

apt-get install -y texlive-fonts-recommended texlive-fonts-extra
apt-get install -y texlive-latex-extra latexmk latex-cjk-all 

PS:这几个软件都有点大,增加的时候注意看看是否需要。

sphinx支持中文搜索

需要安装这个分词软件

pip install jieba

需要在 conf.py 增加如下信息:

try:
    import jieba
    html_search_options = {
        'type': 'test',
        'language': 'zh',
        'tokenizer': 'jieba.sphinx_zh'
    }
except:
    pass

在 Sphinx 搜索引擎中配置 'tokenizer': 'jieba.sphinx_zh' 以支持中文搜索,其核心原理是通过集成 中文分词技术 和 搜索引擎的索引机制,将中文文本拆解为有意义的词语单元,从而实现对中文内容的精准检索。以下是具体实现原理的分步解释:

  1. 中文分词的挑战

中文文本没有像英文那样的空格分隔符,因此需要借助分词工具将连续的字序列切分为独立的词语。例如:

原始文本:"我爱自然语言处理" 分词结果:["我", "爱", "自然语言处理"] 或 ["我", "爱", "自然", "语言", "处理"](取决于分词模式) 分词质量直接影响搜索的准确性和召回率。错误的分词(如 "自然语" 和 "言处理")会导致搜索失败。

  1. Jieba 分词器的角色

Jieba 是一个高效的中文分词工具,提供以下能力:

精确模式:将句子最精确地切分(适合文本分析)。 全模式:扫描所有可能的词语组合(速度快,可能冗余)。 搜索引擎模式:在精确模式基础上,对长词再次切分(适合检索场景)。 在 Sphinx 中,jieba.sphinx_zh 模块是专门为 Sphinx 设计的适配接口,使其能够调用 Jieba 的分词功能。

  1. Sphinx 的索引和搜索流程

配置 tokenizer: jieba.sphinx_zh 后,Sphinx 的工作流程如下:

索引阶段(Indexing) 文本预处理:读取文档中的中文文本。 调用 Jieba 分词:使用 Jieba 将文本切分为词语列表。例如: 输入:"清华大学位于北京" 分词结果:["清华大学", "位于", "北京"] 构建倒排索引:将分词后的词语作为索引项(terms),记录它们在文档中的位置和频率,形成倒排索引结构。 搜索阶段(Searching) 用户输入查询:例如 "北京 大学"。 查询分词:Jieba 将查询词切分为 ["北京", "大学"]。 匹配倒排索引:Sphinx 查找包含这些词语的文档,计算相关性并返回结果。

  1. 关键技术与优化

词典机制:Jieba 内置核心词典(如 dict.txt),包含常见词语及其词频,确保基础分词的准确性。用户也可扩展自定义词典(如添加专业术语 "自然语言处理")。 算法优化: 基于前缀词典的贪婪匹配:最大化匹配长词(如优先匹配 "清华大学" 而非 "清华" + "大学")。 隐马尔可夫模型(HMM):识别未登录词(如人名、机构名)。 停用词过滤:Sphinx 可配置忽略常见无意义词(如 "的"、"了"),减少索引冗余。 词性标注(可选):通过 Jieba 的词性标注功能,实现更细粒度的搜索过滤(如仅搜索名词)。

  1. 为何需要显式配置 Tokenizer?

Sphinx 默认使用基于空格和标点的简单分词(适合英文),无法处理中文连续文本。通过替换为 jieba.sphinx_zh,Sphinx 的底层分词逻辑被重定向到 Jieba 的接口,从而支持中文语义单元的分词。

  1. 实际依赖和扩展

安装 Jieba:需通过 pip install jieba 安装库,并确保 Sphinx 能调用 Python 模块。 自定义词典:可通过 jieba.load_userdict("custom_dict.txt") 添加领域专有词汇。 性能调优:调整 Jieba 的缓存机制和并行分词模式,以提升索引速度。

总的来说,通过集成 Jieba 分词器,Sphinx 将中文文本转化为有意义的词语单元,解决了中文全文检索的核心难题。这一配置直接影响了索引构建和查询处理的底层逻辑,使 Sphinx 能够像处理英文一样高效地支持中文搜索,核心依赖分词准确性和搜索引擎的倒排索引技术

sphinx支持自定义样式/JS

需要在 conf.py 增加如下信息:

html_static_path = ['_static']
html_js_files = ['yeshen.js','yeshen_1.js']
html_css_files = ['yeshen.css']

创建 _static 目录,然后在其中增加 yeshen.js yeshen.css yeshen_1.js 等几个文件 这几个文件会在生成的index.html中引用。

目录说明:

conf.py
_static
    |-- yeshen.js
    |-- yeshen.css
    |__ yeshen_1.js

sphinx支持自定义模版

  1. 创建模板目录

在项目根目录下创建 _templates 文件夹:

mkdir source/_templates
  1. 配置模板路径

在  conf.py 中指定模板路径:

templates_path = ['_templates']
  1. 覆盖现有模板

查找目标模板:确定你要修改的模板文件(如 layout.html, footer.html 等)。参考你所使用的主题的模板结构。 创建同名文件:在 _templates 下创建同名文件,Sphinx将优先使用你的模板。 示例:修改页脚(覆盖 footer.html)

<!-- source/_templates/footer.html -->
<div class="footer">
    © 2025 @yeshen.org | 自定义页脚内容
</div>
  1. 扩展现有模板(继承并修改) 使用Jinja2的 {% extends %} 继承原模板,然后覆盖特定块。

示例:在侧边栏添加内容

<!-- source/_templates/sidebar.html -->
{% extends "!sidebar.html" %}

{% block sidebarlogo %}
    {{ super() }}  <!-- 保留原有内容 -->
    <div class="custom-sidebar-section">
        你的自定义内容
    </div>
{% endblock %}
  1. 创建全新模板

在 _templates 中新建文件,并通过 .. template:: 指令在RST中引用。

  1. 使用自定义模板

HTML主题:确保 conf.py 中设置了正确的主题(如 html_theme = 'alabaster')。 构建文档:

sphinx-build -b html source build

常用模板文件 layout.html: 整体布局 header.html: 页眉 footer.html: 页脚 sidebar.html: 侧边栏 page.html: 单页内容 提示 使用 sphinx-build -b html source build 重新生成文档以查看更改。 查看主题的原始模板文件(如经典主题模板)获取可覆盖的块名。 通过以上步骤,你可以灵活定制Sphinx文档的样式和结构

sphinx支持单页面

多页面的场景下,切换导航的时候,页面会跳动,体验不是很好,sphinx支持编译单页面,可以这样做:

make clean singlehtml

但是观察单页面输出的场景,搜索栏不见了。

~~怎么添加搜索栏还要进一步尝试,本文就不展开了~~

尾声

如果还有下一篇博客的话,应该是怎么定制自己的模版了。:)

have fun~