一个快速灵活的IPv4和IPv6非递归DNS服务器(C/C++代码实现)

在网络世界中,DNS(域名系统)在将人类友好的域名转换为计算机可以理解的IP地址方面发挥着关键作用。然而,设置和管理DNS服务器可能相当复杂,特别是当你需要一个既快速又灵活的解决方案时。这就是quick DNS的用武之地,它是一个轻量级且高效的DNS服务器,能够处理IPv4和IPv6查询,甚至可以在没有将地址绑定到接口的情况下工作。

概述

quick dns旨在提供一个非递归DNS服务器解决方案。它被设计为简单易设置和操作,即使在没有静态IP地址的环境中也能工作。服务器基于本地区域数据库响应DNS查询。

核心特性

  • 支持IPv4和IPv6:能够处理IPv4和IPv6查询,使其成为现代网络的多功能选择。
  • 非递归操作:它作为非递归服务器运行,意味着它不会将查询转发到其他DNS服务器,而是基于本地区域数据库响应。
  • 可定制性:允许通过命令行参数进行定制,使管理员能够根据特定需求调整其行为。
  • 效率:它旨在提供快速的DNS解析。

一个快速灵活的IPv4和IPv6非递归DNS服务器(C/C++代码实现)

int dns::build_error(const string &s)
{
	err = "qdns::";
	err += s;
	if (errno) {
		err += ": ";
		err += strerror(errno);
	}
	return -1;
}


int dns::init(const map<string, string> &args)
{
	if (args.count("laddr"))
		io = new (nothrow) socket_provider();
	else if (args.count("mon") > 0)
		io = new (nothrow) usipp_provider();

	if (!io)
		return build_error("init: OOM");

	if (io->init(args) < 0)
		return build_error(string("init:") + io->why());

	auto it = args.find("nxdomain");
	if (it != args.end())
		nxdomain = (strtoul(it->second.c_str(), NULL, 10) != 0);
	if (args.count("resend") > 0)
		resend = 1;

	return 0;
}


int dns::loop()
{
	if (!io)
		return build_error("loop: no IO provider initialized");

	string log = "";
	int r = 0;

	for (;;) {
		client.pkt = "";
		client.reply = "";

		if (io->recv(client.pkt) < 0) {
			cerr<<io->why()<<endl;
			continue;
		}
		src = io->sender();
		r = parse_packet(client.pkt, client.reply, log);

		if (r == 0) {
			// 返回0的回复等于重新发送的pkt
			if (io->resend(client.reply) < 0) {
				cerr<<src<<": "<<io->why()<<endl;
				continue;
			}
		} else if (r > 0) {
			if (io->reply(client.reply) < 0) {
				cerr<<src<<": "<<io->why()<<endl;
				continue;
			}
		} // in < 0 case, just log output

		cout<<src<<": "<<log<<endl;
	}

	return 0;
}


int dns::parse_packet(const string &query, string &response, string &log)
{
	using net_headers::dnshdr;
	using net_headers::dns_type;

	log = "invalid query";
	response = "";

	if (query.size() <= sizeof(dnshdr))
		return -1;

	const char *ptr = query.c_str(), *end_ptr = ptr + query.size();

	dnshdr hdr;
	memcpy(&hdr, query.c_str(), sizeof(dnshdr));
	ptr += sizeof(dnshdr);

	// dst端口53没有查询?
	if (hdr.qr != 0 || hdr.opcode != 0)
		return -1;

	if (hdr.q_count != htons(1))
		return -1;

	// skip QNAME
	auto qptr = ptr;
	while (*ptr != 0 && ptr < end_ptr)
		++ptr;
	++ptr;


	if (ptr + 2*sizeof(uint16_t) > end_ptr)
		return -1;

	uint16_t qtype = 0;
	memcpy(&qtype, ptr, sizeof(qtype));

	string qname = string(qptr, ptr - qptr);
	string question = string(qptr, ptr + 2*sizeof(uint16_t) - qptr);
	string fqdn = "";

	if (qname2host(qname, fqdn) <= 0)
		return -1;

	switch (ntohs(qtype)) {
	case dns_type::A:
		log = "A? ";
		break;
	case dns_type::AAAA:
		log = "AAAA? ";
		break;
	case dns_type::MX:
		log = "MX? ";
		break;
	case dns_type::CNAME:
		log = "CNAME? ";
		break;
	case dns_type::NS:
		log = "NS? ";
		break;
	case dns_type::PTR:
		log = "PTR? ";
		break;
	case dns_type::SRV:
		log = "SRV? ";
		break;
	case dns_type::TXT:
		log = "TXT? ";
		break;
	default:
		char s[32];
		snprintf(s, sizeof(s), "%d? ", ntohs(qtype));
		log = s;
	}

	log += fqdn;
	log += " -> ";

	bool found_domain = 1;
	auto it1 = exact_matches.find(make_pair(qname, qtype)), lit = it1;

	if (lit == exact_matches.end()) {
		string::size_type pos = string::npos, minpos = string::npos;

		// 尝试找到最大的子字符串匹配
		for (auto it2 = wild_matches.begin(); it2 != wild_matches.end(); ++it2) {
			if ((pos = qname.find(it2->first.first)) == string::npos || it2->first.second != qtype)
				continue;
			if (pos < minpos && pos + it2->first.first.size() == qname.size()) {
				minpos = pos;
				lit = it2;
			}
		}

		// 如果没有找到条目,NXDOMAIN
		if (minpos == string::npos) {
			found_domain = 0;
			log += "NDXOMAIN ";
			auto it3 = exact_matches.find(make_pair(string("\x9[forward]\0", 11), htons(dns_type::SOA)));
			if (it3 != exact_matches.end())
				lit = it3;

			// if -R was given, we are firewalling router,
			// so resend in case we cant resolve ourself
			if (resend) {
				log += "(resend)";
				response = query;
				return 0;
			}

			// NXDOMAIN answers prohibited (-X)
			if (!nxdomain) {
				log += "(nosend)";
				return -1;
			}
		}
	}
	if (lit == exact_matches.end()) {
		log += "no [forward], (nosend)";
		return -1;
	}

	list<match *> &l = lit->second;

	if (l.size() == 0) {
		log += "NULL match. Missing -X?";
		return -1;
	}

	match *m = l.front();

	// TTL为1表示,只处理此客户端src一次
	if (l.size() == 1 && m->ttl == htonl(1)) {
		if (once.count(src) > 0) {
			log += "(once, nosend)";
			return -1;
		}
		once[src] = 1;
	}

	log += m->field;

	// reply-hdr
	dnshdr rhdr;
	memcpy(&rhdr, &hdr, sizeof(hdr));
	rhdr.qr = 1;
	rhdr.aa = 0;
	rhdr.tc = 0;
	rhdr.ra = 0;
	rhdr.unused = 0;
	if (!found_domain)
		rhdr.rcode = 3;
	else
		rhdr.rcode = 0;
	rhdr.q_count = hdr.q_count;
	rhdr.a_count = m->a_count;
	rhdr.rra_count = m->rra_count;
	rhdr.ad_count = m->ad_count;

	response = string((char *)&rhdr, sizeof(rhdr));
	response += question;
	response += m->rr;


	if (l.size() > 1) {
		l.push_back(m);
		l.pop_front();
	}

	return 1;
}


int dns::parse_zone(const string &file)
{
...

	while (fgets(buf, sizeof(buf), f)) {

		if (rr_kind == RR_KIND_MATCHING) {
			link_rr = "";
			dltype = 0;
		}

		memset(rr, 0, sizeof(rr));
		rr_ptr = rr;
		dname = "";
		dlname = "";

		ptr = buf;
		while (*ptr == ' ' || *ptr == '\t')
			++ptr;
		if (*ptr == ';' || *ptr == '\n')
			continue;

		if (*ptr == '@') {
			// wrong format? ignore!
			if (sscanf(ptr + 1, "%255[^ \t]%*[ \t]%255[^ \t;\n]", name, ltype) != 2)
				link_rr = "";
			else {
				link_rr = name;
				rr_kind = RR_KIND_LINKING;
			}
			continue;
		}

		if (sscanf(ptr, "%255[^ \t]%*[ \t]%255[^ \t]%*[ \t]IN%*[ \t]%255[^ \t]%*[ \t]%255[^ \t;\n]", name, ttlb, type, field) != 4)
			continue;

		rr_kind = RR_KIND_MATCHING;



		if (host2qname(name, dname) <= 0)
			continue;
		if (dname.size() > 255)
			continue;

		// DNS type of current entry
		if (strcasecmp(type, "A") == 0) {
			dtype = htons(dns_type::A);
		} else if (strcasecmp(type, "MX") == 0) {
			dtype = htons(dns_type::MX);
		} else if (strcasecmp(type, "AAAA") == 0) {
			dtype = htons(dns_type::AAAA);
		} else if (strcasecmp(type, "NS") == 0) {
			dtype = htons(dns_type::NS);
		} else if (strcasecmp(type, "CNAME") == 0) {
			dtype = htons(dns_type::CNAME);
		} else if (strcasecmp(type, "SOA") == 0) {
			dtype = htons(dns_type::SOA);
		} else if (strcasecmp(type, "SRV") == 0) {
			dtype = htons(dns_type::SRV);
		} else if (strcasecmp(type, "TXT") == 0) {
			dtype = htons(dns_type::TXT);
		} else if (strcasecmp(type, "PTR") == 0) {
			dtype = htons(dns_type::PTR);
		} else
			continue;

		ttl = htonl(strtoul(ttlb, NULL, 10));

		match *m = nullptr;

		// 如果链接到现有RR,则使用现有匹配
		if (link_rr.size() > 0) {
			if (host2qname(link_rr, dlname) <= 0)
				continue;

			// RR的DNS类型
			if (strcasecmp(ltype, "A") == 0) {
				dltype = htons(dns_type::A);
			} else if (strcasecmp(ltype, "MX") == 0) {
				dltype = htons(dns_type::MX);
			} else if (strcasecmp(ltype, "AAAA") == 0) {
				dltype = htons(dns_type::AAAA);
			} else if (strcasecmp(ltype, "NS") == 0) {
				dltype = htons(dns_type::NS);
			} else if (strcasecmp(ltype, "CNAME") == 0) {
				dltype = htons(dns_type::CNAME);
			} else if (strcasecmp(ltype, "SOA") == 0) {
				dltype = htons(dns_type::SOA);
			} else if (strcasecmp(ltype, "SRV") == 0) {
				dltype = htons(dns_type::SRV);
			} else if (strcasecmp(ltype, "TXT") == 0) {
				dltype = htons(dns_type::TXT);
			} else if (strcasecmp(ltype, "PTR") == 0) {
				dltype = htons(dns_type::PTR);
			} else
				continue;

			if (exact_matches.count(make_pair(dlname, dltype)) > 0)
				m = exact_matches.find(make_pair(dlname, dltype))->second.back();
			else if (wild_matches.count(make_pair(dlname, dltype)) > 0)
				m = wild_matches.find(make_pair(dlname, dltype))->second.back();
			else
				continue;

			memcpy(rr_ptr, dname.c_str(), dname.size());
			rr_ptr += dname.size();
		} else {
			m = new match;

			m->field = field;

			if (name[0] == '*') {
				off = 1;
				if (name[1] == '.')
					off = 2;
				memmove(name, name + off, sizeof(name) - off);
				m->mtype = QDNS_MATCH_WILD;

				if (host2qname(name, dname) <= 0)
					continue;
				if (dname.size() > 255)
					continue;

				// 通配符匹配前面的字节数错误
				dname.erase(0, 1);
			} else
				m->mtype = QDNS_MATCH_EXACT;

			memcpy(rr_ptr, &clbl, sizeof(clbl));
			rr_ptr += sizeof(clbl);

			m->fqdn = name;

			// DNS encoded name
			m->name = dname;

			// TTL
			m->ttl = ttl;

			m->type = dtype;
			m->a_count = 0;
			m->ad_count = 0;
			m->rra_count = 0;
		}

		uint16_t prt = 0;

		switch (ntohs(dtype)) {
		case dns_type::A:
			in_addr in;
			if (inet_pton(AF_INET, field, &in) != 1)
				continue;
			// construct RR as per RFC
			rlen = htons(4);
			memcpy(rr_ptr, &dtype, sizeof(dtype));
			rr_ptr += sizeof(dtype);
			memcpy(rr_ptr, &dclass, sizeof(dclass));
			rr_ptr += sizeof(dclass);
			memcpy(rr_ptr, &ttl, sizeof(ttl));
			rr_ptr += sizeof(ttl);
			memcpy(rr_ptr, &rlen, sizeof(rlen));
			rr_ptr += sizeof(rlen);
			memcpy(rr_ptr, &in, sizeof(in));
			rr_ptr += sizeof(in);

			if (dltype == htons(dns_type::SOA))
				m->rr = string(rr, rr_ptr - rr) + m->rr;
			else
				m->rr += string(rr, rr_ptr - rr);
			m->a_count += htons(1);
			break;

		case dns_type::MX:
			if (host2qname(field, dname) <= 0)
				continue;
			if (dname.size() > 255)
				continue;
			rlen = htons(dname.size() + sizeof(uint16_t));
			memcpy(rr_ptr, &dtype, sizeof(dtype));
			rr_ptr += sizeof(dtype);
			memcpy(rr_ptr, &dclass, sizeof(dclass));
			rr_ptr += sizeof(dclass);
			memcpy(rr_ptr, &ttl, sizeof(ttl));
			rr_ptr += sizeof(ttl);
			memcpy(rr_ptr, &rlen, sizeof(rlen));
			rr_ptr += sizeof(rlen);
			memcpy(rr_ptr, &zero, sizeof(zero));		// preference
			rr_ptr += sizeof(zero);
			memcpy(rr_ptr, dname.c_str(), dname.size());
			rr_ptr += dname.size();
			if (dltype == htons(dns_type::SOA))
				m->rr = string(rr, rr_ptr - rr) + m->rr;
			else
				m->rr += string(rr, rr_ptr - rr);
			m->a_count += htons(1);
			break;

		case dns_type::AAAA:
			in6_addr in6;
			if (inet_pton(AF_INET6, field, &in6) != 1)
				continue;
			rlen = htons(sizeof(in6));
			memcpy(rr_ptr, &dtype, sizeof(dtype));
			rr_ptr += sizeof(dtype);
			memcpy(rr_ptr, &dclass, sizeof(dclass));
			rr_ptr += sizeof(dclass);
			memcpy(rr_ptr, &ttl, sizeof(ttl));
			rr_ptr += sizeof(ttl);
			memcpy(rr_ptr, &rlen, sizeof(rlen));
			rr_ptr += sizeof(rlen);
			memcpy(rr_ptr, &in6, sizeof(in6));
			rr_ptr += sizeof(in6);
			if (dltype == htons(dns_type::SOA))
				m->rr = string(rr, rr_ptr - rr) + m->rr;
			else
				m->rr += string(rr, rr_ptr - rr);
			m->a_count += htons(1);
			break;

		case dns_type::NS:
			if (host2qname(field, dname) <= 0)
				continue;
			if (dname.size() > 255)
				continue;
			rlen = htons(dname.size());
			memcpy(rr_ptr, &dtype, sizeof(dtype));
			rr_ptr += sizeof(dtype);
			memcpy(rr_ptr, &dclass, sizeof(dclass));
			rr_ptr += sizeof(dclass);
			memcpy(rr_ptr, &ttl, sizeof(ttl));
			rr_ptr += sizeof(ttl);
			memcpy(rr_ptr, &rlen, sizeof(rlen));
			rr_ptr += sizeof(rlen);
			memcpy(rr_ptr, dname.c_str(), dname.size());
			rr_ptr += dname.size();
			if (dltype == htons(dns_type::SOA))
				m->rr = string(rr, rr_ptr - rr) + m->rr;
			else
				m->rr += string(rr, rr_ptr - rr);
			m->a_count += htons(1);
			break;

		case dns_type::CNAME:
			m->type = dtype;
			if (host2qname(field, dname) <= 0)
				continue;
			if (dname.size() > 255)
				continue;
			rlen = htons(dname.size());
			memcpy(rr_ptr, &dtype, sizeof(dtype));
			rr_ptr += sizeof(dtype);
			memcpy(rr_ptr, &dclass, sizeof(dclass));
			rr_ptr += sizeof(dclass);
			memcpy(rr_ptr, &ttl, sizeof(ttl));
			rr_ptr += sizeof(ttl);
			memcpy(rr_ptr, &rlen, sizeof(rlen));
			rr_ptr += sizeof(rlen);
			memcpy(rr_ptr, dname.c_str(), dname.size());
			rr_ptr += dname.size();
			if (dltype == htons(dns_type::SOA))
				m->rr = string(rr, rr_ptr - rr) + m->rr;
			else
				m->rr += string(rr, rr_ptr - rr);
			m->a_count += htons(1);
			break;

		// Once a SOA has been linked in, no other RR's must be linked,
		// as they must appear between answer and additional section
		case dns_type::SOA:
			m->type = dtype;
			if (host2qname(field, dname) <= 0)
				continue;
			if (dname.size() > 255)
				continue;
			rlen = htons(2*dname.size() + sizeof(soa_ints));
			memcpy(rr_ptr, &dtype, sizeof(dtype));
			rr_ptr += sizeof(dtype);
			memcpy(rr_ptr, &dclass, sizeof(dclass));
			rr_ptr += sizeof(dclass);
			memcpy(rr_ptr, &ttl, sizeof(ttl));
			rr_ptr += sizeof(ttl);
			memcpy(rr_ptr, &rlen, sizeof(rlen));
			rr_ptr += sizeof(rlen);
			memcpy(rr_ptr, dname.c_str(), dname.size());
			rr_ptr += dname.size();
			memcpy(rr_ptr, dname.c_str(), dname.size());
			rr_ptr += dname.size();
			memcpy(rr_ptr, soa_ints, sizeof(soa_ints));
			rr_ptr += sizeof(soa_ints);
			m->rr += string(rr, rr_ptr - rr);
			m->rra_count = htons(1);
			break;
		case dns_type::SRV:
			m->type = dtype;

			// avoid warning about unaligned &src.port access
			if (sscanf(field, "%255[^:]:%hu:%hu:%hu", name, &prio, &weight, &prt) != 4)
				continue;
			srv.port = prt;

			if (host2qname(name, dname) <= 0)
				continue;
			if (dname.size() > 255)
				continue;
			srv.len = htons(dname.size() + 6);
			srv.type = dtype;
			srv._class = dclass;
			srv.ttl = ttl;
			srv.prio = htons(prio);
			srv.weight = htons(weight);
			srv.port = htons(srv.port);
			memcpy(rr_ptr, &srv, sizeof(srv));
			rr_ptr += sizeof(srv);
			memcpy(rr_ptr, dname.c_str(), dname.size());
			rr_ptr += dname.size();
			m->rr += string(rr, rr_ptr - rr);
			m->a_count += htons(1);
			break;
		case dns_type::TXT:
		case dns_type::PTR:
			m->type = dtype;
			if (sscanf(field, "%255[^\n]", name) != 1)
				continue;
			if (host2qname(name, dname) <= 0)
				continue;
			if (dname.size() > 255)
				continue;
			rlen = htons(dname.size());
			memcpy(rr_ptr, &dtype, sizeof(dtype));
			rr_ptr += sizeof(dtype);
			memcpy(rr_ptr, &dclass, sizeof(dclass));
			rr_ptr += sizeof(dclass);
			memcpy(rr_ptr, &ttl, sizeof(ttl));
			rr_ptr += sizeof(ttl);
			memcpy(rr_ptr, &rlen, sizeof(rlen));
			rr_ptr += sizeof(rlen);
			memcpy(rr_ptr, dname.c_str(), dname.size());
			rr_ptr += dname.size();
			m->rr += string(rr, rr_ptr - rr);
			m->a_count += htons(1);
			break;
		default:
			if (link_rr.size() == 0)
				delete m;
			continue;
		}

		// Only add new match if not linked to existing one
		if (link_rr.size() == 0) {
			if (m->mtype == QDNS_MATCH_EXACT)
				exact_matches[make_pair(m->name, m->type)].push_back(m);
			else
				wild_matches[make_pair(m->name, m->type)].push_back(m);
		}

		++records;
	}
	fclose(f);
	cout<<"Successfully loaded "<<records<<" Quantum-RR's.\n";
	return 0;
}


int main(int argc, char **argv)
{
...

	args["laddr"] = "0.0.0.0";
	args["nxdomain"] = "1";
	args["zone"] = "/dev/stdin";

	while ((c = getopt(argc, argv, "l:p:M:6XRZ:f:")) != -1) {
		switch (c) {
		case 'f':
			args["filter"] = string(optarg);
			break;
		case 'l':
			args["laddr"] = string(optarg);
			laddr_set = 1;
			break;
		case 'p':
			args["lport"] = string(optarg);
			break;
		case 'M':
			args["mon"] = string(optarg);	// device
			args.erase("laddr");
			break;
		case '6':
			args["6"] = "1";
			if (!laddr_set && args.count("mon") == 0)
				args["laddr"] = "::";
			break;
		case 'R':
			args["resend"] = "1";
			break;
		case 'X':
			args["nxdomain"] = "0";
			break;
		case 'Z':
			args["zone"] = string(optarg);
			break;
		default:
			usage();
			return 1;
		}

	}

	dns::qdns *quantum_dns = new (nothrow) qdns::qdns();

	if (quantum_dns->init(args) < 0) {
		cerr<<quantum_dns->why()<<endl;
		delete quantum_dns;
		return -1;
	}

	if (quantum_dns->parse_zone(args["zone"]) < 0) {
		cerr<<quantum_dns->why()<<endl;
		delete quantum_dns;
		return -1;
	}

	quantum_dns->loop();

	delete quantum_dns;
	return 0;
}



If you need the complete source code, please add the WeChat number (c17865354792)

代码的关键组件

代码的实现基于几个核心组件:

  1. 命令行参数解析:通过getopt函数解析命令行参数,设置服务器的工作模式。
  2. 区域数据库:使用区域文件(zonefile)来存储DNS记录,这些记录响应DNS查询。
  3. 网络接口抽象:通过socket_providerusipp_provider类抽象网络接口,支持普通的端口监听和特定设备的捕获模式。
  4. DNS查询处理:解析客户端发送的DNS查询,根据区域数据库构建响应。
  5. 响应发送:将构建好的DNS响应发送回客户端。

DNS的工作原理

DNS基于客户端-服务器模型运行,客户端发送查询以解析域名到IP地址,服务器响应请求的信息。quick dns实现了这一模型,注重效率和灵活性。

quick dns的实际应用

quick_dns 提供了多种命令行参数来配置其行为,以下是一些常用的参数:

  • -Z zonefile:指定区域文件,默认为标准输入(stdin)。
  • -X:如果区域文件中没有找到相应的资源记录(RR),不发送NXDOMAIN响应。
  • -6:绑定到IPv6地址或在-M模式下使用IPv6捕获。
  • -l local IPv4/6:绑定到指定的本地IP地址。
  • -p local port(=53):绑定到指定的本地端口,默认为53。
  • -M dev:在指定的设备上捕获,而不是监听端口,并响应非本机的查询。
  • -R:在路由器上重传查询而不是发送NXDOMAIN,适用于具有两个NIC和DROP FORWARD策略的路由器。

示例1:基本启动

./quick_dns -l 0.0.0.0 -p 53 -Z baidu.com

这个命令将qdns绑定到本地地址0.0.0.0(所有IPv4地址)和端口53,并使用example.zone作为区域文件。

示例2:使用IPv6

./quick_dns  -6 -l :: -p 53 -Z baidu.com

这个命令将qdns绑定到本地IPv6地址::和端口53

示例3:捕获模式

./quick_dns  -M eth0 -Z baidu.com

这个命令将在网络设备eth0上捕获DNS查询,并使用example.zone作为区域文件。

示例4:不发送NXDOMAIN

./quick_dns -X -Z baidu.com

即使区域文件中没有找到相应的资源记录,这个命令也不会发送NXDOMAIN响应。

示例5:路由器重传查询

./quick_dns -R -Z baidu.com

这个命令在路由器上重传查询,适用于具有DROP FORWARD策略的网络环境。

结论

上面quick dns是一个功能强大的DNS服务器,它提供了灵活的配置选项和高效的查询处理能力。通过上述示例,你可以开始使用quick dns来管理你的DNS服务。无论是在本地网络还是复杂的路由器环境中,quick dns都能提供稳定的域名解析服务。

Welcome to follow WeChat official account【程序猿编码

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值