kk Blog —— 通用基础


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

shell 获取脚本的绝对路径

https://www.cnblogs.com/FlyFive/p/3640267.html

常见的一种误区,是使用 pwd 命令

该命令的作用是“print name of current/working directory”,这才是此命令的真实含义,当前的工作目录,这里没有任何意思说明,这个目录就是脚本存放的目录。所以,这是不对的。你可以试试 bash shell/a.sh,a.sh 内容是 pwd,你会发现,显示的是执行命令的路径 /home/june,并不是 a.sh 所在路径:/home/june/shell/a.sh

另一个误人子弟的答案,是 $0

这个也是不对的,这个$0是Bash环境下的特殊变量,其真实含义是:

Expands to the name of the shell or shell script. This is set at shell initialization. If bash is invoked with a file of commands, $0 is set to the name of that file. If bash is started with the -c option, then $0 is set to the first argument after the string to be executed, if one is present. Otherwise, it is set to the file name used to invoke bash, as given by argument zero.

这个$0有可能是好几种值,跟调用的方式有关系:

使用一个文件调用bash,那$0的值,是那个文件的名字(没说是绝对路径噢)

使用-c选项启动bash的话,真正执行的命令会从一个字符串中读取,字符串后面如果还有别的参数的话,使用从$0开始的特殊变量引用(跟路径无关了)

除此以外,$0会被设置成调用bash的那个文件的名字(没说是绝对路径)

正确:

1
basepath=$(cd `dirname $0`; pwd)

dirname $0,取得当前执行的脚本文件的父目录

cd dirname $0,进入这个目录(切换当前工作目录)

pwd,显示当前工作目录(cd执行后的)

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信息。