kk Blog —— 通用基础


date [-d @int|str] [+%s|"+%F %T"]
netstat -ltunp
sar -n DEV 1

SKB路由缓存与SOCK路由缓存

https://www.2cto.com/kf/201805/745174.html

skb结构体中的成员 _skb_refdst 与sock结构体中成员sk_rx_dst(缓存入口路由)和sk_dst_cache(缓存出口路由)成员之间的交互操作。

SOCK入口路由与SKB路由缓存

内核在接收流程中,调用early_demux函数提前在IP层做established状态的sock查找,并负责将sock结构体成员sk_rx_dst的路由缓存赋值给skb成员_skb_refdst,对于UDP协议,先判断DST_NOCACHE标志,如果成立,增加dst引用计数,设置skb的dst;否则,调用skb_dst_set_noref直接进行设置。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
void udp_v4_early_demux(struct sk_buff *skb)
{
	dst = READ_ONCE(sk->sk_rx_dst);
 
	if (dst)
		dst = dst_check(dst, 0);
	if (dst) {
		/* DST_NOCACHE can not be used without taking a reference */
		if (dst->flags & DST_NOCACHE) {
			if (likely(atomic_inc_not_zero(&dst->__refcnt)))
				skb_dst_set(skb, dst);
		} else {
			skb_dst_set_noref(skb, dst);
		}
	}
}

对于TCP协议,直接调用skb_dst_set_noref函数,将sock结构体成员sk_rx_dst缓存到skb结构体中。

1
2
3
4
5
6
7
8
9
10
11
12
void tcp_v4_early_demux(struct sk_buff *skb)
{
	if (sk->sk_state != TCP_TIME_WAIT) {
		struct dst_entry *dst = sk->sk_rx_dst;
 
		if (dst)
			dst = dst_check(dst, 0);
		if (dst &&
			inet_sk(sk)->rx_dst_ifindex == skb->skb_iif)
			skb_dst_set_noref(skb, dst);
	}
}

同样都为early_demux函数,都是从sk->sk_rx_dst获取路由缓存,tcp和udp的存在明显差别。TCP直接赋值,UDP需要先判断DST_NOCACHE标志。此情况是由UDP与TCP在sock中缓存dst时的处理不同造成的,TCP预先调用了dst_hold_safe函数,进行了DST_NOCACHE标志的判断处理,如未缓存则增加了引用计数。然而,UDP在缓存路由dst时,使用xchg函数,未判断也未增加引用计数,所以需要在后续判断处理。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
static inline bool dst_hold_safe(struct dst_entry *dst)
{
	if (dst->flags & DST_NOCACHE)
		return atomic_inc_not_zero(&dst->__refcnt);
	dst_hold(dst);
}
 
void inet_sk_rx_dst_set(struct sock *sk, const struct sk_buff *skb)
{
	struct dst_entry *dst = skb_dst(skb);
 
	if (dst && dst_hold_safe(dst)) {
		sk->sk_rx_dst = dst;
		inet_sk(sk)->rx_dst_ifindex = skb->skb_iif;
	}
}
 
static void udp_sk_rx_dst_set(struct sock *sk, struct dst_entry *dst)
{
	struct dst_entry *old;
 
	dst_hold(dst);
	old = xchg(&sk->sk_rx_dst, dst);
	dst_release(old);
}

SOCK出口路由与SKB路由缓存

对于UDP协议客户端,其在connect时(UDP客户端connect不同于TCP,仅绑定通信端地址),查询路由,缓存到sock结构体的sk_dst_cache中。

1
2
3
4
5
int ip4_datagram_connect(struct sock *sk, struct sockaddr *uaddr, int addr_len)
{
	rt = ip_route_connect(...);
	sk_dst_set(sk, &rt->dst);
}

之后,发送UDP数据包时,检查sock结构体中的出口路由是否有效,有效的话可不用再次查询路由表,在函数ip_make_skb中直接使用rt,并且调用skb_dst_set赋值给skb的_skb_refdst结构体,以便在发送过程中使用。

对于UDP服务端,在首次发包检测到rt为空时,查询路由表得到出口路由,缓存在sock结构中,之后发包时rt有效,省去再次查询。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
struct sk_buff *__ip_make_skb(...)
{
	skb_dst_set(skb, &rt->dst);
}
 
int udp_sendmsg(...)
{
	if (connected)
		rt = (struct rtable *)sk_dst_check(sk, 0);
	if (rt == NULL) {
		rt = ip_route_output_flow(net, fl4, sk);
		if (connected)
			sk_dst_set(sk, dst_clone(&rt->dst));
	}
 
	skb = ip_make_skb(sk, fl4, getfrag, msg->msg_iov, ulen,
			sizeof(struct udphdr), &ipc, &rt,
			msg->msg_flags);
}

IP层发送数据包时(调用ip_queue_xmit),检测sock结构中出口路由缓存,如果有效,设置到skb结构体中。否则重新进行出口路由查找。

1
2
3
4
5
6
7
8
9
int ip_queue_xmit(struct sk_buff *skb, struct flowi *fl)
{
	rt = (struct rtable *)__sk_dst_check(sk, 0);
	if (rt == NULL) {
		rt = ip_route_output_ports(...);
		sk_setup_caps(sk, &rt->dst);
	}
	skb_dst_set_noref(skb, &rt->dst);
}

内核版本

linux-3.10.0

ssl SNI(Server Name Indication)

https://blog.csdn.net/makenothing/article/details/53292335

Server Name Indication(SNI)

SNI (Server Name Indication)是用来改善服务器与客户端 SSL (Secure Socket Layer)和 TLS (Transport Layer Security) 的一个扩展。主要解决一台服务器只能使用一个证书(一个域名)的缺点,随着服务器对虚拟主机的支持,一个服务器上可以为多个域名提供服务,因此SNI必须得到支持才能满足需求。

SNI产生背景

SSL以及TLS(SSL的升级版)为客户端与服务器端进行安全连接提供了条件。但是,由于当时技术限制,SSL初期的设计顺应经典的公钥基础设施 PKI(Public Key Infrastructure)设计,PKI 认为一个服务器只为一个域名提供服务,从而一个服务器上也就只能使用一个证书。这样客户端在发送请求的时候,利用DNS域名解析,只要向解析到的IP地址(服务器地址)发送请求,然后服务器将自身唯一的证书返回回来,交给客户端验证,验证通过,则继续进行后续通信。然后通过协商好的加密通道,获得所需要的内容。这意味着服务器可以在 SSL 的启动动阶段发送或提交证书,因为它知道它在为哪个特定的域名服务。

随着HTTP 服务器开启虚拟主机支持后,每个服务器通过相同的IP地址可以为很多域名提供服务。这种为虚拟主机提供通信安全的简单途径,却经常导致使用了错误的数字证书,因为服务器端无法知道客户端到底请求的是哪个域名下的服务,从而导致浏览器对用户发出警告。

不幸的是,当设置了 SSL加密,服务器在读取HTTP请求里面的域名之前已经向客户端提交了证书,也就是已经为默认域提供了服务。但是,一个服务器可能为上千个域名提供服务,不可能将所有证书都发送给客户端,让客户端一一验证,找到与请求域名对应的证书。SNI的设计目的是为了让服务器根据请求来决定为哪个域服务,这个信息通常从HTTP请求头获得。

SSL/TLS握手

熟悉SSL/TLS握手过程的都知道,主要经过以下几个过程: 基于RSA握手和密钥交换的客户端验证服务器为示例详解TLS/SSL握手过程。

1 C->S:client_hello

客户端发起请求,以明文传输请求信息,包含版本信息,加密套件候选列表,压缩算法候选列表,随机数,扩展字段等信息。

SSL/STL版本支持的最高TSL协议版本version,从低到高依次 SSLv2 SSLv3 TLSv1 TLSv1.1 TLSv1.2,当前基本不再使用低于 TLSv1 的版本;

客户端支持的加密套件 cipher suites 列表, 每个加密套件对应前面 TLS 原理中的四个功能的组合:认证算法 Au (身份验证)、密钥交换算法 KeyExchange(密钥协商)、对称加密算法 Enc (信息加密)和信息摘要 Mac(完整性校验);

支持的压缩算法 compression methods 列表,用于后续的信息压缩传输;

随机数 random_C,用于后续的密钥的生成;

扩展字段 extensions,支持协议与算法的相关参数以及其它辅助信息等,常见的 SNI 就属于扩展字段,后续单独讨论该字段作用。

2 server_hello+server_certificate+sever_hello_done

server_hello, 服务端返回协商的信息结果,包括选择使用的协议版本 version,选择的加密套件 cipher suite,选择的压缩算法 compression method、随机数 random_S 等,其中随机数用于后续的密钥协商;

server_certificates, 服务器端配置对应的证书链,用于身份验证与密钥交换;

server_hello_done,通知客户端 server_hello 信息发送结束;

3 证书校验

客户端验证证书的合法性,如果验证通过才会进行后续通信,否则根据错误情况不同做出提示和操作,合法性验证包括如下:

证书链的可信性 trusted certificate path,方法如前文所述;

证书是否吊销 revocation,有两类方式离线 CRL 与在线 OCSP,不同的客户端行为会不同;

有效期 expiry date,证书是否在有效时间范围;

域名 domain,核查证书域名是否与当前的访问域名匹配,匹配规则后续分析;

4 client_key_exchange+change_cipher_spec+encrypted_handshake_message

client_key_exchange,合法性验证通过之后,客户端计算产生随机数字 Pre-master,并用证书公钥加密,发送给服务器;

此时客户端已经获取全部的计算协商密钥需要的信息:两个明文随机数 random_C 和 random_S 与自己计算产生的 Pre-master,计算得到协商密钥;

enc_key=Fuc(random_C, random_S, Pre-Master)

change_cipher_spec,客户端通知服务器后续的通信都采用协商的通信密钥和加密算法进行加密通信;

encrypted_handshake_message,结合之前所有通信参数的 hash 值与其它相关信息生成一段数据,采用协商密钥 session secret 与算法进行加密,然后发送给服务器用于数据与握手验证;

5 change_cipher_spec+encrypted_handshake_message

服务器用私钥解密加密的 Pre-master 数据,基于之前交换的两个明文随机数 random_C 和 random_S,计算得到协商密钥:enc_key=Fuc(random_C, random_S, Pre-Master);

计算之前所有接收信息的 hash 值,然后解密客户端发送的 encrypted_handshake_message,验证数据和密钥正确性;

change_cipher_spec, 验证通过之后,服务器同样发送 change_cipher_spec 以告知客户端后续的通信都采用协商的密钥与算法进行加密通信;

encrypted_handshake_message, 服务器也结合所有当前的通信参数信息生成一段数据并采用协商密钥 session secret 与算法加密并发送到客户端;

6 握手结束

客户端计算所有接收信息的 hash 值,并采用协商密钥解密 encrypted_handshake_message,验证服务器发送的数据和密钥,验证通过则握手完成;

7 加密通信

开始使用协商密钥与算法进行加密通信。

由以上过程可以知道,没有SNI的情况下,服务器无法预知客户端到底请求的是哪一个域名的服务。

SNI 应用

SNI的TLS扩展通过发送虚拟域的名字做为TSL协商的一部分修正了这个问题,在Client Hello阶段,通过SNI扩展,将域名信息提前告诉服务器,服务器根据域名取得对应的证书返回给客户端已完成校验过程。

curl

Linux中主要的网络交互工具,curl 7.18.1+ & openssl 0.9.8j+ 可以支持SNI,CentOS6.5及以下都是curl 7.15 不支持SNI,curl 7.21.3 又支持了–resolve 参数,可以直接定位到IP地址进行访问,对于一个域名有多个部署节点的服务来说,这个参数可以定向的访问某个设备。基本语法为:

1
2
Example:
   curl -k -I --resolve www.example.com:80:192.0.2.1 https://www.example.com/index.html

WireShark抓包验证SNI

使用curl7.15 (不支持SNI)抓包结果:

使用curl7.43(支持SNI)抓包结果:

可以看到,使用curl7.15抓包得到的数据无SNI扩展,而是用curl7.43抓包得到的数据,包含SNI扩展,其中包含host信息。

SSL协议握手过程报文解析

https://blog.csdn.net/tterminator/article/details/50675540

SSL建立握手连接目的:

1.身份的验证,client与server确认对方是它相连接的,而不是第三方冒充的,通过证书实现

2.client与server交换session key,用于连接后数据的传输加密和hash校验

简单的SSL握手连接过程(仅Server端交换证书给client):

1.client发送ClientHello,指定版本,随机数(RN),所有支持的密码套件(CipherSuites)

2.server回应ServerHello,指定版本,RN,选择CipherSuites,会话ID(Session ID)

3.server发送Certificate

4.Server发送ServerHelloDone

5.Client发送ClientKeyExchange,用于与server交换session key

6.Client发送ChangeCipherSpec,指示Server从现在开始发送的消息都是加密过的

7.Client发送Finishd,包含了前面所有握手消息的hash,可以让server验证握手过程是否被第三方篡改

8.Server发送ChangeCipherSpec,指示Client从现在开始发送的消息都是加密过的

9.Server发送Finishd,包含了前面所有握手消息的hash,可以让client验证握手过程是否被第三方篡改,并且证明自己是Certificate密钥的拥有者,即证明自己的身份

下面从抓包数据来具体分析这一过程并说明各部分数据的作用以及如实现前面列出的握手的目标,当然了,最重要的还是说明为何这一过程是安全可靠的,第三方无法截获,篡改或者假冒

1.client发送ClientHello

每一条消息都会包含有ContentType,Version,HandshakeType等信息。

ContentType指示SSL通信处于哪个阶段,是握手(Handshake),开始加密传输(ChangeCipherSpec)还是正常通信(Application)等,见下表

1
2
3
4
5
6
Hex  Dec Type

0x14  20  ChangeCipherSpec
0x15  21  Alert
0x16  22  Handshake
0x17  23  Application

Version是TLS的版本,见下表

1
2
3
4
5
6
Major Version    Minor Version   Version Type

3 0   SSLv3
3 1   TLS 1.0
3 2   TLS 1.1
3 3   TLS 1.2

Handshake Type是在handshanke阶段中的具体哪一步,见下表

1
2
3
4
5
6
7
8
9
10
11
12
Code Description

0 HelloRequest
1 ClientHello
2 ServerHello
11    Certificate
12    ServerKeyExchange
13    CertificateRequest
14    ServerHelloDone
15    CertificateVerify
16    ClientKeyExchange
20    Finished

ClientHello附带的数据随机数据RN,会在生成session key时使用,Cipher suite列出了client支持的所有加密算法组合,可以看出每一组包含3种算法,一个是非对称算法,如RSA,一个是对称算法如DES,3DES,RC4等,一个是Hash算法,如MD5,SHA等,server会从这些算法组合中选取一组,作为本次SSL连接中使用。

2.server回应ServerHello

ession id,如果SSL连接断开,再次连接时,可以使用该属性重新建立连接,在双方都有缓存的情况下可以省略握手的步骤。

server端也会生成随机的RN,用于生成session key使用

server会从client发送的Cipher suite列表中跳出一个,这里挑选的是RSA+RC4+MD5

这次server共发送的3个handshake 消息:Serverhello,Certificate和ServerHelloDone,共用一个ContentType:Handshake

3.server发送Certificate

server的证书信息,只包含public key,server将该public key对应的private key保存好,用于证明server是该证书的实际拥有者,那么如何验证呢?原理很简单:client随机生成一串数,用server这里的public key加密(显然是RSA算法),发给server,server用private key解密后返回给client,client与原文比较,如果一致,则说明server拥有private key,也就说明与client通信的正是证书的拥有者,因为public key加密的数据,只有private key才能解密,目前的技术还没发破解。利用这个原理,也能实现session key的交换,加密前的那串随机数就可用作session key,因为除了client和server,没有第三方能获得该数据了。原理很简单,实际使用时会复杂很多,数据经过多次hash,伪随机等的运算,前面提到的client和server端得RN都会参与计算。

4.Server发送ServerHelloDone

5.Client发送ClientKeyExchange

client拿到server的certificate后,就可以开始利用certificate里的public key进行session key的交换了。从图中可以看出,client发送的是130字节的字节流,显然是加过密的。client随机生成48字节的Pre-master secret,padding后用public key加密就得到这130字节的数据发送给server,server解密也能得到Pre-master secret。双方使用pre-master secret, “master secret"常量字节流,前期交换的server端RN和client的RN作为参数,使用一个伪随机函数PRF,其实就是hash之后再hash,最后得到48字节的master secret。master secret再与"key expansion"常量,双方RN经过伪随机函数运算得到key_block,PRF伪随机函数可以可以仿佛循环输出数据,因此我们想得到多少字节都可以,就如Random伪随机函数,给它一个种子,后续用hash计算能得到无数个随机数,如果每次种子相同,得到的序列是一样的,但是这里的输入时48字节的master secret,2个28字节的RN和一个字符串常量,碰撞的可能性是很小的。得到key block后,算法,就从中取出session key,IV(对称算法中使用的初始化向量)等。client和server使用的session key是不一样的,但只要双方都知道对方使用的是什么就行了。这里会取出4个:client/server加密正文的key,client/server计算handshake数据hash的key。

6.Client发送ChangeCipherSpec

client指示Server从现在开始发送的消息都是加密过的

7.Client发送Finished

client发送的加密数据,这个消息非常关键,一是能证明握手数据没有被篡改过,二也能证明自己确实是密钥的拥有者(这里是单边验证,只有server有certificate,server发送的Finished能证明自己含有private key,原理是一样的)。client将之前发送的所有握手消息存入handshake messages缓存,进行MD5和SHA-1两种hash运算,再与前面的master secret和一串常量"client finished"进行PRF伪随机运算得到12字节的verify data,还要经过改进的MD5计算得到加密信息。为什么能证明上述两点呢,前面说了只有密钥的拥有者才能解密得到pre-master key,master key,最后得到key block后,进行hash运算得到的结果才与发送方的一致。

8.Server发送ChangeCipherSpec

Server指示client从现在开始发送的消息都是加密过的

9.Server发送Finishd

与client发送Finished计算方法一致。server发送的Finished里包含hash给client,client会进行校验,如果通过,说明握手过程中的数据没有被第三方篡改过,也说明server是之前交换证书的拥有者。现在双方就可以开始后续通信,进入Application context了。