GO Negotiation流程分析

本文为《深入理解Android Wi-Fi、NFC和GPS卷》读书笔记,Android源码为Android 5.1

P2pStateMachine收到P2P_PROV_DISC_PBC_RSP_EVENT消息后,将在ProvisionDiscoveryState中调用p2pConnectWithPinDisplay,该函数内部将发送P2P_CONNECT命令给WPAS。来看该命令的处理流程。


P2P_CONNECT 处理流程
P2P_CONNECT命令的参数比较多,而本例中P2pStateMachine发送的命令格式如下。
P2P_CONNECT 8a:32:9b:6c:d1:80 pbc go_intent=7
其中 8a:32:9b:6c:d1:80 代表对端P2P设备地址
pbc指定了WSC配置方法为PBC, go_intent=7 设置GO Intent值为7
P2P_CONNECT对应的处理函数为 p2p_ctrl_connect

android-5.1/external/wpa_supplicant_8/wpa_supplicant/ctrl_iface.c

static int p2p_ctrl_connect(struct wpa_supplicant *wpa_s, char *cmd,
			    char *buf, size_t buflen)
{
	u8 addr[ETH_ALEN];
	char *pos, *pos2;
	char *pin = NULL;
	enum p2p_wps_method wps_method;
	int new_pin;
	int ret;
	int persistent_group, persistent_id = -1;
	int join;
	int auth;
	int automatic;
	int go_intent = -1;
	int freq = 0;
	int pd;
	int ht40, vht;

	/* <addr> <"pbc" | "pin" | PIN> [label|display|keypad]
	 * [persistent|persistent=<network id>]
	 * [join] [auth] [go_intent=<0..15>] [freq=<in MHz>] [provdisc]
	 * [ht40] [vht] */

	if (hwaddr_aton(cmd, addr))
		return -1;

	pos = cmd + 17;
	if (*pos != ' ')
		return -1;
	pos++;

	persistent_group = os_strstr(pos, " persistent") != NULL;
	pos2 = os_strstr(pos, " persistent=");
	if (pos2) {
		struct wpa_ssid *ssid;
		persistent_id = atoi(pos2 + 12);
		ssid = wpa_config_get_network(wpa_s->conf, persistent_id);
		if (ssid == NULL || ssid->disabled != 2 ||
		    ssid->mode != WPAS_MODE_P2P_GO) {
			wpa_printf(MSG_DEBUG, "CTRL_IFACE: Could not find "
				   "SSID id=%d for persistent P2P group (GO)",
				   persistent_id);
			return -1;
		}
	}
	join = os_strstr(pos, " join") != NULL;
	auth = os_strstr(pos, " auth") != NULL;
	automatic = os_strstr(pos, " auto") != NULL;
	pd = os_strstr(pos, " provdisc") != NULL;
	vht = (os_strstr(cmd, " vht") != NULL) || wpa_s->conf->p2p_go_vht;
	ht40 = (os_strstr(cmd, " ht40") != NULL) || wpa_s->conf->p2p_go_ht40 ||
		vht;

	pos2 = os_strstr(pos, " go_intent=");
	if (pos2) {
		pos2 += 11;
		go_intent = atoi(pos2);
		if (go_intent < 0 || go_intent > 15)
			return -1;
	}

	pos2 = os_strstr(pos, " freq=");
	if (pos2) {
		pos2 += 6;
		freq = atoi(pos2);
		if (freq <= 0)
			return -1;
	}

	if (os_strncmp(pos, "pin", 3) == 0) {
		/* Request random PIN (to be displayed) and enable the PIN */
		wps_method = WPS_PIN_DISPLAY;
	} else if (os_strncmp(pos, "pbc", 3) == 0) {
		wps_method = WPS_PBC;
	} else {
		pin = pos;
		pos = os_strchr(pin, ' ');
		wps_method = WPS_PIN_KEYPAD;
		if (pos) {
			*pos++ = '\0';
			if (os_strncmp(pos, "display", 7) == 0)
				wps_method = WPS_PIN_DISPLAY;
		}
		if (!wps_pin_str_valid(pin)) {
			os_memcpy(buf, "FAIL-INVALID-PIN\n", 17);
			return 17;
		}
	}

	//参数处理,最终调用的函数为 wpas_p2p_connect
	new_pin = wpas_p2p_connect(wpa_s, addr, pin, wps_method,
				   persistent_group, automatic, join,
				   auth, go_intent, freq, persistent_id, pd,
				   ht40, vht);
	if (new_pin == -2) {
		os_memcpy(buf, "FAIL-CHANNEL-UNAVAILABLE\n", 25);
		return 25;
	}
	if (new_pin == -3) {
		os_memcpy(buf, "FAIL-CHANNEL-UNSUPPORTED\n", 25);
		return 25;
	}
	if (new_pin < 0)
		return -1;
	if (wps_method == WPS_PIN_DISPLAY && pin == NULL) {
		ret = os_snprintf(buf, buflen, "%08d", new_pin);
		if (ret < 0 || (size_t) ret >= buflen)
			return -1;
		return ret;
	}

	os_memcpy(buf, "OK\n", 3);
	return 3;
}
android-5.1/external/wpa_supplicant_8/wpa_supplicant/p2p_supplicant.c

int wpas_p2p_connect(struct wpa_supplicant *wpa_s, const u8 *peer_addr,
		     const char *pin, enum p2p_wps_method wps_method,
		     int persistent_group, int auto_join, int join, int auth,
		     int go_intent, int freq, int persistent_id, int pd,
		     int ht40, int vht)
{
	int force_freq = 0, pref_freq = 0;
	int ret = 0, res;
	enum wpa_driver_if_type iftype;
	const u8 *if_addr;
	struct wpa_ssid *ssid = NULL;

	if (wpa_s->global->p2p_disabled || wpa_s->global->p2p == NULL)
		return -1;

	if (persistent_id >= 0) {
		ssid = wpa_config_get_network(wpa_s->conf, persistent_id);
		if (ssid == NULL || ssid->disabled != 2 ||
		    ssid->mode != WPAS_MODE_P2P_GO)
			return -1;
	}

	os_free(wpa_s->global->add_psk);
	wpa_s->global->add_psk = NULL;

	wpa_s->global->p2p_fail_on_wps_complete = 0;

	if (go_intent < 0)
		go_intent = wpa_s->conf->p2p_go_intent;

	if (!auth)
		wpa_s->p2p_long_listen = 0;

	wpa_s->p2p_wps_method = wps_method;
	wpa_s->p2p_persistent_group = !!persistent_group;
	wpa_s->p2p_persistent_id = persistent_id;
	wpa_s->p2p_go_intent = go_intent;
	wpa_s->p2p_connect_freq = freq;
	wpa_s->p2p_fallback_to_go_neg = 0;
	wpa_s->p2p_pd_before_go_neg = !!pd;
	wpa_s->p2p_go_ht40 = !!ht40;
	wpa_s->p2p_go_vht = !!vht;

	if (pin)
		os_strlcpy(wpa_s->p2p_pin, pin, sizeof(wpa_s->p2p_pin));
	else if (wps_method == WPS_PIN_DISPLAY) {
		ret = wps_generate_pin();
		os_snprintf(wpa_s->p2p_pin, sizeof(wpa_s->p2p_pin), "%08d",
			    ret);
		wpa_printf(MSG_DEBUG, "P2P: Randomly generated PIN: %s",
			   wpa_s->p2p_pin);
	} else
		wpa_s->p2p_pin[0] = '\0';

	if (join || auto_join) {
		u8 iface_addr[ETH_ALEN], dev_addr[ETH_ALEN];
		if (auth) {
			wpa_printf(MSG_DEBUG, "P2P: Authorize invitation to "
				   "connect a running group from " MACSTR,
				   MAC2STR(peer_addr));
			os_memcpy(wpa_s->p2p_auth_invite, peer_addr, ETH_ALEN);
			return ret;
		}
		os_memcpy(dev_addr, peer_addr, ETH_ALEN);
		if (p2p_get_interface_addr(wpa_s->global->p2p, peer_addr,
					   iface_addr) < 0) {
			os_memcpy(iface_addr, peer_addr, ETH_ALEN);
			p2p_get_dev_addr(wpa_s->global->p2p, peer_addr,
					 dev_addr);
		}
		if (auto_join) {
			os_get_reltime(&wpa_s->p2p_auto_started);
			wpa_printf(MSG_DEBUG, "P2P: Auto join started at "
				   "%ld.%06ld",
				   wpa_s->p2p_auto_started.sec,
				   wpa_s->p2p_auto_started.usec);
		}
		wpa_s->user_initiated_pd = 1;
		if (wpas_p2p_join(wpa_s, iface_addr, dev_addr, wps_method,
				  auto_join, freq, NULL, 0) < 0)
			return -1;
		return ret;
	}

	res = wpas_p2p_setup_freqs(wpa_s, freq, &force_freq, &pref_freq,
				   go_intent == 15);
	if (res)
		return res;
	wpas_p2p_set_own_freq_preference(wpa_s,
					 force_freq ? force_freq : pref_freq);

	//注意下面这个 wpas_p2p_create_iface函数,它将判断是否需要创建一个新的 virtual interface,7.4.1介绍Driver Flags和重要数据结构时提到 WPA_DRIVER_FLAGS_P2P_MGMT_AND_NON_P2P标志,就本例而言,wifi driver flags 中包含了该标志, 所以下面这个函数的返回值为1, 表示需要单独创建一个新的Virtual Interface供P2P使用。这个Virtual Interface的地址应该就是P2P Interface Address。
	wpa_s->create_p2p_iface = wpas_p2p_create_iface(wpa_s);

	if (wpa_s->create_p2p_iface) {		//本例满足此if条件
		/* Prepare to add a new interface for the group */
		iftype = WPA_IF_P2P_GROUP;	//设置Interface type
		if (go_intent == 15)		//本例 go_intent为7
			iftype = WPA_IF_P2P_GO;
		//下面这个函数将创建此 Virtual Interface,并获取其Interface Address。
		//wpas_p2p_add_group_interface内部将调用 driver_nl80211.c的wpa_driver_nl80211_if_add函数。
		if (wpas_p2p_add_group_interface(wpa_s, iftype) < 0) {
			wpa_printf(MSG_ERROR, "P2P: Failed to allocate a new "
				   "interface for the group");
			return -1;
		}

		if_addr = wpa_s->pending_interface_addr;
	} else
		if_addr = wpa_s->own_addr;

	if (auth) {
		if (wpas_p2p_auth_go_neg(wpa_s, peer_addr, wps_method,
					 go_intent, if_addr,
					 force_freq, persistent_group, ssid,
					 pref_freq) < 0)
			return -1;
		return ret;
	}

	//下面这个函数内部将调用p2p_connect,我们将直接分析。
	if (wpas_p2p_start_go_neg(wpa_s, peer_addr, wps_method,
				  go_intent, if_addr, force_freq,
				  persistent_group, ssid, pref_freq) < 0) {
		if (wpa_s->create_p2p_iface)
			wpas_p2p_remove_pending_group_interface(wpa_s);
		return -1;
	}
	return ret;
}
android-5.1/external/wpa_supplicant_8/src/p2p/p2p.c

int p2p_connect(struct p2p_data *p2p, const u8 *peer_addr,
		enum p2p_wps_method wps_method,
		int go_intent, const u8 *own_interface_addr,
		unsigned int force_freq, int persistent_group,
		const u8 *force_ssid, size_t force_ssid_len,
		int pd_before_go_neg, unsigned int pref_freq, u16 oob_pw_id)
{
	struct p2p_device *dev;

	p2p_dbg(p2p, "Request to start group negotiation - peer=" MACSTR
		"  GO Intent=%d  Intended Interface Address=" MACSTR
		" wps_method=%d persistent_group=%d pd_before_go_neg=%d "
		"oob_pw_id=%u",
		MAC2STR(peer_addr), go_intent, MAC2STR(own_interface_addr),
		wps_method, persistent_group, pd_before_go_neg, oob_pw_id);

	dev = p2p_get_device(p2p, peer_addr);
	if (dev == NULL || (dev->flags & P2P_DEV_PROBE_REQ_ONLY)) {
		p2p_dbg(p2p, "Cannot connect to unknown P2P Device " MACSTR,
			MAC2STR(peer_addr));
		return -1;
	}

	//如果指定了工作频段,则需要判断是否支持该工作频段
	if (p2p_prepare_channel(p2p, dev, force_freq, pref_freq,
				go_intent == 15) < 0)
		return -1;

	if (dev->flags & P2P_DEV_GROUP_CLIENT_ONLY) {
		if (!(dev->info.dev_capab &
		      P2P_DEV_CAPAB_CLIENT_DISCOVERABILITY)) {
			p2p_dbg(p2p, "Cannot connect to P2P Device " MACSTR
				" that is in a group and is not discoverable",
				MAC2STR(peer_addr));
			return -1;
		}
		if (dev->oper_freq <= 0) {
			p2p_dbg(p2p, "Cannot connect to P2P Device " MACSTR
				" with incomplete information",
				MAC2STR(peer_addr));
			return -1;
		}

		/*
		 * First, try to connect directly. If the peer does not
		 * acknowledge frames, assume it is sleeping and use device
		 * discoverability via the GO at that point.
		 */
	}

	p2p->ssid_set = 0;
	if (force_ssid) {
		wpa_hexdump_ascii(MSG_DEBUG, "P2P: Forced SSID",
				  force_ssid, force_ssid_len);
		os_memcpy(p2p->ssid, force_ssid, force_ssid_len);
		p2p->ssid_len = force_ssid_len;
		p2p->ssid_set = 1;
	}

	dev->flags &= ~P2P_DEV_NOT_YET_READY;
	dev->flags &= ~P2P_DEV_USER_REJECTED;
	dev->flags &= ~P2P_DEV_WAIT_GO_NEG_RESPONSE;
	dev->flags &= ~P2P_DEV_WAIT_GO_NEG_CONFIRM;
	if (pd_before_go_neg)
		dev->flags |= P2P_DEV_PD_BEFORE_GO_NEG;
	else {
		dev->flags &= ~P2P_DEV_PD_BEFORE_GO_NEG;
		/*
		 * Assign dialog token and tie breaker here to use the same
		 * values in each retry within the same GO Negotiation exchange.
		 */
		dev->dialog_token++;
		if (dev->dialog_token == 0)
			dev->dialog_token = 1;
		dev->tie_breaker = p2p->next_tie_breaker;
		p2p->next_tie_breaker = !p2p->next_tie_breaker;
	}
	dev->connect_reqs = 0;
	dev->go_neg_req_sent = 0;
	dev->go_state = UNKNOWN_GO;
	p2p_set_dev_persistent(dev, persistent_group);
	p2p->go_intent = go_intent;
	os_memcpy(p2p->intended_addr, own_interface_addr, ETH_ALEN);

	//如果P2P模块的状态不为P2P_IDLE,则先停止find工作
	if (p2p->state != P2P_IDLE)
		p2p_stop_find(p2p);

	if (p2p->after_scan_tx) {
		/*
		 * We need to drop the pending frame to avoid issues with the
		 * new GO Negotiation, e.g., when the pending frame was from a
		 * previous attempt at starting a GO Negotiation.
		 */
		p2p_dbg(p2p, "Dropped previous pending Action frame TX that was waiting for p2p_scan completion");
		os_free(p2p->after_scan_tx);
		p2p->after_scan_tx = NULL;
	}

	dev->wps_method = wps_method;
	dev->oob_pw_id = oob_pw_id;
	dev->status = P2P_SC_SUCCESS;

	if (p2p->p2p_scan_running) {
		p2p_dbg(p2p, "p2p_scan running - delay connect send");
		//如果当前P2P还在扫描过程中,则设置 start_after_scan 为 P2P_AFTER_SCAN_CONNECT 标志,当scan结束后,在扫描结果处理流程中,该标志将通知P2P进入 connect 处理流程
		p2p->start_after_scan = P2P_AFTER_SCAN_CONNECT;
		os_memcpy(p2p->after_scan_peer, peer_addr, ETH_ALEN);
		return 0;
	}
	p2p->start_after_scan = P2P_AFTER_SCAN_NOTHING;

	//发送GON Request帧
	return p2p_connect_send(p2p, dev);
}
android-5.1/external/wpa_supplicant_8/src/p2p/p2p_go_neg.c

int p2p_connect_send(struct p2p_data *p2p, struct p2p_device *dev)
{
	struct wpabuf *req;
	int freq;

	if (dev->flags & P2P_DEV_PD_BEFORE_GO_NEG) {
		u16 config_method;
		p2p_dbg(p2p, "Use PD-before-GO-Neg workaround for " MACSTR,
			MAC2STR(dev->info.p2p_device_addr));
		if (dev->wps_method == WPS_PIN_DISPLAY)
			config_method = WPS_CONFIG_KEYPAD;
		else if (dev->wps_method == WPS_PIN_KEYPAD)
			config_method = WPS_CONFIG_DISPLAY;
		else if (dev->wps_method == WPS_PBC)
			config_method = WPS_CONFIG_PUSHBUTTON;
		else
			return -1;
		return p2p_prov_disc_req(p2p, dev->info.p2p_device_addr,
					 config_method, 0, 0, 1);
	}

	freq = dev->listen_freq > 0 ? dev->listen_freq : dev->oper_freq;
	if (dev->oob_go_neg_freq > 0)
		freq = dev->oob_go_neg_freq;
	if (freq <= 0) {
		p2p_dbg(p2p, "No Listen/Operating frequency known for the peer "
			MACSTR " to send GO Negotiation Request",
			MAC2STR(dev->info.p2p_device_addr));
		return -1;
	}

	req = p2p_build_go_neg_req(p2p, dev);
	if (req == NULL)
		return -1;
	p2p_dbg(p2p, "Sending GO Negotiation Request");
	p2p_set_state(p2p, P2P_CONNECT);	//设置P2P模块的状态为 P2P_CONNECT
	//设置pending_action_state为P2P_PENDING_GO_NEG_REQUEST
	p2p->pending_action_state = P2P_PENDING_GO_NEG_REQUEST;
	p2p->go_neg_peer = dev;	//设置GON对端设备
	dev->flags |= P2P_DEV_WAIT_GO_NEG_RESPONSE;
	dev->connect_reqs++;
	//发送GON Request帧
	if (p2p_send_action(p2p, freq, dev->info.p2p_device_addr,
			    p2p->cfg->dev_addr, dev->info.p2p_device_addr,
			    wpabuf_head(req), wpabuf_len(req), 500) < 0) {
		p2p_dbg(p2p, "Failed to send Action frame");
		/* Use P2P find to recover and retry */
		p2p_set_timeout(p2p, 0, 0);
	} else
		dev->go_neg_req_sent++;

	wpabuf_free(req);

	return 0;
}
GON Response帧处理流程
根据前面对Action帧接收流程的分析可知,收到GON Response帧将在 p2p_process_go_neg_resp 函数中被处理:
android-5.1/external/wpa_supplicant_8/src/p2p/p2p_go_neg.c

void p2p_process_go_neg_resp(struct p2p_data *p2p, const u8 *sa,
			     const u8 *data, size_t len, int rx_freq)
{
	struct p2p_device *dev;
	int go = -1;
	struct p2p_message msg;
	u8 status = P2P_SC_SUCCESS;
	int freq;

	p2p_dbg(p2p, "Received GO Negotiation Response from " MACSTR
		" (freq=%d)", MAC2STR(sa), rx_freq);
	dev = p2p_get_device(p2p, sa);
	if (dev == NULL || dev->wps_method == WPS_NOT_READY ||
	    dev != p2p->go_neg_peer) {
		p2p_dbg(p2p, "Not ready for GO negotiation with " MACSTR,
			MAC2STR(sa));
		return;
	}

	//解析GON Response帧
	if (p2p_parse(data, len, &msg))
		return;

	//一系列参数检测
	if (!(dev->flags & P2P_DEV_WAIT_GO_NEG_RESPONSE)) {
		p2p_dbg(p2p, "Was not expecting GO Negotiation Response - ignore");
		p2p_parse_free(&msg);
		return;
	}
	dev->flags &= ~P2P_DEV_WAIT_GO_NEG_RESPONSE;

	if (msg.dialog_token != dev->dialog_token) {
		p2p_dbg(p2p, "Unexpected Dialog Token %u (expected %u)",
			msg.dialog_token, dev->dialog_token);
		p2p_parse_free(&msg);
		return;
	}

	if (!msg.status) {
		p2p_dbg(p2p, "No Status attribute received");
		status = P2P_SC_FAIL_INVALID_PARAMS;
		goto fail;
	}
	if (*msg.status) {
		p2p_dbg(p2p, "GO Negotiation rejected: status %d", *msg.status);
		dev->go_neg_req_sent = 0;
		if (*msg.status == P2P_SC_FAIL_INFO_CURRENTLY_UNAVAILABLE) {
			p2p_dbg(p2p, "Wait for the peer to become ready for GO Negotiation");
			dev->flags |= P2P_DEV_NOT_YET_READY;
			os_get_reltime(&dev->go_neg_wait_started);
			if (p2p->state == P2P_CONNECT_LISTEN)
				p2p_set_state(p2p, P2P_WAIT_PEER_CONNECT);
			else
				p2p_set_state(p2p, P2P_WAIT_PEER_IDLE);
			p2p_set_timeout(p2p, 0, 0);
		} else {
			p2p_dbg(p2p, "Stop GO Negotiation attempt");
			p2p_go_neg_failed(p2p, dev, *msg.status);
		}
		p2p->cfg->send_action_done(p2p->cfg->cb_ctx);
		p2p_parse_free(&msg);
		return;
	}

	if (!msg.capability) {
		p2p_dbg(p2p, "Mandatory Capability attribute missing from GO Negotiation Response");
#ifdef CONFIG_P2P_STRICT
		status = P2P_SC_FAIL_INVALID_PARAMS;
		goto fail;
#endif /* CONFIG_P2P_STRICT */
	}

	if (!msg.p2p_device_info) {
		p2p_dbg(p2p, "Mandatory P2P Device Info attribute missing from GO Negotiation Response");
#ifdef CONFIG_P2P_STRICT
		status = P2P_SC_FAIL_INVALID_PARAMS;
		goto fail;
#endif /* CONFIG_P2P_STRICT */
	}

	if (!msg.intended_addr) {
		p2p_dbg(p2p, "No Intended P2P Interface Address attribute received");
		status = P2P_SC_FAIL_INVALID_PARAMS;
		goto fail;
	}

	if (!msg.go_intent) {
		p2p_dbg(p2p, "No GO Intent attribute received");
		status = P2P_SC_FAIL_INVALID_PARAMS;
		goto fail;
	}
	if ((*msg.go_intent >> 1) > P2P_MAX_GO_INTENT) {
		p2p_dbg(p2p, "Invalid GO Intent value (%u) received",
			*msg.go_intent >> 1);
		status = P2P_SC_FAIL_INVALID_PARAMS;
		goto fail;
	}

	//下面这个函数将计算谁来扮演GO。返回值大于0,表示本机扮演GO,返回-1表示双方都想成为GO,返回值为0,表示对端扮演GO。我们假设GO为本机设备。
	go = p2p_go_det(p2p->go_intent, *msg.go_intent);
	if (go < 0) {
		p2p_dbg(p2p, "Incompatible GO Intent");
		status = P2P_SC_FAIL_INCOMPATIBLE_PARAMS;
		goto fail;
	}

	if (!go && msg.group_id) {
		/* Store SSID for Provisioning step */
		p2p->ssid_len = msg.group_id_len - ETH_ALEN;
		os_memcpy(p2p->ssid, msg.group_id + ETH_ALEN, p2p->ssid_len);
	} else if (!go) {
		p2p_dbg(p2p, "Mandatory P2P Group ID attribute missing from GO Negotiation Response");
		p2p->ssid_len = 0;
		status = P2P_SC_FAIL_INVALID_PARAMS;
		goto fail;
	}

	if (!msg.config_timeout) {
		p2p_dbg(p2p, "Mandatory Configuration Timeout attribute missing from GO Negotiation Response");
#ifdef CONFIG_P2P_STRICT
		status = P2P_SC_FAIL_INVALID_PARAMS;
		goto fail;
#endif /* CONFIG_P2P_STRICT */
	} else {
		dev->go_timeout = msg.config_timeout[0];
		dev->client_timeout = msg.config_timeout[1];
	}

	if (!msg.operating_channel && !go) {
		/*
		 * Note: P2P Client may omit Operating Channel attribute to
		 * indicate it does not have a preference.
		 */
		p2p_dbg(p2p, "No Operating Channel attribute received");
		status = P2P_SC_FAIL_INVALID_PARAMS;
		goto fail;
	}
	if (!msg.channel_list) {
		p2p_dbg(p2p, "No Channel List attribute received");
		status = P2P_SC_FAIL_INVALID_PARAMS;
		goto fail;
	}

	if (p2p_peer_channels(p2p, dev, msg.channel_list,
			      msg.channel_list_len) < 0) {
		p2p_dbg(p2p, "No common channels found");
		status = P2P_SC_FAIL_NO_COMMON_CHANNELS;
		goto fail;
	}

	if (msg.operating_channel) {
		dev->oper_freq = p2p_channel_to_freq(msg.operating_channel[3],
						     msg.operating_channel[4]);
		p2p_dbg(p2p, "Peer operating channel preference: %d MHz",
			dev->oper_freq);
	} else
		dev->oper_freq = 0;

	switch (msg.dev_password_id) {
	case DEV_PW_REGISTRAR_SPECIFIED:
		p2p_dbg(p2p, "PIN from peer Display");
		if (dev->wps_method != WPS_PIN_KEYPAD) {
			p2p_dbg(p2p, "We have wps_method=%s -> incompatible",
				p2p_wps_method_str(dev->wps_method));
			status = P2P_SC_FAIL_INCOMPATIBLE_PROV_METHOD;
			goto fail;
		}
		break;
	case DEV_PW_USER_SPECIFIED:
		p2p_dbg(p2p, "Peer entered PIN on Keypad");
		if (dev->wps_method != WPS_PIN_DISPLAY) {
			p2p_dbg(p2p, "We have wps_method=%s -> incompatible",
				p2p_wps_method_str(dev->wps_method));
			status = P2P_SC_FAIL_INCOMPATIBLE_PROV_METHOD;
			goto fail;
		}
		break;
	case DEV_PW_PUSHBUTTON:
		p2p_dbg(p2p, "Peer using pushbutton");
		if (dev->wps_method != WPS_PBC) {
			p2p_dbg(p2p, "We have wps_method=%s -> incompatible",
				p2p_wps_method_str(dev->wps_method));
			status = P2P_SC_FAIL_INCOMPATIBLE_PROV_METHOD;
			goto fail;
		}
		break;
	default:
		if (msg.dev_password_id &&
		    msg.dev_password_id == dev->oob_pw_id) {
			p2p_dbg(p2p, "Peer using NFC");
			if (dev->wps_method != WPS_NFC) {
				p2p_dbg(p2p, "We have wps_method=%s -> incompatible",
					p2p_wps_method_str(dev->wps_method));
				status = P2P_SC_FAIL_INCOMPATIBLE_PROV_METHOD;
				goto fail;
			}
			break;
		}
		p2p_dbg(p2p, "Unsupported Device Password ID %d",
			msg.dev_password_id);
		status = P2P_SC_FAIL_INCOMPATIBLE_PROV_METHOD;
		goto fail;
	}

	if (go && p2p_go_select_channel(p2p, dev, &status) < 0)
		goto fail;

	p2p_set_state(p2p, P2P_GO_NEG);//设置P2P模块的状态为P2P_GO_NEG
	p2p_clear_timeout(p2p);

	p2p_dbg(p2p, "GO Negotiation with " MACSTR, MAC2STR(sa));
	os_memcpy(dev->intended_addr, msg.intended_addr, ETH_ALEN);

fail:
	/* Store GO Negotiation Confirmation to allow retransmission */
	wpabuf_free(dev->go_neg_conf);
	//构造GON Confirmation帧
	dev->go_neg_conf = p2p_build_go_neg_conf(p2p, dev, msg.dialog_token,
						 status, msg.operating_channel,
						 go);
	p2p_parse_free(&msg);
	if (dev->go_neg_conf == NULL)
		return;
	p2p_dbg(p2p, "Sending GO Negotiation Confirm");
	if (status == P2P_SC_SUCCESS) {
		p2p->pending_action_state = P2P_PENDING_GO_NEG_CONFIRM;
		dev->go_state = go ? LOCAL_GO : REMOTE_GO;
	} else
		p2p->pending_action_state = P2P_NO_PENDING_ACTION;
	if (rx_freq > 0)
		freq = rx_freq;
	else
		freq = dev->listen_freq;

	dev->go_neg_conf_freq = freq;
	dev->go_neg_conf_sent = 0;

	//发送GON Confirmation帧
	if (p2p_send_action(p2p, freq, sa, p2p->cfg->dev_addr, sa,
			    wpabuf_head(dev->go_neg_conf),
			    wpabuf_len(dev->go_neg_conf), 200) < 0) {
		p2p_dbg(p2p, "Failed to send Action frame");
		p2p_go_neg_failed(p2p, dev, -1);
		p2p->cfg->send_action_done(p2p->cfg->cb_ctx);
	} else
		dev->go_neg_conf_sent++;
	if (status != P2P_SC_SUCCESS) {
		p2p_dbg(p2p, "GO Negotiation failed");
		p2p_go_neg_failed(p2p, dev, status);
	}
}
当GON Confirmation帧发送出去后,wifi driver将向WPAS发送一个NL80211_CMD_FRAME_TX_STATUS消息,而该消息将导致driver wrapper发送 EVENT_TX_STATUS 消息给 WPAS。下面来看 EVENT_TX_STATUS 的处理流程。
在event.c中,和P2P以及 EVENT_TX_STATUS相关的处理函数是 offchannel_send_action_tx_status :
android-5.1/external/wpa_supplicant_8/wpa_supplicant/offchannel.c

void offchannel_send_action_tx_status(
	struct wpa_supplicant *wpa_s, const u8 *dst, const u8 *data,
	size_t data_len, enum offchannel_send_action_result result)
{
	if (wpa_s->pending_action_tx == NULL) {
		wpa_printf(MSG_DEBUG, "Off-channel: Ignore Action TX status - "
			   "no pending operation");
		return;
	}

	if (os_memcmp(dst, wpa_s->pending_action_dst, ETH_ALEN) != 0) {
		wpa_printf(MSG_DEBUG, "Off-channel: Ignore Action TX status - "
			   "unknown destination address");
		return;
	}

	/* Accept report only if the contents of the frame matches */
	if (data_len - wpabuf_len(wpa_s->pending_action_tx) != 24 ||
	    os_memcmp(data + 24, wpabuf_head(wpa_s->pending_action_tx),
		      wpabuf_len(wpa_s->pending_action_tx)) != 0) {
		wpa_printf(MSG_DEBUG, "Off-channel: Ignore Action TX status - "
				   "mismatching contents with pending frame");
		wpa_hexdump(MSG_MSGDUMP, "TX status frame data",
			    data, data_len);
		wpa_hexdump_buf(MSG_MSGDUMP, "Pending TX frame",
				wpa_s->pending_action_tx);
		return;
	}

	wpa_printf(MSG_DEBUG, "Off-channel: Delete matching pending action frame");

	wpabuf_free(wpa_s->pending_action_tx);
	wpa_s->pending_action_tx = NULL;

	wpa_printf(MSG_DEBUG, "Off-channel: TX status result=%d cb=%p",
		   result, wpa_s->pending_action_tx_status_cb);

	//注意函数指针pending_action_tx_status_cb,P2P每次发送Action的时候都会设置该变量。其真实函数为 wpas_p2p_send_action_tx_status(在 wpas_send_action 函数中设置)
	if (wpa_s->pending_action_tx_status_cb) {
		wpa_s->pending_action_tx_status_cb(
			wpa_s, wpa_s->pending_action_freq,
			wpa_s->pending_action_dst, wpa_s->pending_action_src,
			wpa_s->pending_action_bssid,
			data, data_len, result);
	}

	if (wpa_s->p2p_long_listen > 0) {
		/* Continue the listen */
		wpa_printf(MSG_DEBUG, "P2P: Continuing long Listen state");
		wpas_p2p_listen_start(wpa_s, wpa_s->p2p_long_listen);
	}
}
android-5.1/external/wpa_supplicant_8/wpa_supplicant/p2p_supplicant.c

static void wpas_p2p_send_action_tx_status(struct wpa_supplicant *wpa_s,
					   unsigned int freq,
					   const u8 *dst, const u8 *src,
					   const u8 *bssid,
					   const u8 *data, size_t data_len,
					   enum offchannel_send_action_result
					   result)
{
	enum p2p_send_action_result res = P2P_SEND_ACTION_SUCCESS;

	wpas_p2p_action_tx_clear(wpa_s);

	if (wpa_s->global->p2p == NULL || wpa_s->global->p2p_disabled)
		return;

	switch (result) {
	case OFFCHANNEL_SEND_ACTION_SUCCESS:
		res = P2P_SEND_ACTION_SUCCESS;
		break;
	case OFFCHANNEL_SEND_ACTION_NO_ACK:
		res = P2P_SEND_ACTION_NO_ACK;
		break;
	case OFFCHANNEL_SEND_ACTION_FAILED:
		res = P2P_SEND_ACTION_FAILED;
		break;
	}

	p2p_send_action_cb(wpa_s->global->p2p, freq, dst, src, bssid, res);

	if (result != OFFCHANNEL_SEND_ACTION_SUCCESS &&
	    wpa_s->pending_pd_before_join &&
	    (os_memcmp(dst, wpa_s->pending_join_dev_addr, ETH_ALEN) == 0 ||
	     os_memcmp(dst, wpa_s->pending_join_iface_addr, ETH_ALEN) == 0) &&
	    wpa_s->p2p_fallback_to_go_neg) {
		wpa_s->pending_pd_before_join = 0;
		wpa_dbg(wpa_s, MSG_DEBUG, "P2P: No ACK for PD Req "
			"during p2p_connect-auto");
		wpas_p2p_fallback_to_go_neg(wpa_s, 0);
		return;
	}
}
android-5.1/external/wpa_supplicant_8/src/p2p/p2p.c

void p2p_send_action_cb(struct p2p_data *p2p, unsigned int freq, const u8 *dst,
			const u8 *src, const u8 *bssid,
			enum p2p_send_action_result result)
{
	enum p2p_pending_action_state state;
	int success;

	p2p_dbg(p2p, "Action frame TX callback (state=%d freq=%u dst=" MACSTR
		" src=" MACSTR " bssid=" MACSTR " result=%d",
		p2p->pending_action_state, freq, MAC2STR(dst), MAC2STR(src),
		MAC2STR(bssid), result);
	success = result == P2P_SEND_ACTION_SUCCESS;
	//pending_action_state应该是P2P_PENDING_GO_NEG_CONFIRM
	state = p2p->pending_action_state;
	p2p->pending_action_state = P2P_NO_PENDING_ACTION;
	switch (state) {
	case P2P_NO_PENDING_ACTION:
		if (p2p->send_action_in_progress) {
			p2p->send_action_in_progress = 0;
			p2p->cfg->send_action_done(p2p->cfg->cb_ctx);
		}
		if (p2p->after_scan_tx_in_progress) {
			p2p->after_scan_tx_in_progress = 0;
			if (p2p->start_after_scan != P2P_AFTER_SCAN_NOTHING &&
			    p2p_run_after_scan(p2p))
				break;
			if (p2p->state == P2P_SEARCH) {
				p2p_dbg(p2p, "Continue find after after_scan_tx completion");
				p2p_continue_find(p2p);
			}
		}
		break;
	case P2P_PENDING_GO_NEG_REQUEST:
		//当发送完GON Request帧后,此函数也会触发
		p2p_go_neg_req_cb(p2p, success);
		break;
	case P2P_PENDING_GO_NEG_RESPONSE:
		p2p_go_neg_resp_cb(p2p, success);
		break;
	case P2P_PENDING_GO_NEG_RESPONSE_FAILURE:
		p2p_go_neg_resp_failure_cb(p2p, success, dst);
		break;
	case P2P_PENDING_GO_NEG_CONFIRM:
		p2p_go_neg_conf_cb(p2p, result);
		break;
	case P2P_PENDING_SD:
		p2p_sd_cb(p2p, success);
		break;
	case P2P_PENDING_PD:
		p2p_prov_disc_cb(p2p, success);
		break;
	case P2P_PENDING_INVITATION_REQUEST:
		p2p_invitation_req_cb(p2p, success);
		break;
	case P2P_PENDING_INVITATION_RESPONSE:
		p2p_invitation_resp_cb(p2p, success);
		break;
	case P2P_PENDING_DEV_DISC_REQUEST:
		p2p_dev_disc_req_cb(p2p, success);
		break;
	case P2P_PENDING_DEV_DISC_RESPONSE:
		p2p_dev_disc_resp_cb(p2p, success);
		break;
	case P2P_PENDING_GO_DISC_REQ:
		p2p_go_disc_req_cb(p2p, success);
		break;
	}

	p2p->after_scan_tx_in_progress = 0;
}
android-5.1/external/wpa_supplicant_8/src/p2p/p2p.c

static void p2p_go_neg_conf_cb(struct p2p_data *p2p,
			       enum p2p_send_action_result result)
{
	struct p2p_device *dev;

	p2p_dbg(p2p, "GO Negotiation Confirm TX callback: result=%d", result);
	if (result == P2P_SEND_ACTION_FAILED) {
		p2p->cfg->send_action_done(p2p->cfg->cb_ctx);
		p2p_go_neg_failed(p2p, p2p->go_neg_peer, -1);
		return;
	}

	dev = p2p->go_neg_peer;

	if (result == P2P_SEND_ACTION_NO_ACK) {
		/*
		 * Retry GO Negotiation Confirmation
		 * P2P_GO_NEG_CNF_MAX_RETRY_COUNT times if we did not receive
		 * ACK for confirmation.
		 */
		if (dev && dev->go_neg_conf &&
		    dev->go_neg_conf_sent <= P2P_GO_NEG_CNF_MAX_RETRY_COUNT) {
			p2p_dbg(p2p, "GO Negotiation Confirm retry %d",
				dev->go_neg_conf_sent);
			p2p->pending_action_state = P2P_PENDING_GO_NEG_CONFIRM;
			if (p2p_send_action(p2p, dev->go_neg_conf_freq,
					    dev->info.p2p_device_addr,
					    p2p->cfg->dev_addr,
					    dev->info.p2p_device_addr,
					    wpabuf_head(dev->go_neg_conf),
					    wpabuf_len(dev->go_neg_conf), 0) >=
			    0) {
				dev->go_neg_conf_sent++;
				return;
			}
			p2p_dbg(p2p, "Failed to re-send Action frame");

			/*
			 * Continue with the assumption that the first attempt
			 * went through and just the ACK frame was lost.
			 */
		}

		/*
		 * It looks like the TX status for GO Negotiation Confirm is
		 * often showing failure even when the peer has actually
		 * received the frame. Since the peer may change channels
		 * immediately after having received the frame, we may not see
		 * an Ack for retries, so just dropping a single frame may
		 * trigger this. To allow the group formation to succeed if the
		 * peer did indeed receive the frame, continue regardless of
		 * the TX status.
		 */
		p2p_dbg(p2p, "Assume GO Negotiation Confirm TX was actually received by the peer even though Ack was not reported");
	}

	//send_action_done真实函数是 wpas_send_action_done
	p2p->cfg->send_action_done(p2p->cfg->cb_ctx);

	if (dev == NULL)
		return;

	p2p_go_complete(p2p, dev);
}

android-5.1/external/wpa_supplicant_8/src/p2p/p2p.c

void p2p_go_complete(struct p2p_data *p2p, struct p2p_device *peer)
{
	struct p2p_go_neg_results res;
	int go = peer->go_state == LOCAL_GO;
	struct p2p_channels intersection;
	int freqs;
	size_t i, j;

	p2p_dbg(p2p, "GO Negotiation with " MACSTR " completed (%s will be GO)",
		MAC2STR(peer->info.p2p_device_addr), go ? "local end" : "peer");

	os_memset(&res, 0, sizeof(res));
	res.role_go = go;
	os_memcpy(res.peer_device_addr, peer->info.p2p_device_addr, ETH_ALEN);
	os_memcpy(res.peer_interface_addr, peer->intended_addr, ETH_ALEN);
	res.wps_method = peer->wps_method;
	if (peer->flags & P2P_DEV_PREFER_PERSISTENT_GROUP) {
		if (peer->flags & P2P_DEV_PREFER_PERSISTENT_RECONN)
			res.persistent_group = 2;
		else
			res.persistent_group = 1;
	}

	if (go) {
		/* Setup AP mode for WPS provisioning */
		res.freq = p2p_channel_to_freq(p2p->op_reg_class,
					       p2p->op_channel);
		os_memcpy(res.ssid, p2p->ssid, p2p->ssid_len);
		res.ssid_len = p2p->ssid_len;
		p2p_random(res.passphrase, p2p->cfg->passphrase_len);
	} else {
		res.freq = peer->oper_freq;
		if (p2p->ssid_len) {
			os_memcpy(res.ssid, p2p->ssid, p2p->ssid_len);
			res.ssid_len = p2p->ssid_len;
		}
	}

	p2p_channels_dump(p2p, "own channels", &p2p->channels);
	p2p_channels_dump(p2p, "peer channels", &peer->channels);
	p2p_channels_intersect(&p2p->channels, &peer->channels,
			       &intersection);
	if (go) {
		p2p_channels_remove_freqs(&intersection, &p2p->no_go_freq);
		p2p_channels_dump(p2p, "intersection after no-GO removal",
				  &intersection);
	}
	freqs = 0;
	for (i = 0; i < intersection.reg_classes; i++) {
		struct p2p_reg_class *c = &intersection.reg_class[i];
		if (freqs + 1 == P2P_MAX_CHANNELS)
			break;
		for (j = 0; j < c->channels; j++) {
			int freq;
			if (freqs + 1 == P2P_MAX_CHANNELS)
				break;
			freq = p2p_channel_to_freq(c->reg_class, c->channel[j]);
			if (freq < 0)
				continue;
			res.freq_list[freqs++] = freq;
		}
	}

	res.peer_config_timeout = go ? peer->client_timeout : peer->go_timeout;

	p2p_clear_timeout(p2p);
	p2p->ssid_set = 0;
	peer->go_neg_req_sent = 0;
	peer->wps_method = WPS_NOT_READY;
	peer->oob_pw_id = 0;
	wpabuf_free(peer->go_neg_conf);
	peer->go_neg_conf = NULL;

	p2p_set_state(p2p, P2P_PROVISIONING);//进入Group Formation的Provisioning阶段
	//go_neg_completed 对应的函数是 wpas_go_neg_completed
	p2p->cfg->go_neg_completed(p2p->cfg->cb_ctx, &res);
}
android-5.1/external/wpa_supplicant_8/wpa_supplicant/p2p_supplicant.c

static void wpas_go_neg_completed(void *ctx, struct p2p_go_neg_results *res)
{
	struct wpa_supplicant *wpa_s = ctx;

	if (wpa_s->off_channel_freq || wpa_s->roc_waiting_drv_freq) {
		wpa_drv_cancel_remain_on_channel(wpa_s);
		wpa_s->off_channel_freq = 0;
		wpa_s->roc_waiting_drv_freq = 0;
	}

	if (res->status) {
		wpa_msg_global(wpa_s, MSG_INFO,
			       P2P_EVENT_GO_NEG_FAILURE "status=%d",
			       res->status);
		wpas_notify_p2p_go_neg_completed(wpa_s, res);
		wpas_p2p_remove_pending_group_interface(wpa_s);
		return;
	}

	if (wpa_s->p2p_go_ht40)
		res->ht40 = 1;
	if (wpa_s->p2p_go_vht)
		res->vht = 1;

	//下面这个函数将导致P2pStateMachine受到 P2P_GO_NEGOTIATION_SUCCESS_EVENT消息
	wpa_msg_global(wpa_s, MSG_INFO, P2P_EVENT_GO_NEG_SUCCESS "role=%s "
		       "freq=%d ht40=%d peer_dev=" MACSTR " peer_iface=" MACSTR
		       " wps_method=%s",
		       res->role_go ? "GO" : "client", res->freq, res->ht40,
		       MAC2STR(res->peer_device_addr),
		       MAC2STR(res->peer_interface_addr),
		       p2p_wps_method_text(res->wps_method));
	wpas_notify_p2p_go_neg_completed(wpa_s, res);

	if (res->role_go && wpa_s->p2p_persistent_id >= 0) {
		struct wpa_ssid *ssid;
		ssid = wpa_config_get_network(wpa_s->conf,
					      wpa_s->p2p_persistent_id);
		if (ssid && ssid->disabled == 2 &&
		    ssid->mode == WPAS_MODE_P2P_GO && ssid->passphrase) {
			size_t len = os_strlen(ssid->passphrase);
			wpa_printf(MSG_DEBUG, "P2P: Override passphrase based "
				   "on requested persistent group");
			os_memcpy(res->passphrase, ssid->passphrase, len);
			res->passphrase[len] = '\0';
		}
	}

	if (wpa_s->create_p2p_iface) {
		//再创建一个wpa_supplicant对象。其内部将调用 wpa_supplicant_add_iface函数
		struct wpa_supplicant *group_wpa_s =
			wpas_p2p_init_group_interface(wpa_s, res->role_go);
		if (group_wpa_s == NULL) {
			wpas_p2p_remove_pending_group_interface(wpa_s);
			eloop_cancel_timeout(wpas_p2p_long_listen_timeout,
					     wpa_s, NULL);
			wpas_p2p_group_formation_failed(wpa_s);
			return;
		}
		if (group_wpa_s != wpa_s) {
			os_memcpy(group_wpa_s->p2p_pin, wpa_s->p2p_pin,
				  sizeof(group_wpa_s->p2p_pin));
			group_wpa_s->p2p_wps_method = wpa_s->p2p_wps_method;
		}
		os_memset(wpa_s->pending_interface_addr, 0, ETH_ALEN);
		wpa_s->pending_interface_name[0] = '\0';
		group_wpa_s->p2p_in_provisioning = 1;

		//如果本机扮演GO,则启动WSC Registrar功能,否则启动Enrollee功能
		if (res->role_go)
			wpas_start_wps_go(group_wpa_s, res, 1);
		else
			wpas_start_wps_enrollee(group_wpa_s, res);
	} else {
		wpa_s->p2p_in_provisioning = 1;
		wpa_s->global->p2p_group_formation = wpa_s;

		if (res->role_go)
			wpas_start_wps_go(wpa_s, res, 1);
		else
			wpas_start_wps_enrollee(ctx, res);
	}

	wpa_s->p2p_long_listen = 0;
	eloop_cancel_timeout(wpas_p2p_long_listen_timeout, wpa_s, NULL);

	eloop_cancel_timeout(wpas_p2p_group_formation_timeout, wpa_s, NULL);
	//Group Formation的超时时间为15秒左右
	eloop_register_timeout(15 + res->peer_config_timeout / 100,
			       (res->peer_config_timeout % 100) * 10000,
			       wpas_p2p_group_formation_timeout, wpa_s, NULL);
}
当Group Negotiation完成后,WPAS将新创建一个wpa_supplicant对象,它将用于管理和操作专门用于P2P Group的Virtual Interface,此处有几点请注意:
一个Interface对应一个wpa_supplicant对象
此处新创建的wpa_supplicant对象用于GO,即扮演AP的角色,专门处理和P2P Group相关的事情,其MAC地址为P2P Interface Address。
之前使用的wpa_supplicant用于非P2P Group操作,其MAC地址为P2P Device Address。

  • 0
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
SFTP (Secure File Transfer Protocol) 是一种安全的文件传输协议,用于在不安全的网络中提供加密的数据传输。如果遇到 "SFTP Connect Fail! Algorithm negotiation fail" 错误,通常意味着客户端和服务器在协商加密算法时未能达成一致,导致连接失败。以下是一些可能的原因: 1. **加密算法不兼容**:SFTP支持多种加密算法,如SSH-1的DES, SSH-2的AES等。如果服务器支持的加密级别低于客户端期望的,或反之,就会发生谈判失败。 2. **SSL/TLS版本问题**:如果客户端和服务器使用的SSL/TLS版本不匹配,也可能导致算法协商错误。 3. **证书问题**:客户端可能无法识别或信任服务器的SSL/TLS证书,这会导致协议无法正常初始化。 4. **防火墙或安全软件阻止**:某些安全设备可能会阻止使用特定的加密算法,尤其是在企业环境中。 5. **系统配置错误**:客户端的SFTP客户端软件可能存在配置问题,比如没有正确设置加密算法或者协议版本。 6. **服务器配置限制**:有些服务器可能出于性能考虑或安全策略,只允许特定的加密算法。 为了排查问题,你可以按照以下步骤操作: 1. **检查服务器设置**:确保服务器启用并支持的加密算法与客户端兼容。 2. **查看日志**:查看服务器端和客户端的SFTP日志,寻找关于失败协商的具体信息。 3. **更新客户端**:确保使用的SFTP客户端软件是最新版本,并且加密库也是最新的。 4. **临时关闭防火墙**:测试连接是否能成功,以排除防火墙或安全软件的干扰。 5. **检查SSL/TLS配置**:确认双方使用的SSL/TLS版本是否在允许范围内。 如果你需要更详细的帮助,可能需要提供更具体的错误信息或环境描述。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值