Wireguard握手交互代码分析

消息结构

握手请求消息
struct message_handshake_initiation {
	struct message_header header;      				// header里只有一个type, 发起0x01
	__le32 sender_index;							// Sender的标识,自定义。
	u8 unencrypted_ephemeral[NOISE_PUBLIC_KEY_LEN]; // 本地临时公钥
    
    // AEAD加密后的本地公钥
	u8 encrypted_static[noise_encrypted_len(NOISE_PUBLIC_KEY_LEN)];  
    // AEAD加密后的时间戳
	u8 encrypted_timestamp[noise_encrypted_len(NOISE_TIMESTAMP_LEN)];
	// macs.mac1为对端公钥计算出的哈希值
    // macs.mac2为空或者cookie计算出的哈希值
    struct message_macs macs;
};

握手响应消息
struct message_handshake_response {
	struct message_header header;     // type = 0x02 
	__le32 sender_index;              // Sender的标识
	__le32 receiver_index;			  // Receiver标识
	u8 unencrypted_ephemeral[NOISE_PUBLIC_KEY_LEN]; // 本地临时公钥
	u8 encrypted_nothing[noise_encrypted_len(0)];   // 可校验的虚无的加密
	struct message_macs macs;		  
};
握手Cookie消息
struct message_handshake_cookie {
	struct message_header header;    // type = 0x03
	__le32 receiver_index;			 // Receiver标识
	u8 nonce[COOKIE_NONCE_LEN];      // 随机数
	u8 encrypted_cookie[noise_encrypted_len(COOKIE_LEN)]; //AEAD加密后的cookie
};
数据传输消息
struct message_data {
	struct message_header header;   //type = 0x04
	__le32 key_idx;
	__le64 counter;
	u8 encrypted_data[];            //AEAD加密后的inner IP pkt
};

交互流程

发送握手请求
static void wg_packet_send_handshake_initiation(struct wg_peer *peer)
{
	struct message_handshake_initiation packet;

	if (!wg_birthdate_has_expired(atomic64_read(&peer->last_sent_handshake),
				      REKEY_TIMEOUT))
		return; /* This function is rate limited. */

	atomic64_set(&peer->last_sent_handshake, ktime_get_coarse_boottime_ns());
	net_dbg_ratelimited("%s: Sending handshake initiation to peer %llu (%pISpfsc)\n",
			    peer->device->dev->name, peer->internal_id,
			    &peer->endpoint.addr);

    // 构造一个握手发起消息,赋值macs之前内容
	if (wg_noise_handshake_create_initiation(&packet, &peer->handshake)) {
        //赋值macs.mac1和macs.mac2
        // macs.mac1为 对端公钥为参数的blake2s哈希值, mac2为0或者上一个响应的cookie的哈希值
		wg_cookie_add_mac_to_packet(&packet, sizeof(packet), peer);
		wg_timers_any_authenticated_packet_traversal(peer);
		wg_timers_any_authenticated_packet_sent(peer);
		atomic64_set(&peer->last_sent_handshake,
			     ktime_get_coarse_boottime_ns());
		wg_socket_send_buffer_to_peer(peer, &packet, sizeof(packet),
					      HANDSHAKE_DSCP);
		wg_timers_handshake_initiated(peer);
	}
}

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;

	/* We need to wait for crng _before_ taking any locks, since
	 * curve25519_generate_secret uses get_random_bytes_wait.
	 */
	wait_for_random_bytes();

	down_read(&handshake->static_identity->lock);
	down_write(&handshake->lock);

	if (unlikely(!handshake->static_identity->has_identity))
		goto out;

    // type为0x01,发起消息
	dst->header.type = cpu_to_le32(MESSAGE_HANDSHAKE_INITIATION);

    //使用对端公钥初始化握手chaining_key和hash
    //chaining_key初始值为Noise_IKpsk2_25519_ChaChaPoly_BLAKE2s的blake2哈希值
    //hash是与chaining_key和对端公钥有关的blake2哈希值
	handshake_init(handshake->chaining_key, handshake->hash,
		       handshake->remote_static);

	/* e */
    //生成临时私钥
	curve25519_generate_secret(handshake->ephemeral_private);
    //临时私钥生成临时公钥
	if (!curve25519_generate_public(dst->unencrypted_ephemeral,
					handshake->ephemeral_private))
		goto out;
    // 临时公钥参与哈希计算更新hash和chaining_key
	message_ephemeral(dst->unencrypted_ephemeral,
			  dst->unencrypted_ephemeral, handshake->chaining_key,
			  handshake->hash);

	/* es */
    // 临时私钥与对端公钥点乘(DH)计算出一个值,该值参与哈希计算更新chaining_key和key
	if (!mix_dh(handshake->chaining_key, key, handshake->ephemeral_private,
		    handshake->remote_static))
		goto out;

	/* s */
    // 本地公钥用上述key AEAD加密,hash随之更新
	message_encrypt(dst->encrypted_static,
			handshake->static_identity->static_public,
			NOISE_PUBLIC_KEY_LEN, key, handshake->hash);

	/* ss */
    // 预静态密钥和chaining_key哈希更新key和chaining_key(kdf算法)
	if (!mix_precomputed_dh(handshake->chaining_key, key,
				handshake->precomputed_static_static))
		goto out;

	/* {t} */
	tai64n_now(timestamp);
    // 时间戳用上述key加密,hash随之更新
	message_encrypt(dst->encrypted_timestamp, timestamp,
			NOISE_TIMESTAMP_LEN, key, handshake->hash);
	// 握手信息插入哈希表
	dst->sender_index = wg_index_hashtable_insert(
		handshake->entry.peer->device->index_hashtable,
		&handshake->entry);

	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;
}
接收握手请求

收到握手请求之后会判断当前状态是否是低负载,如果超过低负载阈值则返回cookie,如果比低负载还低则直接处理握手请求。

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;

	down_read(&wg->static_identity.lock);
	if (unlikely(!wg->static_identity.has_identity))
		goto out;
	// 本地公钥初始化hash和chaining_key和hash
	handshake_init(chaining_key, hash, wg->static_identity.static_public);

	/* e */
    // 对端临时公钥参与哈希计算更新hash和chaining_key
	message_ephemeral(e, src->unencrypted_ephemeral, chaining_key, hash);

	/* es */
    // 本地私钥与对端临时公钥点乘(DH)计算出一个值,该值参与哈希计算更新chaining_key和key
	if (!mix_dh(chaining_key, key, wg->static_identity.static_private, e))
		goto out;

	/* s */
    // 用上述key AEAD解密对端公钥,hash随之更新
	if (!message_decrypt(s, src->encrypted_static,
			     sizeof(src->encrypted_static), key, hash))
		goto out;

	/* Lookup which peer we're actually talking to */
    // 对端公钥查寻peer
	peer = wg_pubkey_hashtable_lookup(wg->peer_hashtable, s);
	if (!peer)
		goto out;
	handshake = &peer->handshake;

	/* ss */
    // 预静态密钥和chaining_key哈希更新key和chaining_key(kdf算法)
	if (!mix_precomputed_dh(chaining_key, key,
				handshake->precomputed_static_static))
	    goto out;

	/* {t} */
    // 利用上述key hash AEAD解密时间戳
	if (!message_decrypt(t, src->encrypted_timestamp,
			     sizeof(src->encrypted_timestamp), key, hash))
		goto out;

	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;

	/* Success! Copy everything to peer */
	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);
	// 记录hash和chaining_key
    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;
}
发送握手响应

接收握手请求之后,校验正确之后,响应者会发送握手响应消息,并计算出共享密钥。

void wg_packet_send_handshake_response(struct wg_peer *peer)
{
	struct message_handshake_response packet;

	atomic64_set(&peer->last_sent_handshake, ktime_get_coarse_boottime_ns());
	net_dbg_ratelimited("%s: Sending handshake response to peer %llu (%pISpfsc)\n",
			    peer->device->dev->name, peer->internal_id,
			    &peer->endpoint.addr);
		//构造握手响应报文
	if (wg_noise_handshake_create_response(&packet, &peer->handshake)) {
        // 为报文中mac1, mac2赋值
		wg_cookie_add_mac_to_packet(&packet, sizeof(packet), peer);
		if (wg_noise_handshake_begin_session(&peer->handshake,
						     &peer->keypairs)) {
			wg_timers_session_derived(peer);
			wg_timers_any_authenticated_packet_traversal(peer);
			wg_timers_any_authenticated_packet_sent(peer);
			atomic64_set(&peer->last_sent_handshake,
				     ktime_get_coarse_boottime_ns());
			wg_socket_send_buffer_to_peer(peer, &packet,
						      sizeof(packet),
						      HANDSHAKE_DSCP);
		}
	}
}

bool wg_noise_handshake_create_response(struct message_handshake_response *dst,
					struct noise_handshake *handshake)
{
	u8 key[NOISE_SYMMETRIC_KEY_LEN];
	bool ret = false;

	/* We need to wait for crng _before_ taking any locks, since
	 * curve25519_generate_secret uses get_random_bytes_wait.
	 */
	wait_for_random_bytes();

	down_read(&handshake->static_identity->lock);
	down_write(&handshake->lock);

	if (handshake->state != HANDSHAKE_CONSUMED_INITIATION)
		goto out;

	dst->header.type = cpu_to_le32(MESSAGE_HANDSHAKE_RESPONSE);
	dst->receiver_index = handshake->remote_index;

	/* e */
    //生成临时私钥和临时公钥
	curve25519_generate_secret(handshake->ephemeral_private);
	if (!curve25519_generate_public(dst->unencrypted_ephemeral,
					handshake->ephemeral_private))
		goto out;
    // 临时公钥参与哈希计算更新hash和chaining_key
    // 此处的hash和chaining_key为解析请求包时更新的结果
	message_ephemeral(dst->unencrypted_ephemeral,
			  dst->unencrypted_ephemeral, handshake->chaining_key,
			  handshake->hash);

	/* ee */
    // 临时私钥与对端临时公钥点乘(DH)计算出一个值,该值参与哈希计算更新chaining_key和key
	if (!mix_dh(handshake->chaining_key, NULL, handshake->ephemeral_private,
		    handshake->remote_ephemeral))
		goto out;

	/* se */
    // 临时私钥与对端公钥点乘(DH)计算出一个值,该值参与哈希计算更新chaining_key和key
	if (!mix_dh(handshake->chaining_key, NULL, handshake->ephemeral_private,
		    handshake->remote_static))
		goto out;

	/* psk */
    // 利用预共享密钥kdf计算更新chaining_key、hash和key
	mix_psk(handshake->chaining_key, handshake->hash, key,
		handshake->preshared_key);

	/* {} */
    // 利用上述key和hash AEAD加密一段空内容, hash更新
	message_encrypt(dst->encrypted_nothing, NULL, 0, key, handshake->hash);

    // 将该握手信息插入索引哈希表
	dst->sender_index = wg_index_hashtable_insert(
		handshake->entry.peer->device->index_hashtable,
		&handshake->entry);

	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;
}

握手响应包组完以后,调用wg_noise_handshake_begin_session生成一对共享密钥。

使用当前chaining_key kdf算法,生成两个共享密钥

	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);
接收握手响应
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 static_private[NOISE_PUBLIC_KEY_LEN];
	u8 preshared_key[NOISE_SYMMETRIC_KEY_LEN];

	down_read(&wg->static_identity.lock);

	if (unlikely(!wg->static_identity.has_identity))
		goto out;

    //查询该响应的请求是否存在
	handshake = (struct noise_handshake *)wg_index_hashtable_lookup(
		wg->index_hashtable, INDEX_HASHTABLE_HANDSHAKE,
		src->receiver_index, &peer);
	if (unlikely(!handshake))
		goto out;

	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);

	if (state != HANDSHAKE_CREATED_INITIATION)
		goto fail;

	/* e */
    // 对端临时公钥参与哈希计算更新hash和chaining_key
	message_ephemeral(e, src->unencrypted_ephemeral, chaining_key, hash);

	/* ee */
    // 临时私钥与对端临时公钥点乘(DH)计算出一个值,该值参与哈希计算更新chaining_key和key
	if (!mix_dh(chaining_key, NULL, ephemeral_private, e))
		goto fail;

	/* se */
    // 私钥与对端临时公钥点乘(DH)计算出一个值,该值参与哈希计算更新chaining_key和key
	if (!mix_dh(chaining_key, NULL, wg->static_identity.static_private, e))
		goto fail;

	/* psk */
    // 利用预共享密钥kdf计算更新chaining_key、hash和key
	mix_psk(chaining_key, hash, key, preshared_key);

	/* {} */
    // 利用上述key和hash AEAD解密一段空内容
	if (!message_decrypt(NULL, src->encrypted_nothing,
			     sizeof(src->encrypted_nothing), key, hash))
		goto fail;

	/* Success! Copy everything to peer */
	down_write(&handshake->lock);
	/* It's important to check that the state is still the same, while we
	 * have an exclusive 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(static_private, NOISE_PUBLIC_KEY_LEN);
	memzero_explicit(preshared_key, NOISE_SYMMETRIC_KEY_LEN);
	up_read(&wg->static_identity.lock);
	return ret_peer;
}

接受响应成功以后,同样调用wg_noise_handshake_begin_session计算出往返的共享密钥。

发送Cookie

在接收到握手请求,同时接受者处于低负载时,即握手队列深度大于512时,接受者会发送Cookie消息给响应者。

void wg_cookie_message_create(struct message_handshake_cookie *dst,
			      struct sk_buff *skb, __le32 index,
			      struct cookie_checker *checker)
{
	struct message_macs *macs = (struct message_macs *)
		((u8 *)skb->data + skb->len - sizeof(*macs));
	u8 cookie[COOKIE_LEN];

	dst->header.type = cpu_to_le32(MESSAGE_HANDSHAKE_COOKIE);
	dst->receiver_index = index;
	get_random_bytes_wait(dst->nonce, COOKIE_NONCE_LEN);
	// 计算cookie
	make_cookie(cookie, skb, checker);
    // AEAD加密cookie, key为本地公钥的哈希值
	xchacha20poly1305_encrypt(dst->encrypted_cookie, cookie, COOKIE_LEN,
				  macs->mac1, COOKIE_LEN, dst->nonce,
				  checker->cookie_encryption_key);
}

static void make_cookie(u8 cookie[COOKIE_LEN], struct sk_buff *skb,
			struct cookie_checker *checker)
{
	struct blake2s_state state;

    //判断秘密随机数超时,超时则重新生成
	if (wg_birthdate_has_expired(checker->secret_birthdate,
				     COOKIE_SECRET_MAX_AGE)) {
		down_write(&checker->secret_lock);
		checker->secret_birthdate = ktime_get_coarse_boottime_ns();
		get_random_bytes(checker->secret, NOISE_HASH_LEN);
		up_write(&checker->secret_lock);
	}

	down_read(&checker->secret_lock);
	// 秘密随机数 + 对端IP地址 + 对端UDP端口blake2s计算出哈希值cookie
	blake2s_init_key(&state, COOKIE_LEN, checker->secret, NOISE_HASH_LEN);
	if (skb->protocol == htons(ETH_P_IP))
		blake2s_update(&state, (u8 *)&ip_hdr(skb)->saddr,
			       sizeof(struct in_addr));
	else if (skb->protocol == htons(ETH_P_IPV6))
		blake2s_update(&state, (u8 *)&ipv6_hdr(skb)->saddr,
			       sizeof(struct in6_addr));
	blake2s_update(&state, (u8 *)&udp_hdr(skb)->source, sizeof(__be16));
	blake2s_final(&state, cookie);

	up_read(&checker->secret_lock);
}
接收Cookie
void wg_cookie_message_consume(struct message_handshake_cookie *src,
			       struct wg_device *wg)
{
	struct wg_peer *peer = NULL;
	u8 cookie[COOKIE_LEN];
	bool ret;

    // 查询握手表
	if (unlikely(!wg_index_hashtable_lookup(wg->index_hashtable,
						INDEX_HASHTABLE_HANDSHAKE |
						INDEX_HASHTABLE_KEYPAIR,
						src->receiver_index, &peer)))
		return;

	down_read(&peer->latest_cookie.lock);
	if (unlikely(!peer->latest_cookie.have_sent_mac1)) {
		up_read(&peer->latest_cookie.lock);
		goto out;
	}
    // AEAD解密cookie, key为对端公钥的哈希值
	ret = xchacha20poly1305_decrypt(
		cookie, src->encrypted_cookie, sizeof(src->encrypted_cookie),
		peer->latest_cookie.last_mac1_sent, COOKIE_LEN, src->nonce,
		peer->latest_cookie.cookie_decryption_key);
	up_read(&peer->latest_cookie.lock);

	if (ret) {
		down_write(&peer->latest_cookie.lock);
		memcpy(peer->latest_cookie.cookie, cookie, COOKIE_LEN);
		peer->latest_cookie.birthdate = ktime_get_coarse_boottime_ns();
		peer->latest_cookie.is_valid = true;
		peer->latest_cookie.have_sent_mac1 = false;
		up_write(&peer->latest_cookie.lock);
	} else {
		net_dbg_ratelimited("%s: Could not decrypt invalid cookie response\n",
				    wg->dev->name);
	}

out:
	wg_peer_put(peer);
}

  • 3
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
OpenWRT 是一个轻量级的嵌入式 Linux 发行版,常用于路由器和物联网设备的固件定制。WireGuard 是一种快速、安全的隧道加密网络协议,用于创建点对点的加密连接。 如果你想要在 OpenWRT 中配置 WireGuard 并成功实现两个设备之间的握手(即建立连接),通常你需要按照以下步骤操作: 1. **安装 WireGuard**: 在 OpenWRT 上启用 Git,并添加 WireGuard 包源: ```bash opkg update opkg install git git clone https://github.com/WireGuard/wireguard-go.git ``` 安装 WireGuard 库及其工具。 2. **设置密钥交换**: 创建公钥/私钥对,然后将它们分别分发给设备: ```bash cd wireguard-go make wg genkey | tee private_key | wg pubkey > public_key ``` 3. **配置 WireGuard 配置文件**: 使用公钥创建 `wg0` 或其他设备接口的配置文件。例如,在 `/etc/wireguard/wg0.conf` 文件中: ```yaml [Interface] PrivateKey = <your_private_key> Address = 10.0.0.1/24 ListenPort = 51820 [Peer] PublicKey = <remote_public_key> AllowedIPs = 10.0.0.2/32 ``` 4. **启动 WireGuard**: 启动服务并使接口开机自启: ```bash wg-quick up wg0 rc-service wireguard start rc-service wireguard enable ``` 5. **验证连接**: 在两台设备上确认彼此能够 ping 通或访问对方的服务,如果成功则表示握手完成。 相关问题: 1. 如何检查 WireGuard 是否已成功配置? 2. 如果配置过程中遇到连接失败,可能的原因有哪些? 3. 在 OpenWRT 中如何查看 WireGuard 的状态信息?
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值