to inspire confidence in somebody.

0%

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

1
# git log
2
commit 9995aafb7a597d9a7fcf9a341a731324813c5aad (HEAD -> master)
3
...
4
    Commit 4
5
6
commit 54062e7317fa19a228d8f4f63236467317c17672
7
...
8
    Commit 3
9
10
commit 1571ee6b861315ec46875fbececd46c9daaa5d04
11
...
12
    Commit 2
13
14
commit ae95aac116af934742e1dd2eca435a0d6e70b77f
15
...
16
    Commit 1
17
18
commit 3f0373c3afb9e9ffd6174b8244ec3e936d3583e0
19
...
20
    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
static int
2
getdestaddr(int fd, struct sockaddr_storage *destaddr)
3
{
4
    socklen_t socklen = sizeof(*destaddr);
5
    int error         = 0;
6
7
    error = getsockopt(fd, SOL_IPV6, IP6T_SO_ORIGINAL_DST, destaddr, &socklen);
8
    if (error) { // Didn't find a proper way to detect IP version.
9
        error = getsockopt(fd, SOL_IP, SO_ORIGINAL_DST, destaddr, &socklen);
10
        if (error) {
11
            return -1;
12
        }
13
    }
14
    return 0;
15
}

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

TPROXY

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

1
iptables -t mangle -A PREROUTING -p tcp -j TPROXY --tproxy-mark 0x1/0x1 --on-port 8888
2
ip rule add fwmark 0x1/0x1 pref 100 table 100
3
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
if((listen_fd = socket(res->ai_family, res->ai_socktype,
2
                res->ai_protocol)) == -1){
3
    perror("socket: ");
4
    exit(EXIT_FAILURE);
5
}
6
7
if(setsockopt(listen_fd, SOL_SOCKET, SO_REUSEADDR, &yes, sizeof(yes))
8
        == -1){
9
    perror("setsockopt (SO_REUSEADDR): ");
10
    close(listen_fd);
11
    exit(EXIT_FAILURE);
12
}
13
14
//Mark that this socket can be used for transparent proxying
15
//This allows the socket to accept connections for non-local IPs
16
if(setsockopt(listen_fd, SOL_IP, IP_TRANSPARENT, &yes, sizeof(yes))
17
        == -1){
18
    perror("setsockopt (IP_TRANSPARENT): ");
19
    close(listen_fd);
20
    exit(EXIT_FAILURE);
21
}
22
23
if(bind(listen_fd, res->ai_addr, res->ai_addrlen) == -1){
24
    perror("bind: ");
25
    close(listen_fd);
26
    exit(EXIT_FAILURE);
27
}
28
29
if(listen(listen_fd, BACKLOG) == -1){
30
    perror("listen: ");
31
    close(listen_fd);
32
    exit(EXIT_FAILURE);
33
}

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

1
//Store the original destination address in remote_addr
2
//Return 0 on success, <0 on failure
3
static int get_org_dstaddr(int sockfd, struct sockaddr_storage *orig_dst){
4
    char orig_dst_str[INET6_ADDRSTRLEN];
5
    socklen_t addrlen = sizeof(*orig_dst);
6
7
    memset(orig_dst, 0, addrlen);
8
9
    //For UDP transparent proxying:
10
    //Set IP_RECVORIGDSTADDR socket option for getting the original
11
    //destination of a datagram
12
13
    //Socket is bound to original destination
14
    if(getsockname(sockfd, (struct sockaddr*) orig_dst, &addrlen)
15
            < 0){
16
        perror("getsockname: ");
17
        return -1;
18
    } else {
19
        if(orig_dst->ss_family == AF_INET){
20
            inet_ntop(AF_INET,
21
                    &(((struct sockaddr_in*) orig_dst)->sin_addr),
22
                    orig_dst_str, INET_ADDRSTRLEN);
23
            fprintf(stderr, "Original destination %s\n", orig_dst_str);
24
        } else if(orig_dst->ss_family == AF_INET6){
25
            inet_ntop(AF_INET6,
26
                    &(((struct sockaddr_in6*) orig_dst)->sin6_addr),
27
                    orig_dst_str, INET6_ADDRSTRLEN);
28
            fprintf(stderr, "Original destination %s\n", orig_dst_str);
29
        }
30
31
        return 0;
32
    }
33
}

总结

上面就是两种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
$ ip addr
2
...
3
771: cali45b9132fec1@if4: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1440 qdisc noqueue state UP group default
4
    link/ether ee:ee:ee:ee:ee:ee brd ff:ff:ff:ff:ff:ff link-netnsid 14
5
    inet6 fe80::ecee:eeff:feee:eeee/64 scope link
6
       valid_lft forever preferred_lft forever
7
...
8
9
$ ip route 
10
...
11
10.218.240.252 dev cali45b9132fec1 scope link
12
...

容器里:

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

偶然看到一篇cloudflare的博客How to drop 10 million packets per second,如何实现单核情况下一秒钟丢弃1000万个数据包,原文循序渐进,从最简单的用户态丢弃到使用非常新的技术XDP,逐步将单核丢包性能提升到10mpps,很有意思,网上也没有看到原文的中文版本,所以这里顺便翻译一下,看看cloudflare是如何处理类似的情况的。

阅读全文 »

想要删除K8s里的一个Namespace,结果删除了所有该Namespace资源之后使用kubectl delete namespace test发现删除不掉,一直卡在Terminating状态,使用--force参数依然无法删除,报错:
Error from server (Conflict): Operation cannot be fulfilled on namespaces "test": The system is ensuring all content is removed from this namespace. Upon completion, this namespace will automatically be purged by the system.
找了一圈,发现这个Issue,里面有条评论

kubectl get namespace annoying-namespace-to-delete -o json > tmp.json
then edit tmp.json and remove”kubernetes”

curl -k -H “Content-Type: application/json” -X PUT –data-binary @tmp.json https://kubernetes-cluster-ip/api/v1/namespaces/annoying-namespace-to-delete/finalize

and it should delete your namespace,

跟着试了一下,很管用,直接就删除了:
先运行kubectl get namespace test -o json > tmp.json,拿到当前namespace描述,然后打开tmp.json,删除其中的spec字段。因为这边的K8s集群是带认证的,所以又新开了窗口运行kubectl proxy跑一个API代理在本地的8081端口。最后运行curl -k -H "Content-Type: application/json" -X PUT --data-binary @tmp.json http://127.0.0.1:8001/api/v1/namespaces/test/finalize

搞定!

阅读全文 »

如果K8s中某个Node节点重启,在Event信息中会有一条消息,大致内容如Node xxxxx has been rebooted, boot id: xxx,而如果是重启kubelet,则不会有这条消息,所以kubelet是怎么判断是自己重启了还是机器重启了呢?

搜索了一下代码,在https://github.com/kubernetes/kubernetes/blob/v1.10.12/pkg/kubelet/kubelet_node_status.go#L621这里,会判断上次记录的Node的BootID和当前从cAdvisor获取的BootID是否相同,如果不同则说明机器重启了。

那么cAdvisor是怎么获得这个BootID的呢?看了一下cAdvisor的文档,发现默认是从/proc/sys/kernel/random/boot_id这个文件读取的。针对这个文件,找到一段解析:

/proc/sys/kernel/random/boot_id: A random ID that is regenerated on each boot. As such it can be used to identify the local machine’s current boot. It’s universally available on any recent Linux kernel. It’s a good and safe choice if you need to identify a specific boot on a specific booted kernel.

是内核暴露的一个接口,每次启动都会随机生成一个ID,是一个比较通用和安全的判断启动的办法。

参考:

  1. http://0pointer.de/blog/projects/ids.html

由于业务的需要,需要在我们的一台虚拟化机器上,实现如下的配置:

首先,需要将两块网卡设置Bonding并配置交换机对应端口trunk模式;在此基础上,添加宿主机的IP地址,并添加相应的VLAN,最后,还需要添加一个Bridge,用于桥接创建的虚拟机。

由于本身这台机器就是Openstack的宿主机,所以当前的状况是除了所需要的一个Bridge,其他都已经配置完成了,并且由于Openstack的原因,已经有个Bridge virbr0被绑定到bond0上了。
但是呢,这个Bridge是给ovs用的,也就是说,桥接在virbr0上的网络需要自己带上VLAN的tag才能正常工作,而我们希望的是再有一个Bridge br0,桥接在br0上不需要管理VLAN,保持和宿主机相同就可以。

阅读全文 »

在Kubernetes中,可以针对每个Pod设置DNS的策略,通过PodSpec下的dnsPolicy字段可以指定相应的策略,目前支持的策略如下:

  • Default“: Pod继承所在宿主机的设置,也就是直接将宿主机的/etc/resolv.conf内容挂载到容器中。
  • ClusterFirst“: 默认的配置,所有请求会优先在集群所在域查询,如果没有才会转发到上游DNS。
  • ClusterFirstWithHostNet“: 和ClusterFirst一样,不过是Pod运行在hostNetwork:true的情况下强制指定的。
  • None“: 1.9版本引入的一个新值,这个配置忽略所有配置,以Pod的dnsConfig字段为准。
阅读全文 »

一行Python代码生成随机字符串:

1
import random, string; print(''.join(random.choice(string.ascii_letters + string.digits) for _ in range(15)))
1
# python -c "import random, string; print(''.join(random.choice(string.ascii_letters + string.digits) for _ in range(15)))"
2
hTvLXAGUzTISKmZ

如果是Python3,还可以使用:

1
import random, string; print(''.join(random.choices(string.ascii_letters + string.digits, k=15)))
1
# python3 -c "import random, string; print(''.join(random.choices(string.ascii_letters + string.digits, k=15)))"            
2
BRYr0FncnkXUL9F

先说说背景,为什么会要了解一下/etc/resolv.conf配置,起因是一个跑在k8s集群的一个业务出现问题,仔细排查后,发现其中一个Pod的域名解析有问题,域名login.example.com被解析到了一个IP,而这个IP地址是另一个范域名*.ichenfu.com的解析,经过一番调查,最终发现是同事在配置一台机器上的kubelet时填错了clusterDomain的配置,将原本需要配置为c2.ichenfu.com的配置写成了c1.ichenfu.com,那么问题来了,为什么这么配置会导致DNS解析到一个错误的,而且是完全不相干的地址的呢?下面就慢慢分析一下。

首先还原一下场景,默认情况下,kubelet启动Pod的时候,会将DNS配置注入到Pod中,出问题的Pod里/etc/resove.conf内容如下:

1
nameserver 10.254.0.2
2
search default.svc.c1.ichenfu.com svc.c1.ichenfu.com c1.ichenfu.com localdomain
3
options ndots:5
阅读全文 »