UDP和运营商有什么关系?
这个问题有点大且突兀。只要不是在三大运营商上班的,其实我们都是端到端用户,而端到端用户对于网络的认知必然是盲目的,我们不知道路由器对我们的流量做了什么,我们更没有能力去控制它们,我们只能猜测。
本来一个技术范畴的讨论一旦涉及到了猜测,就不是技术讨论了,而是社会学讨论,这往往会带来无休止的辩论,争吵,在此其中,独占鳌头的往往不是靠技术实力,而是靠口才和措辞,或者还有夹杂着各种手势的抑扬顿挫。
我是极其讨厌充斥着此类调调的场合的,我在这种场合往往会选择闭嘴,然后离开。
人们无休止地讨论如何针对CC(Congestion Control)进行调优,其实每个人心里都没底,说出来的貌似令人信服的话靠的无非就是自信和强势,然后结论会瞬间打脸这种信口开河。
摘录一段深信服白皮书上的话:
如上图所示,刚好与传统 TCP“慢上升、快下降”相对,HTP 快速传输协议对于数据传输为“快上升、慢下降”。当网络吞吐允许的情况下以最短的时间将传输速度提高到吞吐量所允许的最高;
搞得好像真的一样,但其实这简直就是扯淡!
很多人都说QUIC其实不如TCP,这个结论我至今都不知道从哪个 “权威” 那里得到的,就好像人们人云亦云地诟病Netfilter/iptales一样。可是当你测试的时候,却发现 并不是每次 QUIC都不如TCP。
人们很少去深入探究中间网络,就好像开着个跑车在闹市区依然总觉得自己能起飞一样,说起运营商和TCP/UDP的关系,人们总是避而不谈,结论往往就两种:
说的跟真事儿一样,就好像他们无所不知的样子。我不明白这些懂点儿网络的人为什么总是学不会懂礼貌,其实我脾气也不好,我心里早就一万个XXX了。
但如果哪一天我也想聊聊这种话题,我会从一个具体的事情开始。嗯,今天就是,2021年春节前最后的一个周末。
我不能理解的一个问题是 一个UDP socket为什么不能多次bind不同的端口。 UDP本身就是无状态无连接的,它想发数据的时候,随时抓一个和其它五元组不冲突的端口使用即可,但事实上在一个UDP socket的生命周期内,它的源端口是不会变的,这看起来对UDP施加了更紧的约束,我觉得这是不合理的,这种约束也太不UDP了。
我们看下面的代码:
#!/usr/bin/python3
import socket
#import sys
sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
server_address = ('192.168.56.101', 1234)
message = str.encode('aaaaaaaaaaaaaaaaaaaaa')
sport = 4321
while True:
sent = sock.sendto(message, server_address)
sock.bind(('192.168.56.102', sport))
sport = sport + 1
当你执行的时候,你会得到下面的报错:
Traceback (most recent call last):
File "./udpsender.py", line 14, in <module>
sock.bind(('192.168.56.102', sport))
OSError: [Errno 22] Invalid argument
至少在Linux上是这样的。
这是因为Linux禁止double bind:
int __inet_bind(struct sock *sk, struct sockaddr *uaddr, int addr_len,
bool force_bind_address_no_port, bool with_lock)
{
...
/* Check these errors (active socket, double bind). */
err = -EINVAL;
if (sk->sk_state != TCP_CLOSE || inet->inet_num)
goto out_release_sock;
...
}
OK,既然我这么笃信这是不合理的,改了它便是了,执行下面的脚本:
#!/usr/bin/stap -g
%{
#include <net/inet_sock.h>
%}
function alter(ssk:long)
%{
struct sock *sk = (struct sock *)STAP_ARG_ssk;
struct inet_sock *inet = inet_sk(sk);
sk->sk_prot->unhash(sk);
inet->inet_num = 0;
%}
probe kernel.function("__inet_bind")
{
alter($sk);
}
现在再来执行上面的python脚本试试看。
我们主要应该关注的是如果UDP socket每次发包都换一个不同的源端口,这将会在短时间内创造大量的五元组。这个事实会影响运营商的决策。
传统的状态防火墙,状态NAT会用一个 五元组 来追踪一条 连接 。如果连接过多,就会对这些保存状态的设备造成很大的压力,这种压力主要体现在两个方面:
由于UDP的无状态特征,没有任何报文指示一条连接什么时候该创建什么时候该销毁,设备必须有能力自行老化已经创建的UDP连接,且不得不在权衡中作出抉择:
攻击者只需要用不同的UDP五元组构造报文使其经过状态设备即可,由于UDP报文没有任何指示连接创建销毁的控制信息,状态设备不得不平等对待任何新来的五元组,即为它们创建连接,并且指定相同的老化时间。TCP与此完全不同,由于存在syn,fin,rst等控制信息,状态设备便可以针对不同状态的TCP连接指定不同的老化时间,ESTABLISHED状态的连接显然要比其它状态的连接老化时间长得多。
这导致使用TCP来实施同样的攻击会困难很多。
为什么,为什么快速构造不同的TCP五元组达不到UDP同样的效果?
你发起一个TCP连接,为了让状态设备保存这条连接,你自己也不得不保存这条连接,除非你通过海量的反射主机同时发起真连接,否则在单台甚至少量的主机上,这种攻击很难奏效。
办法也不是没有,如果你能在三次握手成功创建连接后迅速关掉它们,同时阻止任何rst,fin报文的发出,攻击可能会简单一些。
在S上执行下面的命令:
while true; do telnet 172.18.0.1 22;done
刷一波之后,在Midbox上查看conntrack条目:
# conntrack -L -p tcp src 172.16.0.1|grep ESTABLISHED |wc -l
conntrack v1.4.5 (conntrack-tools): 14118 flow entries have been shown.
14117
而此时,S和D上均早已经看不见任何连接了:
# netstat -antp|grep 172.16.0.1 |wc -l
0
此外,不通过协议栈,构造raw报文来进行TCP握手进行攻击,本地不保存任何TCP连接信息显然是一种 更佳 的方法,因为我懒得去写那么多代码,也不想折腾scapy这种东西,就只能用上述iptables/stap的方式模拟了。
然而无论怎样,TCP做类似的攻击依然达不到UDP快速构建连接损害状态设备的效果,至少效率损失个3倍(需要三次握手)起跳吧。
好了,结束对状态设备的讨论。现在问题又来了,如果是非状态设备呢?
既然是无状态设备,我们便不必再纠结五元组连接的保持了。但是UDP短期构造海量五元组的能力仍然会影响无状态设备包分类算法的正常运行。基于包分类算法的优先级队列,缓存管理几乎也是通过五元组计算来完成的,UDP的特征将会使无状态设备对其做流量管控变得困难。其结果就是,眼睁睁任凭UDP流量挤满各级队列缓存却没有办法将其精确识别出来,即便是BBR遇到了UDP流量,也只能自降pacing rate而兴叹,在它看来,瓶颈带宽是真的减小了,运营商设备本应该做的更多,然而它却无能为力。
运营商(特别是国内运营商)显然没有能力和精力根据TCP和UDP的不同去深度定制不同的QoS策略,一刀切显然是最便捷的手段。
当然,国内运营商的带宽套餐超卖可能会导致下面的事情的发生(本人从不同非技术渠道获知,用技术手段确认):
这就是我为什么总是提倡 “在自然月月初的时候把你的UDP流量的UDP头换成TCP头,接收端再换回来” 的原因。
当我说这个话的时候,我听到过两种意思截然相反的反对声音:
我不知道这些人出于什么目的,我和他们并不熟识,只是单纯的讨论技术而已,在我要求他们给出理论分析或者至少给出测试数据的时候,他们便再也不回复了,我不知道他们是出于什么心态处处反驳,大概是为了彰显一下自己?不得而知。为了怼而怼没有意义,还是要亲自试一下。
当我需要亲自试一下的时候,又有人出来怼了,大概是说,即便真的要这么做,何必换头,直接修改掉IP头的protocol字段即可。很多人煞有其事地告诉我,直接改个字段就行:
...
if (iph->protocol == IPPROTO_TCP) {
iph->protocol = IPPROTO_UDP;
ip_send_check(iph);
udph->check = 0;
} else if (iph->protocol == IPPROTO_UDP) {
iph->protocol = IPPROTO_TCP;
ip_send_check(iph);
}
我敢保证他们绝对没有真的试过,因为当你真正去这么尝试的时候,就会发现,丢包率大大增加了,甚至根本无法通信!这个事实难道没有从反面证明换头操作会影响运营商的QoS策略吗?
下面是我测试这种情况的一个代码片段,它是一个udpping程序,每次发给对端一个随机的字符串,等待对端echo回来。首先在对端部署仅仅修改IP头protocol字段的逻辑,并且执行socat:
socat -v UDP-LISTEN:4321,fork PIPE
发送端同样部署上述的IP头换protocol字段的逻辑,执行发送的代码片段如下:
while True:
payload= random_string(20)
sock.sendto(payload.encode(), (IP, PORT))
...
recv_data,addr = sock.recvfrom(65536)
if addr[0]==IP and addr[1]==PORT:
rtt=((time.time()-time_of_send)*1000)
print("Reply from",IP,"seq=%d"%count, "time=%.2f"%(rtt),"ms")
...
很遗憾,结果如下:
# 地址和rtt为伪造,真实测试链路为上海到日本。
Reply from 123.123.123.123 seq=97 time=25.41 ms
Request timed out
Request timed out
Request timed out
Reply from 123.123.123.123 seq=101 time=25.41 ms
Request timed out
Reply from 123.123.123.123 seq=103 time=25.44 ms
Reply from 123.123.123.123 seq=104 time=25.39 ms
Request timed out
Request timed out
Request timed out
Reply from 123.123.123.123 seq=108 time=25.40 ms
Reply from 123.123.123.123 seq=109 time=25.43 ms
由于发包程序发送的是random字符串,它们将会被解析成TCP头超过UDP头大小的部分,抓包可以一窥究竟,它可以分别被解析成下面的样子:
IP (tos 0x20, ttl 64, id 60168, offset 0, flags [DF], proto TCP (6), length 128)
111.111.111.111.1234 > 123.123.123.123.4321: Flags [FSRPUE], cksum 0x5651 (incorrect -> 0x1bba), seq 7108832:7108912, win 17975, urg 19510, options [[bad opt]
...
IP (tos 0x20, ttl 64, id 60969, offset 0, flags [DF], proto TCP (6), length 128)
111.111.111.111.1234 > 123.123.123.123.4321: tcp 96 [bad hdr length 12 - too short, < 20]
...
IP (tos 0x20, ttl 64, id 63113, offset 0, flags [DF], proto TCP (6), length 128)
111.111.111.111.1234 > 123.123.123.123.4321: Flags [RE], cksum 0x5430 (incorrect -> 0x8e6e), seq 7108832:7108920, win 20022, length 88 [RST+ lCufDCd5oRmJCW02dV6coRDKCGVmJc]
嗯,结果大概就是这个样子。
仅仅修改IP头的protocol字段,仅仅将TCP修改成UDP为什么不行?
因为运营商状态设备确实会检查TCP报文中的syn,ack,fin,rst等标志(否则它们如何创建连接呢?),如果状态设备确实检查了这些标志,那么它就会在内部维护一个TCP连接的状态机,至少大概是这样,如果你的TCP报文序列严重违反了TCP状态机,结果可想而知。
如果只是设置了syn而没有设置ack,并且这个伪造的TCP报文还携带数据,在我的测试环境中则是被100%丢弃。
如果你只是修改了IP头的protocol字段,比方说改成了TCP,那么运营商设备就会将后面的UDP头解析成TCP头,由于UDP头只有8个字节,而TCP头的flag字段在8字节以外,因此运营商设备会将UDP的payload解析成TCP头的flag字段,而UDP payload几乎可以是任意的。
感谢TCP头和UDP头的前两个字段是一致的!
因此,如果要执行UDP头换TCP头的操作,并不是如那些信口开河之人想象的那样简单的。至少我测试下来,需要做的是:
下面的代码片段将UDP换成TCP头,为了不妨碍Netfilter深度检测报文,这个代码要在POSTROUTING的最后执行:
struct iphdr *iph = ip_hdr(skb), ihdr;
struct dst_entry *dst = NULL;
static unsigned int seq = 1239876;
static unsigned int ack_seq = 2345;
udph = (struct udphdr *)start;
ihdr = *iph;
uhdr = *udph;
if (iph->protocol != IPPROTO_UDP)
goto out;
ihdr.protocol = IPPROTO_TCP;
oldlen = ntohs(ihdr.tot_len);
ihdr.tot_len = htons(oldlen + delta);
if ((dst = skb_dst(skb)) == NULL)
goto out;
// 假TCP不支持TSO/GSO,因此由于新增了12字节的TCP和UDP头长差之后超过MTU的包不予换头。
if (oldlen + delta + LL_RESERVED_SPACE(dst->dev) > dst_mtu(dst))
goto out;
if (pskb_expand_head(skb, delta, 0, GFP_ATOMIC))
goto out;
iph = (struct iphdr *)skb_push(skb, delta);
*iph = ihdr;
skb_reset_network_header(skb);
skb_set_transport_header(skb, iph->ihl*4);
ip_send_check(iph);
tcph = (struct tcphdr *)skb_transport_header(skb);
tcph->source = uhdr.source;
tcph->dest = uhdr.dest;
tcph->seq = htonl(seq);
seq += 1000;
tcph->ack_seq = ack_seq;
tcph->syn = 0;
tcph->doff = 5;
tcph->ack = 1;
tcph->urg = 1;
tcph->rst = 0;
tcph->fin = 0;
tcph->window = htons(1000);
// 无需计算TCP校验和,因为我会在接收端将它换回UDP并且取消校验和检查,以实现真正的尽力而为的弱校验。
将UDP头换成TCP头之后,在另一端,我们需要执行相反的操作,将伪造的假TCP头再换回UDP,这个要更容易些,至少不需要expand_head了。
下面的代码在PREROUTING的raw之后conntrack之前执行,这是因为需要在iptables的raw表中对需要转换的数据包进行识别,并且要保证恢复UDP头之后不影响后续可能存在的nf_conntrack操作:
struct iphdr *iph = ip_hdr(skb), ihdr;
struct dst_entry *dst = NULL;
udph = (struct udphdr *)start;
ihdr = *iph;
uhdr = *udph;
if (iph->protocol != IPPROTO_TCP)
goto out;
ihdr.protocol = IPPROTO_UDP;
oldlen = ntohs(ihdr.tot_len);
ihdr.tot_len = htons(oldlen - delta);
iph = (struct iphdr *)skb_pull(skb, delta);
*iph = ihdr;
skb_reset_network_header(skb);
skb_set_transport_header(skb, iph->ihl*4);
ip_send_check(iph);
udph = (struct udphdr *)skb_transport_header(skb);
udph->source = thdr.source;
udph->dest = thdr.dest;
udph->len = htons(ntohs(iph->tot_len) - sizeof(struct iphdr));
// 取消校验和,补偿下换头操作带来的额外延迟。
udph->check = 0;
下面是一个iperf测试中换头功能切换前后的抓包效果:
23:37:25.951045 IP 192.168.56.101.1234 > 192.168.56.102.4321: UDP, length 1000
23:37:25.957493 IP 192.168.56.101.1234 > 192.168.56.102.4321: UDP, length 1000
23:37:25.968388 IP 192.168.56.101.1234 > 192.168.56.102.4321: UDP, length 1000
23:37:25.973226 IP 192.168.56.101.1234 > 192.168.56.102.4321: UDP, length 1000
23:37:25.988933 IP 192.168.56.101.1234 > 192.168.56.102.4321: UDP, length 1000
23:37:25.988933 IP 192.168.56.101.1234 > 192.168.56.102.4321: UDP, length 1000
23:37:26.009427 IP 192.168.56.102.4321 > 192.168.56.101.1234: UDP, length 1000
23:39:03.572431 IP 192.168.56.101.1234 > 192.168.56.102.4321: Flags [.UEW], seq 8934678:8935678, ack 3458334720, win 1000, urg 43727, length 1000
23:39:03.580616 IP 192.168.56.101.1234 > 192.168.56.102.4321: Flags [.UEW], seq 1000:2000, ack 1, win 1000, urg 35592, length 1000
23:39:03.587336 IP 192.168.56.101.1234 > 192.168.56.102.4321: Flags [.UEW], seq 2000:3000, ack 1, win 1000, urg 28841, length 1000
23:39:03.597182 IP 192.168.56.101.1234 > 192.168.56.102.4321: Flags [.UEW], seq 3000:4000, ack 1, win 1000, urg 19029, length 1000
23:39:03.603321 IP 192.168.56.101.1234 > 192.168.56.102.4321: Flags [.UEW], seq 4000:5000, ack 1, win 1000, urg 12785, length 1000
23:39:03.610564 IP 192.168.56.101.1234 > 192.168.56.102.4321: Flags [.UEW], seq 5000:6000, ack 1, win 1000, urg 5600, length 1000
23:39:03.618043 IP 192.168.56.101.1234 > 192.168.56.102.4321: Flags [.UEW], seq 6000:7000, ack 1, win 1000, urg 63666, length 1000
好漂亮的序列。
关于重算IP头校验和的问题,很多人是很反感,总觉得这里会增加额外的处理延时。实际上这种校验和计算开销是可以忽略不计的。
校验和是一种非常弱的校验算法,它本质上就是就是将数据序列按照16bits拆分,然后将这些16bits数据加到一起,从这个基本原理可以看出,用 加法交换律 很容易实现 修改数据而不改变校验和 以及 校验和增量重新计算 。
下面的代码展示了通过修改IP头的id字段来弥补protocol字段和tot_len字段的的改变从而不改变原始的校验和:
unsigned short old1 = *(unsigned short *)&iph->ttl; // ttl和protocol合体作为16bits参与计算
unsigned short old2 = *(unsigned short *)&iph->tot_len; // 换头后总长会改变
unsigned short *pID;
...// 修改protocol和总长
pID = (unsigned short *)&iph->id;
*pID -= *(unsigned *)&iph->ttl - old1;
*pID -= *(unsigned *)&iph->tot_len - old2;
我个人倾向于采用同时修改IPID和原始IP校验和的方式,因为这样可以应对中间设备对IPID的分布进行扫描:
下面的代码实现了校验和的增量计算:
unsigned short old1 = *(unsigned short *)&iph->ttl; // ttl和protocol合体作为16bits参与计算
unsigned short old2 = *(unsigned short *)&iph->tot_len; // 换头后总长会改变
... // 修改protocol和总长
iph->check = ~iph->check + *(unsigned *)&iph->ttl - old1 + *(unsigned *)&iph->tot_len - old2;
iph->check = ~iph->check;
其实,由于IP头本身就很小,一共才10个bits,因此即便是重新全部计算一遍也不会有多大开销。而诸如TCP,UDP这种连带载荷一起计算的校验和才能算得上开销。慢载MSS的报文差不多需要700多次的计算。因此一般比如在做了NAT之后需要对TCP/UDP重新计算校验和的时候,会用增量计算,只计算改变的量,而IP头的校验和,无所谓咯。
TCP校验和太弱了,以至于我们完全不可信任它,交换payload的两个16bits不会影响检验和的正确性,因此在我们下载文件的时候,一般会顺手下载一个MD5文件用来自行校验。
有点跑题,OK,言归正传。
我在公网上进行了测试,同样的上海到日本的链路,换头操作之后丢包率小幅降低但不是很明显,等哪天我找一条国内的链路在流量高峰期测试一把再看效果。
行文至此,可以看到,首先,运营商真的会根据UDP和TCP标志做区分对待,其次,UDP头换成TCP头传输在技术上是可行的。现在,我想最后聊一下这种技术适合做什么,不能做什么。
显然,TCP换头有个前提,你必须能控制两端,换句话说这是一个双边操作,这和IP/GRE/TCP/UDP/VXX等隧道技术非常相似,简直就是同一类东西。当你可以控制两端时,你才有能力去搭建一条隧道。那么隧道里封装什么呢?
在我看来,任何东西都可以封装在这个伪造的假TCP隧道里,当然,包括皮鞋和真丝的领带。
现在,让我们忽略隧道的概念。我可以把遇到的任意UDP头换成TCP头吗?比如DNS的流量?
当然可以!并且我建议你这么做!只要你能卡住该UDP流量必经路径上的两个点,你就可以在这两个点之间将UDP头换成TCP头,我的理由如下:
zhengjiang wenzhou skinshoe wet,down rain enter water not can fat.
浙江温州皮鞋湿,下雨进水不会胖。
文章浏览阅读290次,点赞8次,收藏10次。1.背景介绍稀疏编码是一种用于处理稀疏数据的编码技术,其主要应用于信息传输、存储和处理等领域。稀疏数据是指数据中大部分元素为零或近似于零的数据,例如文本、图像、音频、视频等。稀疏编码的核心思想是将稀疏数据表示为非零元素和它们对应的位置信息,从而减少存储空间和计算复杂度。稀疏编码的研究起源于1990年代,随着大数据时代的到来,稀疏编码技术的应用范围和影响力不断扩大。目前,稀疏编码已经成为计算...
文章浏览阅读217次。EasyGBS - GB28181 国标方案安装使用文档下载安装包下载,正式使用需商业授权, 功能一致在线演示在线API架构图EasySIPCMSSIP 中心信令服务, 单节点, 自带一个 Redis Server, 随 EasySIPCMS 自启动, 不需要手动运行EasySIPSMSSIP 流媒体服务, 根..._easygbs-windows-2.6.0-23042316使用文档
文章浏览阅读1.2k次,点赞27次,收藏7次。2023巅峰极客 BabyURL之前AliyunCTF Bypassit I这题考查了这样一条链子:其实就是Jackson的原生反序列化利用今天复现的这题也是大同小异,一起来整一下。_原生jackson 反序列化链子
文章浏览阅读734次,点赞9次,收藏7次。微服务架构简单的说就是将单体应用进一步拆分,拆分成更小的服务,每个服务都是一个可以独立运行的项目。这么多小服务,如何管理他们?(服务治理 注册中心[服务注册 发现 剔除])这么多小服务,他们之间如何通讯?这么多小服务,客户端怎么访问他们?(网关)这么多小服务,一旦出现问题了,应该如何自处理?(容错)这么多小服务,一旦出现问题了,应该如何排错?(链路追踪)对于上面的问题,是任何一个微服务设计者都不能绕过去的,因此大部分的微服务产品都针对每一个问题提供了相应的组件来解决它们。_spring cloud
文章浏览阅读5.9k次,点赞6次,收藏20次。Js实现图片点击切换与轮播图片点击切换<!DOCTYPE html><html> <head> <meta charset="UTF-8"> <title></title> <script type="text/ja..._点击图片进行轮播图切换
文章浏览阅读10w+次,点赞245次,收藏1.5k次。在开始安装前,如果你的电脑装过tensorflow,请先把他们卸载干净,包括依赖的包(tensorflow-estimator、tensorboard、tensorflow、keras-applications、keras-preprocessing),不然后续安装了tensorflow-gpu可能会出现找不到cuda的问题。cuda、cudnn。..._tensorflow gpu版本安装
文章浏览阅读243次。0x00 简介权限滥用漏洞一般归类于逻辑问题,是指服务端功能开放过多或权限限制不严格,导致攻击者可以通过直接或间接调用的方式达到攻击效果。随着物联网时代的到来,这种漏洞已经屡见不鲜,各种漏洞组合利用也是千奇百怪、五花八门,这里总结漏洞是为了更好地应对和预防,如有不妥之处还请业内人士多多指教。0x01 背景2014年4月,在比特币飞涨的时代某网站曾经..._使用物联网漏洞的使用者
文章浏览阅读786次。A. Epipolar geometry and triangulationThe epipolar geometry mainly adopts the feature point method, such as SIFT, SURF and ORB, etc. to obtain the feature points corresponding to two frames of images. As shown in Figure 1, let the first image be and th_normalized plane coordinates
文章浏览阅读708次,点赞2次,收藏3次。开放信息抽取(OIE)系统(三)-- 第二代开放信息抽取系统(人工规则, rule-based, 先关系再实体)一.第二代开放信息抽取系统背景 第一代开放信息抽取系统(Open Information Extraction, OIE, learning-based, 自学习, 先抽取实体)通常抽取大量冗余信息,为了消除这些冗余信息,诞生了第二代开放信息抽取系统。二.第二代开放信息抽取系统历史第二代开放信息抽取系统着眼于解决第一代系统的三大问题: 大量非信息性提取(即省略关键信息的提取)、_语义角色增强的关系抽取
文章浏览阅读1.1w次,点赞6次,收藏51次。快速完成网页设计,10个顶尖响应式HTML5网页模板助你一臂之力为了寻找一个优质的网页模板,网页设计师和开发者往往可能会花上大半天的时间。不过幸运的是,现在的网页设计师和开发人员已经开始共享HTML5,Bootstrap和CSS3中的免费网页模板资源。鉴于网站模板的灵活性和强大的功能,现在广大设计师和开发者对html5网站的实际需求日益增长。为了造福大众,Mockplus的小伙伴整理了2018年最..._html欢迎页面
文章浏览阅读282次。原标题:2018全国计算机等级考试调整,一、二级都增加了考试科目全国计算机等级考试将于9月15-17日举行。在备考的最后冲刺阶段,小编为大家整理了今年新公布的全国计算机等级考试调整方案,希望对备考的小伙伴有所帮助,快随小编往下看吧!从2018年3月开始,全国计算机等级考试实施2018版考试大纲,并按新体系开考各个考试级别。具体调整内容如下:一、考试级别及科目1.一级新增“网络安全素质教育”科目(代..._计算机二级增报科目什么意思
文章浏览阅读240次。conan简单使用。_apt install conan