to inspire confidence in somebody.

0%

最近一段时间在折腾虚拟化,想把SR-IOV硬件直通给用起来,所以免不了要利用lspci这个工具,用来查看当前系统连接的所有PCI/PCIe设备。其实之前也有用到过,也有一些不理解的地方,只是当时无脑跟着文档设置,也就没多关心了,这次需要好好的理解一下相关的概念什么的。

首先很简单,看看不加参数直接调用lspci命令的输出结果,下面的是我笔记本上的输出:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
00:00.0 Host bridge: Intel Corporation Broadwell-U Host Bridge -OPI (rev 09)
00:02.0 VGA compatible controller: Intel Corporation HD Graphics 5500 (rev 09)
00:03.0 Audio device: Intel Corporation Broadwell-U Audio Controller (rev 09)
00:04.0 Signal processing controller: Intel Corporation Broadwell-U Processor Thermal Subsystem (rev 09)
00:14.0 USB controller: Intel Corporation Wildcat Point-LP USB xHCI Controller (rev 03)
00:16.0 Communication controller: Intel Corporation Wildcat Point-LP MEI Controller #1 (rev 03)
00:19.0 Ethernet controller: Intel Corporation Ethernet Connection (3) I218-LM (rev 03)
00:1b.0 Audio device: Intel Corporation Wildcat Point-LP High Definition Audio Controller (rev 03)
00:1c.0 PCI bridge: Intel Corporation Wildcat Point-LP PCI Express Root Port #1 (rev e3)
00:1c.3 PCI bridge: Intel Corporation Wildcat Point-LP PCI Express Root Port #4 (rev e3)
00:1c.4 PCI bridge: Intel Corporation Wildcat Point-LP PCI Express Root Port #5 (rev e3)
00:1d.0 USB controller: Intel Corporation Wildcat Point-LP USB EHCI Controller (rev 03)
00:1f.0 ISA bridge: Intel Corporation Wildcat Point-LP LPC Controller (rev 03)
00:1f.2 SATA controller: Intel Corporation Wildcat Point-LP SATA Controller [AHCI Mode] (rev 03)
00:1f.3 SMBus: Intel Corporation Wildcat Point-LP SMBus Controller (rev 03)
01:00.0 SD Host controller: O2 Micro, Inc. SD/MMC Card Reader Controller (rev 01)
02:00.0 Network controller: Intel Corporation Wireless 7265 (rev 59)
阅读全文 »

之前线上运行的K8S集群出现了一个Pod IP无法访问问题,调查了一下,发现和CalicoIP地址的分配策略相关,具体表现为一个/26的IP Block192.168.100.0/26分配给了A机器之后,在另外一台B机器上又出现了该IP Block内的一个IP 192.168.100.10,同时因为A机器上有该IP Block的blackhole路由blackhole 192.168.100.0/26 proto bird,所以导致A机器上所有的Pod访问192.168.100.10时因为黑洞路由原因直接失败。

遇到这个问题之前,只是通过文档大致了解Calico的IP分配策略,没有深入源码看看实际的情况,现在出现了相关问题,还是需要阅读一下相关代码,当然在这过程中也发现了一些问题,有些问题Calico官方也没有很好的解决。

阅读全文 »

前段时间将家里的树莓派3B系统换成了ArchLinux Arm,这样就可以用上64位指令集,不过遇到了一个问题,在没有连接HDMI时系统无法启动,刚开始还以为是什么其他的原因,因为之前接上显示器开机一切正常,但是换个地方不接显示器就启动不了,后来把显示器搬到旁边,一插上HDMI,立马就进入启动了。

定位到和HDMI相关之后,搜索了一下,刚开始以为和系统有关,后来搜到了Won’t boot without HDMI connected (solved)这个帖子,发现不是系统问题,是个简单的配置问题,很简单,在/boot/config.txt中,加入一行hdmi_force_hotplug=1就可以了,直接重启,没有接显示器也可以正常启动了。

这个配置信息在Video options in config.txt有详细的解释,意思是强制认为HDMI已经连接,直接开启HDMI输出。

阅读全文 »

今天在我们的环境中遇到了一个比较诡异的问题,我们在一台虚拟机上想要挂载一个CephFS,但是出现了一个failed: No such process的诡异问题,具体表现如下:

1
2
]# mount -t ceph mon1.ichenfu.com:6789,mon2.ichenfu.com:6789,mon3.ichenfu.com:6789:/ /tmp/data
mount: mount mon1.ichenfu.com:6789,mon2.ichenfu.com:6789,mon3.ichenfu.com:6789:/ on /tmp/data failed: No such process
阅读全文 »

在Libvirt里,一个Domain是一个运行在虚拟机器上的操作系统的实例,它可以指一个运行着的虚拟机,或者用于启动虚拟机的配置。
那么,对于一个Domain而言,主要有哪些状态呢?,他们的转换关系是什么?可以参考文档VM lifecycle。其中,状态主要包括以下:

阅读全文 »

随着Git仓库不断的被修改,整个仓库会变得越来越大,其中最主要的原因是历史提交特别的多,这个对于想立即阅读最新代码或者CI/CD场景下不是特别友好。

面对这种场景,可以利用git提供的浅克隆功能,只clone少部分历史到本地,这样可以极大的减少clone的仓库大小,以PHP源代码代码为例:

阅读全文 »

在Linux下,可以对进程使用的资源做一些限制,比如,可以使用的内存、可以使用的线程、最大能打开的文件数等等,这些也就是我们常说的rlimit,在bash里,可以非常方便的用ulimit这个内置的命令查看和修改这些限制,那么到底这些限制有那些,是怎么来的呢?

首先,在C编程环境下,系统提供了三个接口:int getrlimit(int resource, struct rlimit *rlim);int setrlimit(int resource, const struct rlimit *rlim);int prlimit(pid_t pid, int resource, const struct rlimit *new_limit, struct rlimit *old_limit);分别用来获取当前进程的限制、设置当前进程的限制以及根据Pid设置对应进程的限制。

那么具体有哪些限制,也就是接口中的resource参数有哪些,可以参考man里的信息,这里大致翻译一下:

阅读全文 »

工作中如果使用Git作为版本管理工具的话,应该经常会遇到因为各种原因一下提交了很多个commit的情况,比如添加一个功能,测试出问题继续commit修改,最后git log看提交历史就会变成这样:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
# git log
commit 9995aafb7a597d9a7fcf9a341a731324813c5aad (HEAD -> master)
...
Commit 4

commit 54062e7317fa19a228d8f4f63236467317c17672
...
Commit 3

commit 1571ee6b861315ec46875fbececd46c9daaa5d04
...
Commit 2

commit ae95aac116af934742e1dd2eca435a0d6e70b77f
...
Commit 1

commit 3f0373c3afb9e9ffd6174b8244ec3e936d3583e0
...
init

这样看起来不那么美观,也会一定程度上污染主分支,如果遇到问题,需要看diff的时候,也会不那么方便,所以介绍一个利用git rebase命令合并一系列commit的方法。

阅读全文 »

一般情况下,如果一个程序需要使用代理服务器,那么需要在运行的时候设置一下参数,或者,在Linux下,大部分的程序支持http_proxy这个环境变量,设置这个环境变量,意味着程序将使用设置值作为代理。
这样的问题在于,设置代理这个操作是不透明的,也就是说,客户端必须要知道代理的存在,需要手动设置将流量导入到代理,如果程序本身不支持代理,或者我们不希望执行所有程序的时候都手动设置代理,那么就需要一个相对“透明”的代理办法了。

同样的,作为ServiceMesh界的当红实现Istio,也会遇到类似的问题,如何在程序完全没有感知的情况下悄无声息的将程序的流量劫持到自己的代理呢?

借助Istio的两种实现方式,也说一下目前Liunx下两种透明代理的实现。

REDIRECT

首先是使用iptables的REDIRECT模式,通过iptables,可以将所有的流量都重定向到一个特定的端口上,如果配置过ss-redir的话,应该会对这种实现非常的熟悉,具体的,在iptables里对应一条规则:

1
iptables -t nat -A PREROUTING -p tcp -j REDIRECT --to-port 5000

即将所有流量都重定向到5000端口,仔细看一下,是不是和iptables实现DNAT有点相似?没错!本质上REDIRECT就是一个特殊的DNAT规则,一般情况下,我们利用iptables做DNAT的时候,需要指定目标的IP和端口,这样iptables才能知道需要将数据包的目的修改成什么,而REDIRECT模式下,只需要指定端口就可以,iptables会自动帮我们判断需要设置的IP地址。

继续思考,会发现另一个问题,那就是,既然是做了DNAT,也就意味着数据包里已经没有原始的目的地址了,那数据包到了代理程序,代理程序是如何知道这个数据包应该往什么地方发送呢?这个是个非常核心的问题,因为如果不知道原始的目的IP端口信息,代理完全不能起作用啊!

当然问题是有办法的,conntrack在这时候起作用了,conntrack会记录原始的地址,而在用户侧,内核提供了一个接口,可以让应用程序获取到原始的IP端口,可以参考一下ss-redir的实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
static int
getdestaddr(int fd, struct sockaddr_storage *destaddr)
{
socklen_t socklen = sizeof(*destaddr);
int error = 0;

error = getsockopt(fd, SOL_IPV6, IP6T_SO_ORIGINAL_DST, destaddr, &socklen);
if (error) { // Didn't find a proper way to detect IP version.
error = getsockopt(fd, SOL_IP, SO_ORIGINAL_DST, destaddr, &socklen);
if (error) {
return -1;
}
}
return 0;
}

利用getsockoptSO_ORIGINAL_DST参数,可以获取到原始的连接IP和端口,好了,目前代理所需要的所有的信息都完整了,整个代理理论上就可以工作了,剩下的就是代理如何实现的问题了,这里就不探讨了。

TPROXY

除了利用REDIRECT模式,Istio还提供TPROXY模式,当然也是借助Linux内核提供的功能实现的,对于TPROXY模式,实现的原理要相对复杂不少,需要借助iptables和路由:

1
2
3
iptables -t mangle -A PREROUTING -p tcp -j TPROXY --tproxy-mark 0x1/0x1 --on-port 8888
ip rule add fwmark 0x1/0x1 pref 100 table 100
ip route add local default dev lo table 100

通过iptables将数据包打上mark,然后使用一个特殊的路由,将数据包指向本地,由于使用了mangle表,所以数据包的原始和目的地址都是不会被修改的。

那么问题来了,应用程序怎么编写?假如需要连接1.2.3.4:80这个端口,就算数据包到了本地,但是本地并没有1.2.3.4这个IP地址啊,程序是怎么能拿到数据的?不是应该直接丢弃这个数据包么?
针对这个问题,可以看一个例子tproxy-example,这个例子实现了一个简单的基于TPROXY的代理。

针对上面的情况,Linux提供了一个选项IP_TRANSPARENT,这个选项很神奇,可以让程序bind一个不属于本机的地址,作为客户端,它可以使用一个不属于本机地址的IP地址作为源IP发起连接,作为服务端,它可以侦听在一个不属于本机的IP地址上,而这正是透明代理所必须的。我们看下例子程序里的代码:

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
26
27
28
29
30
31
32
33
if((listen_fd = socket(res->ai_family, res->ai_socktype,
res->ai_protocol)) == -1){
perror("socket: ");
exit(EXIT_FAILURE);
}

if(setsockopt(listen_fd, SOL_SOCKET, SO_REUSEADDR, &yes, sizeof(yes))
== -1){
perror("setsockopt (SO_REUSEADDR): ");
close(listen_fd);
exit(EXIT_FAILURE);
}

//Mark that this socket can be used for transparent proxying
//This allows the socket to accept connections for non-local IPs
if(setsockopt(listen_fd, SOL_IP, IP_TRANSPARENT, &yes, sizeof(yes))
== -1){
perror("setsockopt (IP_TRANSPARENT): ");
close(listen_fd);
exit(EXIT_FAILURE);
}

if(bind(listen_fd, res->ai_addr, res->ai_addrlen) == -1){
perror("bind: ");
close(listen_fd);
exit(EXIT_FAILURE);
}

if(listen(listen_fd, BACKLOG) == -1){
perror("listen: ");
close(listen_fd);
exit(EXIT_FAILURE);
}

也确实是设置了IP_TRANSPARENT,有了这个选项,也就意味着代理绑定了所有的IP,当然1.2.3.4这个IP也在范围内,所以可以正常的接受连接。
而由于TPROXY模式并没有改变数据包,所以直接通过getsockname获取到原始的IP端口信息:

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
26
27
28
29
30
31
32
33
//Store the original destination address in remote_addr
//Return 0 on success, <0 on failure
static int get_org_dstaddr(int sockfd, struct sockaddr_storage *orig_dst){
char orig_dst_str[INET6_ADDRSTRLEN];
socklen_t addrlen = sizeof(*orig_dst);

memset(orig_dst, 0, addrlen);

//For UDP transparent proxying:
//Set IP_RECVORIGDSTADDR socket option for getting the original
//destination of a datagram

//Socket is bound to original destination
if(getsockname(sockfd, (struct sockaddr*) orig_dst, &addrlen)
< 0){
perror("getsockname: ");
return -1;
} else {
if(orig_dst->ss_family == AF_INET){
inet_ntop(AF_INET,
&(((struct sockaddr_in*) orig_dst)->sin_addr),
orig_dst_str, INET_ADDRSTRLEN);
fprintf(stderr, "Original destination %s\n", orig_dst_str);
} else if(orig_dst->ss_family == AF_INET6){
inet_ntop(AF_INET6,
&(((struct sockaddr_in6*) orig_dst)->sin6_addr),
orig_dst_str, INET6_ADDRSTRLEN);
fprintf(stderr, "Original destination %s\n", orig_dst_str);
}

return 0;
}
}

总结

上面就是两种Linux下实现透明代理的方式,透过现象看本质,无论实现方式是什么,其实都定位到一个核心问题,即在没有代理的情况下,连接的五元组是什么?数据包最核心的源地址源端口,目的地址目的端口,无论是通过NAT方式修改数据包重定向,或者借助内核的一些特殊特性,都必须要知道这4个关键信息,一旦搞清楚这些,那理论上代理就能工作了,剩下的就是如何将代理本身做好,那就是一个业务逻辑的问题了。

参考:

  1. https://serverfault.com/questions/179200/difference-beetween-dnat-and-redirect-in-iptables
  2. https://vvl.me/2018/06/09/from-ss-redir-to-linux-nat/
  3. https://blog.csdn.net/dog250/article/details/13161945
  4. https://www.kernel.org/doc/Documentation/networking/tproxy.txt

如果K8s使用Calico作为网络方案的话,应该都会知道Calico是个纯3层的方案,也是就说,所有的数据包,都是通过路由的形式找到对应机器和容器的,然后通过BGP协议来将所有的路由同步到所有的机器或者数据中心,来完成整个网络的互联。
简单的来说,Calico针对一个容器,在主机上创建了一堆veth pair,其中一端在主机,一端在容器的网络空间里,然后在主机和容器中分别设置几条路由,来完成网络的互联,我们可以看一个例子:

主机上:

1
2
3
4
5
6
7
8
9
10
11
12
$ ip addr
...
771: cali45b9132fec1@if4: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1440 qdisc noqueue state UP group default
link/ether ee:ee:ee:ee:ee:ee brd ff:ff:ff:ff:ff:ff link-netnsid 14
inet6 fe80::ecee:eeff:feee:eeee/64 scope link
valid_lft forever preferred_lft forever
...

$ ip route
...
10.218.240.252 dev cali45b9132fec1 scope link
...

容器里:

1
2
3
4
5
6
7
8
9
10
11
12
13
$ ip a
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN qlen 1000
link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
inet 127.0.0.1/8 scope host lo
valid_lft forever preferred_lft forever
3: eth0@if771: <BROADCAST,MULTICAST,UP,LOWER_UP,M-DOWN> mtu 1440 qdisc noqueue state UP
link/ether 66:fb:34:db:c9:b4 brd ff:ff:ff:ff:ff:ff
inet 10.218.240.252/32 scope global eth0
valid_lft forever preferred_lft forever

$ ip route
default via 169.254.1.1 dev eth0
169.254.1.1 dev eth0

按照上面的逻辑,可以理一下:

  • 当目的地址是10.218.240.252的数据包,也就是目的是容器的数据包,到达主机,主机根据10.218.240.252 dev cali45b9132fec1 scope link这条路由,将数据包丢给cali45b9132fec1这个veth
    ,然后容器中对应的eth0就可以收到数据包了。
  • 当容器中的数据包需要发出,就是走默认路由,也就是default via 169.254.1.1 dev eth0,将数据包丢给eth0,这时主机对应的cali45b9132fec1可以收到包,然后继续进行路由选择,转发到对应端口。

这么一看好像没什么问题,但是总觉得不对,为什么容器里的默认网关是169.254.1.1呢?二层是怎么处理的?

我们重新思考一下数据包的传输:
当一个数据包的目的地址不是本机,所以需要查询路由表,当查到路由表中的网关之后,需要获取网关的MAC地址,并将数据包的MAC地址修改成网关地址,然后发送到对应的网卡。

问题来了。在容器里的网关是169.254.1.1,那网关的MAC地址是什么?
正常情况下,内核会对外发送ARP请求,去询问整个二层网络中谁拥有169.254.1.1这个IP地址,拥有这个IP地址的设备会将自己的MAC返回。
但是现在的情况是,对于容器和主机,都没有169.254.1.1这个IP,甚至,在主机上的端口cali45b9132fec1,MAC地址也是一个无用的ee:ee:ee:ee:ee:ee。所以,如果仅仅是目前的状况,容器和主机网络根本就无法通信!
所以Calico是怎么做到的呢?在Calico的FAQ里,官方给了答案:

Why can’t I see the 169.254.1.1 address mentioned above on my host?

Calico tries hard to avoid interfering with any other configuration on the host. Rather than adding the gateway address to the host side of each workload interface, Calico sets the proxy_arp flag on the interface. This makes the host behave like a gateway, responding to ARPs for 169.254.1.1 without having to actually allocate the IP address to the interface.

Calico利用了网卡的proxy_arp功能,具体的,是将/proc/sys/net/ipv4/conf/DEV/proxy_arp置为1,当设置这个标志之后,主机就会看起来像一个网关,会响应所有的ARP请求,并将自己的MAC地址告诉客户端。
也就是说,当容器发送ARP请求时,主机会告诉容器,我拥有169.254.1.1这个IP,我的MAC地址是XXX,这样,容器就可以顺利的将数据包发出来了,于是网络就通了。

其实Calico不仅仅设置了这个标志,但是这个标志是最重要的,毕竟关系到网络是否能通的问题。看了看Cailco的代码,发现Calico还设置了其他几个标志位:

  • /proc/sys/net/ipv4/conf/DEV/rp_filter => 1:开启反向路径过滤,确认数据包来源,对于普通容器,IP基本无法伪装,但是如果是VM(Calico也支持VM),很容易伪装IP地址,所以为了安全打开这个选项。
  • /proc/sys/net/ipv4/conf/DEV/route_localnet => 1:允许路由到本地。
  • /proc/sys/net/ipv4/neigh/DEV/proxy_delay => 0:默认情况下,主机为了减少ARP风暴的可能,会延迟一段时间回复ARP包,这个选项关闭这个延迟。
  • /proc/sys/net/ipv4/conf/DEV/forwarding => 1:允许转发数据包(如果不允许转发的话,那数据包就出不去主机了)。

上面是IPv4的情况,如果是IPv6的网络,则会设置:

  • /proc/sys/net/ipv6/conf/DEV/proxy_ndp => 1:这个和proxy_arp是一样的。
  • /proc/sys/net/ipv4/conf/DEV/forwarding => 1:同IPv4。