Menu

  • Home
  • Work
    • Cloud
      • Virtualization
      • IaaS
      • PaaS
    • Java
    • Go
    • C
    • C++
    • JavaScript
    • PHP
    • Python
    • Architecture
    • Others
      • Assembly
      • Ruby
      • Perl
      • Lua
      • Rust
      • XML
      • Network
      • IoT
      • GIS
      • Algorithm
      • AI
      • Math
      • RE
      • Graphic
    • OS
      • Linux
      • Windows
      • Mac OS X
    • BigData
    • Database
      • MySQL
      • Oracle
    • Mobile
      • Android
      • IOS
    • Web
      • HTML
      • CSS
  • Life
    • Cooking
    • Travel
    • Gardening
  • Gallery
  • Video
  • Music
  • Essay
  • Home
  • Work
    • Cloud
      • Virtualization
      • IaaS
      • PaaS
    • Java
    • Go
    • C
    • C++
    • JavaScript
    • PHP
    • Python
    • Architecture
    • Others
      • Assembly
      • Ruby
      • Perl
      • Lua
      • Rust
      • XML
      • Network
      • IoT
      • GIS
      • Algorithm
      • AI
      • Math
      • RE
      • Graphic
    • OS
      • Linux
      • Windows
      • Mac OS X
    • BigData
    • Database
      • MySQL
      • Oracle
    • Mobile
      • Android
      • IOS
    • Web
      • HTML
      • CSS
  • Life
    • Cooking
    • Travel
    • Gardening
  • Gallery
  • Video
  • Music
  • Essay

IPVS模式下ClusterIP泄露宿主机端口的问题

5
Jan
2021

IPVS模式下ClusterIP泄露宿主机端口的问题

By Alex
/ in C,Linux,Network,PaaS
/ tags IPVS, K8S
0 Comments
问题

在一个启用了IPVS模式kube-proxy的K8S集群中,运行着一个Docker Registry服务。我们尝试通过docker manifest命令(带上--insecure参数)来推送manifest时,出现TLS timeout错误。

这个Registry通过ClusterIP类型的Service暴露访问端点,且仅仅配置了HTTP/80端口。docker manifest命令的--insecure参数的含义是,在Registry不支持HTTPS的情况下,允许使用不安全的HTTP协议通信。从报错上来看,很明显docker manifest认为Registry支持HTTPS协议。

在宿主机上尝试 telnet RegistryClusterIP 443,居然可以连通。检查后发现节点上使用443端口的,只有Ingress Controller的NodePort类型的Service,它在0.0.0.0上监听。删除此NodePort服务后,RegistryClusterIP:443就不通了,docker manifest命令恢复正常。

定义

如果kube-proxy启用了IPVS模式,并且宿主机在0.0.0.0:NonServicePort上监听,那么可以在宿主机上、或者Pod内,通过任意ClusterIP:NonServicePort访问到宿主机的NonServicePort。

这一行为显然不符合预期,我们期望仅仅在Service对象中声明的端口,才可能通过Cluster连通。如果ClusterIP上的未知端口,内核应该丢弃报文或者返回适当的ICMP。

如果kube-proxy使用iptables模式,不会出现这种异常行为。

原因

启用IPVS的情况下,所有ClusterIP都会绑定在kube-ipvs0这个虚拟的网络接口上。例如对于kube-dns服务的ClusterIP 10.96.0.10(ServicePort为TCP 53 / TCP 9153):

Shell
1
2
3
4
5: kube-ipvs0: <BROADCAST,NOARP> mtu 1500 qdisc noop state DOWN group default
    link/ether fa:d9:9e:37:12:68 brd ff:ff:ff:ff:ff:ff
    inet 10.96.0.10/32 brd 10.96.0.10 scope global kube-ipvs0
       valid_lft forever preferred_lft forever

这种绑定是必须的,因为IPVS的工作原理是,在netfilter挂载点LOCAL_IN上注册钩子ip_vs_in,拦截目的地是VIP(ClusterIP)的封包。而要使得封包进入到LOCAL_IN,它的目的地址必须是本机地址。

每当为网络接口添加一个IP地址,内核都会自动在local路由表中增加一条规则,对于上面的10.96.0.10,会增加:

Shell
1
2
# 对于目的地址是10.96.0.10的封包,从kube-ipvs0发出,如果没有指定源IP,使用10.96.0.10
local 10.96.0.10 dev kube-ipvs0 proto kernel scope host src 10.96.0.10

上述自动添加路由的一个副作用是,对于任意一个端口Port,如果不存在匹配ClusterIP:Port的IPVS规则,同时宿主机上某个应用在0.0.0.0:Port上监听,封包就会交由此应用处理。

在宿主机上执行 telnet 10.96.0.10 22,会发生以下事件序列:

  1. 出站选路,根据local表路由规则,从kube-ipvs0接口发出封包
  2. 由于kube-ipvs0是dummy的,封包立刻从kube-ipvs0的出站队列移动到入站队列
  3. 目的地址是本地地址,因此进入LOCAL_IN挂载点
  4. 由于22不是ServicePort,封包被转发给本地进程处理,即监听了22的那个进程

如果删除内核自动在local表中添加的路由:

Shell
1
ip route del table local local 10.96.0.10 dev kube-ipvs0 proto kernel scope host src 10.96.0.10

则会出现以下现象:

  1. 无法访问10.96.0.10:22。这是我们期望的,因为10.96.0.10这个服务没有暴露22端口,此端口理当不通
  2. 无法ping 10.96.0.10。这不是我们期望的,但是一般情况下不会有什么问题。iptables模式下ClusterIP就是无法ping的,IPVS模式下可以在本机ping仅仅是绑定ClusterIP到kube-ipvs0的一个副作用。通常应用程序不应该对ClusterIP做ICMP检测,来判断服务是否可用,因为这依赖了kube-proxy的特定工作模式
  3. 在宿主机上,可以访问10.96.0.10:53。这是我们期望的,宿主机上可以访问ClusterIP
  4. 在某个容器的网络命名空间下,无法访问10.96.0.10:53。这不是我们期望的,相当于Pod无法访问ClusterIP了

以上4条,惟独3难以理解。为什么路由没了,宿主机仍然能访问ClusterIP:ServicePort?这个我们还没有从源码级别深究,但是很明显和IPVS有关。IPVS在LOCAL_OUT上挂有钩子,它可能在此钩子中检测到来自本机(主网络命名空间)的、访问ClusterIP+ServicePort(即IPVS虚拟服务)的封包,并进行了某种“魔法”处理,从而避开了没有路由的问题。

下面我们进一步验证上述“魔法”处理的可能性。使用 tcpdump -i any host 10.96.0.10来捕获流量,从容器命名空间访问ClusterIP:ServicePort时,可以看到:

Shell
1
2
#                  容器IP
11:32:00.448470 IP 172.27.0.24.56378 > 10.96.0.10.53: Flags [S], seq 2946888109, win 28200, options...

但是从宿主机访问ClusterIP:ServicePort时,则捕获不到任何流量。但是,通过iptables logging,我们可以确定,内核的确以ClusterIP为源地址和目的地址,发起了封包:

Shell
1
2
3
4
5
iptables -t mangle -I OUTPUT 1 -p tcp --dport 53 -j LOG --log-prefix 'out-d53: '
 
# dmesg -w
#                                      源地址          目的地址
# [3374381.426541] out-d53: IN= OUT=lo SRC=10.96.0.100 DST=10.96.0.100 LEN=52 TOS=0x10 PREC=0x00 TTL=64 ID=18885 DF PROTO=TCP SPT=42442 DPT=53 WINDOW=86 RES=0x00 ACK URGP=0 

回顾一下数据报出站、入站的处理过程:

  1. 出站,依次经过 netfilter/iptables ⇨ tcpdump ⇨ 网络接口 ⇨网线
  2. 入站,依次经过 网线 ⇨ 网络接口 ⇨ tcpdump ⇨ netfilter/iptables

只有当IPVS在宿主机请求10.96.0.10的封包出站时,在netfilter中对匹配IPVS虚拟服务的封包进行如下处理,才能解释iptables中能看到10.96.0.10,而紧随其后的tcpdump中却又看不到的现象:

  1. 修改目的地址为Service的Endpoint地址,这就是NAT模式的IPVS(即kube-proxy使用NAT模式)应有的行为
  2. 修改了源地址为当前宿主机的地址,不这样做,回程报文就无法路由回来

另外注意一下,如果从宿主机访问ClusterIP:NonServicePort,则tcpdump能捕获到源或目的地址为ClusterIP的流量。这是因为IPVS发现它不匹配任何虚拟服务,会直接返回NF_ACCEPT,然后封包就按照常规流程处理了。

后果
安全问题

如果宿主机上有一个在0.0.0.0上监听的、存在安全漏洞的服务,则可能被恶意的工作负载利用。

行为异常

少部分的应用程序,例如docker manifest,其行为取决于端口探测的结果,会无法正常工作。

解决

可能的解决方案有:

  1. 在iptables中匹配哪些针对ClusterIP:NonServicePort的流量,Drop或Reject掉
  2. 使用基于fwmark的IPVS虚拟服务,这需要在iptables中对针对ClusterIP:ServicePort的流量打fwmark,而且每个ClusterIP都需要占用独立的fwmark,难以管理

对于解决方案1,可以使用如下iptables规则: 

Shell
1
2
#                 如果目的地址是ClusterIP    但是目的端口不是ServicePort           则拒绝
iptables -A INPUT -d  10.96.0.0/12 -m set ! --match-set KUBE-CLUSTER-IP dst,dst -j REJECT

这个规则能够为容器解决宿主机端口泄露的问题,但是会导致宿主机上无法访问ClusterIP。

引起此问题的原因是,在宿主机访问ClusterIP时,会同时使用ClusterIP作为源地址/目的地址。这样,来自Endpoint的回程报文,unNATed后的目的地址,就会匹配到上面的iptables规则,从而导致封包被Reject掉。

要解决此问题,我们可以修改内核自动添加的路由,提示使用其它地址作为源地址:

Shell
1
2
# 这条路由给出src提示,当访问10.96.0.10时,选取192.168.104.82(节点IP)作为源地址
ip route replace table local local 10.96.0.10 dev kube-ipvs0 proto kernel scope host src 192.168.104.82
深入

上文我们提到了一个“魔法”处理的猜想,这里我们对IPVS的实现细节进行深入学习,证实此猜想。

本节牵涉到的内核源码均来自linux-3.10.y分支。

Netfilter

这是从2.4.x引入内核的一个框架,用于实现防火墙、NAT、封包修改、记录封包日志、用户空间封包排队之类的功能。

netfilter运行在内核中,允许内核模块在Linux网络栈的不同位置注册钩子(回调函数),当每个封包穿过网络栈时,这些钩子函数会被调用。

iptables是经典的,基于netfilter的用户空间工具。它的继任者是nftables,它更加灵活、可扩容、性能好。

钩子挂载点

netfilter提供了5套钩子(的挂载点):

挂载点 说明
NF_IP_PER_ROUTING

当封包进入网络栈时调用。封包的目的地可能是本机,或者需要转发

ip_rcv / ipv6_rcv是内核接受并处理IP数据报的入口,此函数会调用这类钩子:

C
1
2
3
4
5
6
int ip_rcv(struct sk_buff *skb, struct net_device *dev, struct packet_type *pt, struct net_device *orig_dev)
{
    // ...
    return NF_HOOK(NFPROTO_IPV4, NF_INET_PRE_ROUTING, skb, dev, NULL,
               ip_rcv_finish);
}
NF_IP_LOCAL_IN

当路由判断封包应该由本机处理时(目的地址是本机地址)调用

ip_local_deliver / ip6_input负责将IP数据报向上层传递,此函数会调用这类钩子

NF_IP_FORWARD

当路由判断封包应该被转发给其它机器(或者网络命名空间)时调用

ip_forward / ip6_forward负责封包转发,此函数会调用这类钩子

NF_IP_POST_ROUTING

在封包即将离开网络栈(进入网线)时调用,不管是转发的、还是本机发出的,都需要经过此挂载点

ip_output / ip6_finish_output2会调用这类钩子

NF_IP_LOCAL_OUT

当封包由本机产生,需要往外发送时调用

__ip_local_out / __ip6_local_out会调用这类钩子

这些挂载点,和iptables的各链是对应的。

注册钩子

要在内核中使用netfilter的钩子,你需要调用函数:

C
1
2
3
4
// 注册钩子
int nf_register_hook(struct nf_hook_ops *reg){}
// 反注册钩子
void nf_unregister_hook(struct nf_hook_ops *reg){}

入参nf_hook_ops是一个结构:

C
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
struct nf_hook_ops {
    // 钩子的函数指针,依据内核的版本不同此函数的签名有所差异
    nf_hookfn        *hook;
    struct net_device    *dev;
    void            *priv;
    // 钩子针对的协议族,PF_INET表示IPv4
    u_int8_t        pf;
    // 钩子类型代码,参考上面的表格
    unsigned int        hooknum;
    // 每种类型的钩子,都可以有多个,此数字决定执行优先级
    int            priority;
};
 
 
// 钩子函数的签名
typedef unsigned int nf_hookfn(unsigned int hooknum,
                   struct sk_buff *skb, // 正被处理的数据报
                   const struct net_device *in, // 输入设备
                   const struct net_device *out, // 是出设备
                   int (*okfn)(struct sk_buff *)); // 如果通过钩子检查,则调用此函数,通常用不到
钩子返回值
C
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
/* Responses from hook functions. */
// 丢弃该报文,不再继续传输或处理
#define NF_DROP 0
// 继续正常传输报文,如果后面由低优先级的钩子,仍然会调用它们
#define NF_ACCEPT 1
// 告知netfilter,报文被别人偷走处理了,不需要再对它做任何处理
// 下文的分析中,我们有个例子。一个netfilter钩子在内部触发了对netfilter钩子的调用
// 外层钩子返回的就是NF_STOLEN,相当于将封包的控制器转交给内层钩子了
#define NF_STOLEN 2
// 对该数据报进行排队,通常用于将数据报提交给用户空间进程处理
#define NF_QUEUE 3
// 再次调用该钩子函数
#define NF_REPEAT 4
// 继续正常传输报文,不会调用此挂载点的后续钩子
#define NF_STOP 5
#define NF_MAX_VERDICT NF_STOP 
钩子优先级

优先级通常以下面的枚举为基准+/-:

C
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
enum nf_ip_hook_priorities {
    // 数值越小,优先级越高,越先执行
    NF_IP_PRI_FIRST = INT_MIN,
    NF_IP_PRI_CONNTRACK_DEFRAG = -400,
    // 可以看到iptables各表注册的钩子的优先级
    NF_IP_PRI_RAW = -300,
    NF_IP_PRI_SELINUX_FIRST = -225,
    NF_IP_PRI_CONNTRACK = -200,
    NF_IP_PRI_MANGLE = -150,
    NF_IP_PRI_NAT_DST = -100,
    NF_IP_PRI_FILTER = 0,
    NF_IP_PRI_SECURITY = 50,
    NF_IP_PRI_NAT_SRC = 100,
    NF_IP_PRI_SELINUX_LAST = 225,
    NF_IP_PRI_CONNTRACK_HELPER = 300,
    NF_IP_PRI_CONNTRACK_CONFIRM = INT_MAX,
    NF_IP_PRI_LAST = INT_MAX,
};
IPVS
钩子列表

 ip_vs模块初始化时,会通过ip_vs_init函数,调用nf_register_hook,注册以下netfilter钩子:

C
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
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
static struct nf_hook_ops ip_vs_ops[] __read_mostly = {
    // 注册到LOCAL_IN,这两个钩子处理外部客户端的报文
    // 转而调用ip_vs_out,用于NAT模式下,处理LVS回复外部客户端的报文,例如修改IP地址
    {
        .hook        = ip_vs_reply4,
        .owner        = THIS_MODULE,
        .pf        = NFPROTO_IPV4,
        .hooknum    = NF_INET_LOCAL_IN,
        .priority    = NF_IP_PRI_NAT_SRC - 2,
    },
    // 转而调用ip_vs_in,用于处理外部客户端进入IPVS的请求报文
    // 如果没有对应请求报文的连接,则使用调度函数创建连接结构,这其中牵涉选择RS负载均衡算法
    {
        .hook        = ip_vs_remote_request4,
        .owner        = THIS_MODULE,
        .pf        = NFPROTO_IPV4,
        .hooknum    = NF_INET_LOCAL_IN,
        .priority    = NF_IP_PRI_NAT_SRC - 1,
    },
 
    // 注册到LOCAL_OUT,这两个钩子处理LVS本机的报文
    // 转而调用ip_vs_out,用于NAT模式下,处理LVS回复客户端的报文
    {
        .hook        = ip_vs_local_reply4,
        .owner        = THIS_MODULE,
        .pf        = NFPROTO_IPV4,
        .hooknum    = NF_INET_LOCAL_OUT,
        .priority    = NF_IP_PRI_NAT_DST + 1,
    },
    // 转而调用ip_vs_in,调度并转发(给RS)本机的请求
    {
        .hook        = ip_vs_local_request4,
        .owner        = THIS_MODULE,
        .pf        = NFPROTO_IPV4,
        .hooknum    = NF_INET_LOCAL_OUT,
        .priority    = NF_IP_PRI_NAT_DST + 2,
    },
 
    // 这两个函数注册到FORWARD
    // 转而调用ip_vs_in_icmp,用于处理外部客户端发到IPVS的ICMP报文,并转发到RS
    {
        .hook        = ip_vs_forward_icmp,
        .owner        = THIS_MODULE,
        .pf        = NFPROTO_IPV4,
        .hooknum    = NF_INET_FORWARD,
        .priority    = 99,
    },
    // 转而调用ip_vs_out,用于NAT模式下,修改RS给的应答报文的源地址为IPVS虚拟地址
    {
        .hook        = ip_vs_reply4,
        .owner        = THIS_MODULE,
        .pf        = NFPROTO_IPV4,
        .hooknum    = NF_INET_FORWARD,
        .priority    = 100,
    }
};
ip_vs_in

从上面的钩子我们可以看到:

  1. 针对外部发起的、本机发起的,对IPVS的请求(目的是VIP的SYN),钩子的位置是不一样的:
    1. 对于外部的请求,在LOCAL_IN中处理,钩子函数为ip_vs_remote_request4
    2. 对于本机的请求,在LOCAL_OUT中处理,钩子函数为ip_vs_local_request4
  2.  尽管钩子的位置不同,但是函数ip_vs_remote_request4、ip_vs_local_request4都是调用ip_vs_in。实际上,这两个函数的逻辑完全一样:
    C
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    /*
    *    AF_INET handler in NF_INET_LOCAL_IN chain
    *    Schedule and forward packets from remote clients
    */
    static unsigned int
    ip_vs_remote_request4(unsigned int hooknum, struct sk_buff *skb,
                  const struct net_device *in,
                  const struct net_device *out,
                  int (*okfn)(struct sk_buff *))
    {
        return ip_vs_in(hooknum, skb, AF_INET);
    }
     
    /*
    *    AF_INET handler in NF_INET_LOCAL_OUT chain
    *    Schedule and forward packets from local clients
    */
    static unsigned int
    ip_vs_local_request4(unsigned int hooknum, struct sk_buff *skb,
                 const struct net_device *in, const struct net_device *out,
                 int (*okfn)(struct sk_buff *))
    {
        return ip_vs_in(hooknum, skb, AF_INET);
    }

回顾一下上文我们关于“魔法”处理的疑惑。对于从宿主机发起对10.96.0.10:53的请求,我们通过iptables logging证实了使用的源IP地址是10.96.0.10:

  1. 这个请求为什么tcpdump捕获不到?
  2. 为什么删除路由不影响宿主机对ClusterIP的请求(却又导致容器无法请求ClusterIP)?

这两个问题的答案,很可能就隐藏在ip_vs_in函数中,因为它是处理进入IPVS的数据报的统一入口。如果该函数同时修改了原始封包的源/目的地址,就解释了问题1;如果该函数在内部进行了选路操作,则解释了问题2。

下面分析一下ip_vs_in的代码:

C
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
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
static unsigned int
ip_vs_in(unsigned int hooknum, struct sk_buff *skb, int af)
{
    // 网络命名空间
    struct net *net;
    // IPVS的IP头,其中存有3层头len、协议、标记、源/目的地址
    struct ip_vs_iphdr iph;
    // 持有协议(TCP/UDP/SCTP/AH/ESP)信息,更重要的是带着很多函数指针。这些指针负责针对特定协议的IPVS逻辑
    struct ip_vs_protocol *pp;
    // 每个命名空间一个此对象,包含统计计数器、超时表
    struct ip_vs_proto_data *pd;
    // 当前封包所属的IPVS连接对象,此对象最重要的是packet_xmit函数指针。它负责将封包发走
    struct ip_vs_conn *cp;
    int ret, pkts;
    // 描述当前网络命名空间的IPVS状态
    struct netns_ipvs *ipvs;
 
    // 如果封包已经被标记为IPVS请求/应答,不做处理,继续netfilter常规流程
    // 后续ip_vs_nat_xmit会让封包“重入”netfilter,那时封包已经打上IPVS标记
    // 这里的判断确保重入的封包走netfilter常规流程,而不是进入死循环
    if (skb->ipvs_property)
        return NF_ACCEPT;
 
 
    // 如果封包目的地不是本机且当前不在LOCAL_OUT
    // 或者封包的dst_entry不存在,不做处理,继续netfilter常规流程
    if (unlikely((skb->pkt_type != PACKET_HOST &&
              hooknum != NF_INET_LOCAL_OUT) ||
             !skb_dst(skb))) {
        ip_vs_fill_iph_skb(af, skb, &iph);
        IP_VS_DBG_BUF(12, "packet type=%d proto=%d daddr=%s"
                  " ignored in hook %u\n",
                  skb->pkt_type, iph.protocol,
                  IP_VS_DBG_ADDR(af, &iph.daddr), hooknum);
        return NF_ACCEPT;
    }
    // 如果当前IPVS主机是backup,或者当前命名空间没有启用IPVS,不做处理,继续netfilter常规流程
    net = skb_net(skb);
    ipvs = net_ipvs(net);
    if (unlikely(sysctl_backup_only(ipvs) || !ipvs->enable))
        return NF_ACCEPT;
 
    // 使用封包的IP头填充IPVS的IP头
    ip_vs_fill_iph_skb(af, skb, &iph);
 
    // 如果是RAW套接字,不做处理,继续netfilter常规流程
    if (unlikely(skb->sk != NULL && hooknum == NF_INET_LOCAL_OUT &&
             af == AF_INET)) {
        struct sock *sk = skb->sk;
        struct inet_sock *inet = inet_sk(skb->sk);
 
        if (inet && sk->sk_family == PF_INET && inet->nodefrag)
            return NF_ACCEPT;
    }
 
    // 处理ICMP报文,和我们的场景无关
    if (unlikely(iph.protocol == IPPROTO_ICMP)) {
        int related;
        int verdict = ip_vs_in_icmp(skb, &related, hooknum);
        if (related)
            return verdict;
    }
 
    // 如果协议不受IPVS支持,不做处理,继续netfilter常规流程
    pd = ip_vs_proto_data_get(net, iph.protocol);
    if (unlikely(!pd))
        return NF_ACCEPT;
    // 协议被支持,得到pp
    pp = pd->pp;
    // 尝试获取封包所属的IPVS连接对象
    cp = pp->conn_in_get(af, skb, &iph, 0);
    // 如果封包属于既有IPVS连接,且此连接的RS(dest)已经设置,且RS的权重为0
    // 认为是无效连接,设为过期
    if (unlikely(sysctl_expire_nodest_conn(ipvs)) && cp && cp->dest &&
        unlikely(!atomic_read(&cp->dest->weight)) && !iph.fragoffs &&
        is_new_conn(skb, &iph)) {
        ip_vs_conn_expire_now(cp);
        __ip_vs_conn_put(cp);
        cp = NULL;
    }
 
    // 调度一个新的IPVS连接,这里牵涉到RS的LB算法
    if (unlikely(!cp) && !iph.fragoffs) {
        int v;
        if (!pp->conn_schedule(af, skb, pd, &v, &cp, &iph))
            // 如果返回0,通常v是NF_DROP,这以为这调度失败,封包丢弃
            return v;
    }
 
    if (unlikely(!cp)) {
        IP_VS_DBG_PKT(12, af, pp, skb, 0,
                  "ip_vs_in: packet continues traversal as normal");
        if (iph.fragoffs) {
            IP_VS_DBG_RL("Unhandled frag, load nf_defrag_ipv6\n");
            IP_VS_DBG_PKT(7, af, pp, skb, 0, "unhandled fragment");
        }
        return NF_ACCEPT;
    }
 
    // 入站封包 —— 在我们的场景中,这是本地客户端入了IPVS系统的封包
    // 从网络栈的角度来说,我们正在处理的是出站封包...
    IP_VS_DBG_PKT(11, af, pp, skb, 0, "Incoming packet");
 
    // IPVS连接的RS不可用
    if (cp->dest && !(cp->dest->flags & IP_VS_DEST_F_AVAILABLE)) {
        // 立即将连接设为过期
        if (sysctl_expire_nodest_conn(ipvs)) {
            ip_vs_conn_expire_now(cp);
        }
        // 丢弃封包
        __ip_vs_conn_put(cp);
        return NF_DROP;
    }
    // 更新计数器
    ip_vs_in_stats(cp, skb);
    // 更新IPVS连接状态机,做的事情包括
    //   根据数据包 tcp 标记字段来更新当前状态机
    //   更新连接对应的统计数据,包括:活跃连接和非活跃连接
    //   根据连接状态,设置超时时间
    ip_vs_set_state(cp, IP_VS_DIR_INPUT, skb, pd);
 
    if (cp->packet_xmit)
        // 调用packet_xmit将封包发走,实际上是重入netfilter的LOCAL_OUT,封包控制权转移走,后续不该再操控skb
        ret = cp->packet_xmit(skb, cp, pp, &iph);
    else {
        IP_VS_DBG_RL("warning: packet_xmit is null");
        ret = NF_ACCEPT;
    }
 
    if (cp->flags & IP_VS_CONN_F_ONE_PACKET)
        pkts = sysctl_sync_threshold(ipvs);
    else
        pkts = atomic_add_return(1, &cp->in_pkts);
 
    if (ipvs->sync_state & IP_VS_STATE_MASTER)
        ip_vs_sync_conn(net, cp, pkts);
 
    // 放回连接对象,重置连接定时器
    ip_vs_conn_put(cp);
    return ret;
}

上面这段代码中,“魔法”处理最可能发生在:

  1. conn_schedule:在这里需要进行IPVS连接的调度
  2. packet_xmit:在这里发送经过IPVS处理的封包

二者都是函数指针,在TCP协议下,conn_schedule指向tcp_conn_schedule。在NAT模式下,packet_xmit指向ip_vs_nat_xmit。packet_xmit指针是在conn_schedule过程中初始化的。

tcp_conn_schedule

我们看一下TCP协议下IPVS连接的调度过程。

C
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
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
static int
tcp_conn_schedule(int af, struct sk_buff *skb, struct ip_vs_proto_data *pd,
          int *verdict, struct ip_vs_conn **cpp,
          struct ip_vs_iphdr *iph)
{
    // 网络命名空间
    struct net *net;
    // IPVS虚拟服务对象
    struct ip_vs_service *svc;
    struct tcphdr _tcph, *th;
 
    // 解析L4头,如果失败,提示ip_vs_in丢弃封包
    th = skb_header_pointer(skb, iph->len, sizeof(_tcph), &_tcph);
    if (th == NULL) {
        *verdict = NF_DROP;
        return 0;
    }
    net = skb_net(skb);
    rcu_read_lock();
    if (th->syn &&
        // 根据封包特征,去查找匹配的虚拟服务
        (svc = ip_vs_service_find(net, af, skb->mark, iph->protocol,
                      &iph->daddr, th->dest))) {
        int ignored;
        // 如果当前网络命名空间“过载”了,丢弃封包
        if (ip_vs_todrop(net_ipvs(net))) {
            rcu_read_unlock();
            *verdict = NF_DROP;
            return 0;
        }
 
        // 选择一个RS,建立IPVS连接
        // 如果找不到RS,或者发生致命错误,则ignore为0或-1,这种情况下
        // IPVS连接没有成功创建,提示ip_vs_in丢弃封包,可能附带回复ICMP
        *cpp = ip_vs_schedule(svc, skb, pd, &ignored, iph);
        if (!*cpp && ignored <= 0) {
            if (!ignored)
                // ignored=0,找不到RS
                *verdict = ip_vs_leave(svc, skb, pd, iph);
            else
                *verdict = NF_DROP;
            rcu_read_unlock();
            return 0;
        }
    }
    rcu_read_unlock();
    // 如果调度成功,IPVS连接对象不为空,返回1
    /* NF_ACCEPT */
    return 1;
}

到这里我们还没有看到IPVS对封包地址进行更改,需要进一步阅读ip_vs_schedule。 

ip_vs_schedule

这是IPVS调度的核心函数,它支持TCP/UDP,它为虚拟服务选择一个RS,创建IPVS连接对象。

C
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
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
struct ip_vs_conn *
ip_vs_schedule(struct ip_vs_service *svc, struct sk_buff *skb,
           struct ip_vs_proto_data *pd, int *ignored,
           struct ip_vs_iphdr *iph)
{
    struct ip_vs_protocol *pp = pd->pp;
    // IPVS连接对象(connection entry)
    struct ip_vs_conn *cp = NULL;
    struct ip_vs_scheduler *sched;
    struct ip_vs_dest *dest;
    __be16 _ports[2], *pptr;
    unsigned int flags;
 
    // ...
 
    *ignored = 0;
 
    /*
     *    Non-persistent service
     */
    // 调度工作委托给虚拟服务的scheduler
    sched = rcu_dereference(svc->scheduler);
    // 调度器就是选择一个RS(ip_vs_dest)
    dest = sched->schedule(svc, skb);
    if (dest == NULL) {
        IP_VS_DBG(1, "Schedule: no dest found.\n");
        return NULL;
    }
 
    flags = (svc->flags & IP_VS_SVC_F_ONEPACKET
         && iph->protocol == IPPROTO_UDP) ?
        IP_VS_CONN_F_ONE_PACKET : 0;
 
    // 初始化IPVS连接对象 ip_vs_conn
    {
        struct ip_vs_conn_param p;
 
        ip_vs_conn_fill_param(svc->net, svc->af, iph->protocol,
                      &iph->saddr, pptr[0], &iph->daddr,
                      pptr[1], &p);
        // 操控ip_vs_conn的逻辑包括:
        //   初始化定时器
        //   设置网络命名空间
        //   设置地址、fwmark、端口
        //   根据IP版本、IPVS模式(NAT/DR/TUN)为连接设置一个packet_xmit
        cp = ip_vs_conn_new(&p, &dest->addr,
                    dest->port ? dest->port : pptr[1],
                    flags, dest, skb->mark);
        if (!cp) {
            *ignored = -1;
            return NULL;
        }
    }
 
    // ...
    return cp;
}

到这里我们可以看到, conn_schedule仍然没有对封包做任何修改。看来关键在packet_xmit函数中。

ip_vs_nat_xmit
C
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
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
int ip_vs_nat_xmit(struct sk_buff *skb, struct ip_vs_conn *cp,
           struct ip_vs_protocol *pp, struct ip_vs_iphdr *ipvsh)
{
    // 路由表项
    struct rtable *rt;        /* Route to the other host */
    // 是否本机    是否输入路由
    int local, rc, was_input;
 
    EnterFunction(10);
 
    rcu_read_lock();
    // 是否尚未设置客户端端口
    if (unlikely(cp->flags & IP_VS_CONN_F_NO_CPORT)) {
        __be16 _pt, *p;
 
        p = skb_header_pointer(skb, ipvsh->len, sizeof(_pt), &_pt);
        if (p == NULL)
            goto tx_error;
        // 设置IPVS连接对象的cport
        // caddr cport 客户端地址
        // vaddr vport 虚拟服务地址
        // daddr dport RS地址
        ip_vs_conn_fill_cport(cp, *p);
        IP_VS_DBG(10, "filled cport=%d\n", ntohs(*p));
    }
 
    was_input = rt_is_input_route(skb_rtable(skb));
    // 出口路由查找,依据是封包、RS的地址、以及若干标识位
    // 返回值提示路由目的地是否是本机
    local = __ip_vs_get_out_rt(skb, cp->dest, cp->daddr.ip,
                   IP_VS_RT_MODE_LOCAL |
                   IP_VS_RT_MODE_NON_LOCAL |
                   IP_VS_RT_MODE_RDR, NULL);
    if (local < 0)
        goto tx_error;
    rt = skb_rtable(skb);
 
    // 如果目的地是本机,RS地址是环回地址,是输入
    if (local && ipv4_is_loopback(cp->daddr.ip) && was_input) {
        IP_VS_DBG_RL_PKT(1, AF_INET, pp, skb, 0, "ip_vs_nat_xmit(): "
                 "stopping DNAT to loopback address");
        goto tx_error;
    }
 
    // 封包将被修改,执行copy-on-write
    if (!skb_make_writable(skb, sizeof(struct iphdr)))
        goto tx_error;
 
    if (skb_cow(skb, rt->dst.dev->hard_header_len))
        goto tx_error;
 
    // 修改封包,dnat_handler指向tcp_dnat_handler
    if (pp->dnat_handler && !pp->dnat_handler(skb, pp, cp, ipvsh))
        goto tx_error;
    // 更改目的地址
    ip_hdr(skb)->daddr = cp->daddr.ip;
    // 为出站封包生成chksum
    ip_send_check(ip_hdr(skb));
 
    IP_VS_DBG_PKT(10, AF_INET, pp, skb, 0, "After DNAT");
 
    skb->local_df = 1;
 
    // 发送封包:
    //   如果发送出去了,返回 NF_STOLEN
    //   如果没有发送(local=1,目的地是本机),返回NF_ACCEPT
    rc = ip_vs_nat_send_or_cont(NFPROTO_IPV4, skb, cp, local);
    rcu_read_unlock();
 
    LeaveFunction(10);
    return rc;
 
  tx_error:
    kfree_skb(skb);
    rcu_read_unlock();
    LeaveFunction(10);
    return NF_STOLEN;
}
 
static inline int ip_vs_nat_send_or_cont(int pf, struct sk_buff *skb,  struct ip_vs_conn *cp, int local)
{
    // 注意这个NF_STOLEN的含义,参考上文
    int ret = NF_STOLEN;
    // 给封包设置IPVS标记,NF_HOOK会导致当前封包重入netfilter,此标记会让重入后的封包立即NF_ACCEPT、
    // 重入让修改后的封包有机会被ipables处理
    skb->ipvs_property = 1;
    if (likely(!(cp->flags & IP_VS_CONN_F_NFCT)))
        ip_vs_notrack(skb);
    else
        ip_vs_update_conntrack(skb, cp, 1);
    // 如果目的地不是本机
    if (!local) {
        skb_forward_csum(skb);
        // 调用LOCAL_OUT挂载点
        NF_HOOK(pf, NF_INET_LOCAL_OUT, skb, NULL, skb_dst(skb)->dev, dst_output);
    } else
        ret = NF_ACCEPT;
    return ret;
}

在ip_vs_nat_xmit中,我们可以了解到,对于宿主机发起的针对ClusterIP:ServicePort的请求

  1. 封包的目的地址被修改为Endpoint(通常是Pod,IPVS中的RS)的地址
  2. 修改后的封包,重新被塞入netfilter(内层),注意当前就正在netfilter(外层)中
    1. 外层钩子的返回值是NF_STOLEN:封包处理权转移给内层钩子,停止后续netfilter流程
    2. 内层钩子的返回值是NF_ACCEPT:不做IPVS相关处理,继续后续netfilter流程。IPVS前、后的LOCAL_OUT、POSTROUTING钩子都会正常执行。也就是说,对于修改后的封包,内核会进行完整、常规的netfilter处理,就像没有IPVS存在一样

到这里,我们确定了,IPVS会在LOCAL_OUT中进行DNAT。但是只有同时进行SNAT,才能解释上文的中的疑惑。

SNAT

花费了不少时间在IPVS上探究后,我们意识到走错了方向。我们忘记了SANT是kube-proxy会去做的事情。查看一下iptables规则就一目了然了:

Shell
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
# iptables -t nat -L -n -v
 
Chain OUTPUT (policy ACCEPT 2 packets, 150 bytes)
pkts bytes target     prot opt in     out     source        destination        
# 所有出站流量都要经过自定义的 KUBE-SERVICES 链
  21M 3825M KUBE-SERVICES  all  --  *  *   0.0.0.0/0         0.0.0.0/0            /* kubernetes service portals */
 
Chain KUBE-SERVICES (2 references)
pkts bytes target     prot opt in     out     source        destination        
# 如果目的IP:PORT属于K8S服务,则调用KUBE-MARK-MASQ链
    0     0 KUBE-MARK-MASQ  all  --  * *  !172.27.0.0/16     0.0.0.0/0   match-set KUBE-CLUSTER-IP dst,dst
 
# 给封包打上标记 0x4000
Chain KUBE-MARK-MASQ (5 references)
pkts bytes target     prot opt in     out     source         destination        
   98  5880 MARK       all  --  *       *  0.0.0.0/0          0.0.0.0/0            MARK or 0x4000
 
Chain POSTROUTING (policy ACCEPT 2 packets, 150 bytes)
pkts bytes target     prot opt in     out     source         destination        
  44M 5256M KUBE-POSTROUTING  all  --  *  *       0.0.0.0/0   0.0.0.0/0            /* kubernetes postrouting rules */
 
Chain KUBE-POSTROUTING (1 references)
pkts bytes target     prot opt in     out     source         destination        
# 仅仅处理 0x4000标记的封包
1781  166K RETURN     all  --  *      *  0.0.0.0/0           0.0.0.0/0            mark match ! 0x4000/0x4000
# 执行SNAT    
   97  5820 MARK       all  --  *      *  0.0.0.0/0           0.0.0.0/0   MARK xor 0x4000
   97  5820 MASQUERADE  all  --  *     *  0.0.0.0/0           0.0.0.0/0   /* kubernetes service traffic requiring SNAT */

由于ip_vs_local_request4挂钩在LOCAL_OUT,优先级为NF_IP_PRI_NAT_DST+2 ,因此它是发生在上面nat表OUTPUT链中MARK之后的。也就是说在IPVS处理之前,kube-proxy已经给原始的封包打上标记。

重入的、DNAT后的封包进入LOCAL_OUT,随后进入POSTROUTING。由于标记的缘故,封包被kube-proxy的规则SNAT。

经过POSTROUTING的封包,经过tcpdump,但是由于源、目的IP地址,以及目的端口都改变了,因而我们看到tcpdump没有任何输出。

总结

这里做一下小结。

为什么IPVS模式下,能够ping通ClusterIP? 这是因为IPVS模式下,ClusterIP被配置为宿主机上一张虚拟网卡kube-ipvs0的IP地址。

为什么IPVS模式下,宿主机端口被ClusterIP泄漏?每当添加一个ClusterIP给网络接口后,内核自动在local表中增加一条路由,此路由保证了针对ClusterIP的访问,在没有IPVS干涉的情况下,路由到本机处理。这样,在0.0.0.0上监听的进程,就接收到报文并处理。

为什么删除内核添加的路由后:

  1. 宿主机上访问ClusterIP:NonServicePort不通了?因为没有路由了
  2. 没有路由了,为什么宿主机上访问ClusterIP:ServicePort仍然畅通?如上文分析,IPVS在ip_vs_nat_xmit中仍然会进行选路操作
  3. 那为什么从容器网络命名空间访问ClusterIP:ServicePort不通呢?IPVS处理本地、远程客户端的代码路径不一样。容器网络命名空间是远程客户端,需要首先进入PER_ROUTING,然后选路,路由目的地是本机,才会进入LOCAL_IN,IPVS才有介入的时机。由于路由被删掉了,选路那一步就会出问题

为什么通过--match-set KUBE-CLUSTER-IP匹配目的地址,如果封包目的端口是NonServicePort则Reject:

  1. 这种方案对容器命名空间有效?容器请求的源地址不会是ClusterIP,因此回程报文的目的地址不会因为匹配规则而Reject
  2. 这种方案导致宿主机无法访问ClusterIP?宿主机发起请求时用的是ClusterIP,请求端口是随机的。这种请求的回程报文必然匹配规则导致Reject
← 念爷爷
eBPF学习笔记 →

Leave a Reply Cancel reply

Your email address will not be published. Required fields are marked *

You may use these HTML tags and attributes: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code class="" title="" data-url=""> <del datetime=""> <em> <i> <q cite=""> <strike> <strong> <pre class="" title="" data-url=""> <span class="" title="" data-url="">

Related Posts

  • 基于Calico的CNI
  • Kubefed学习笔记
  • CRIU和Pod在线迁移
  • Kubernetes集群部署记录
  • 使用Grafana展示时间序列数据

Recent Posts

  • Investigating and Solving the Issue of Failed Certificate Request with ZeroSSL and Cert-Manager
  • A Comprehensive Study of Kotlin for Java Developers
  • 背诵营笔记
  • 利用LangChain和语言模型交互
  • 享学营笔记
ABOUT ME

汪震 | Alex Wong

江苏淮安人,现居北京。目前供职于腾讯云,专注容器方向。

GitHub:gmemcc

Git:git.gmem.cc

Email:gmemjunk@gmem.cc@me.com

ABOUT GMEM

绿色记忆是我的个人网站,域名gmem.cc中G是Green的简写,MEM是Memory的简写,CC则是我的小天使彩彩名字的简写。

我在这里记录自己的工作与生活,同时和大家分享一些编程方面的知识。

GMEM HISTORY
v2.00:微风
v1.03:单车旅行
v1.02:夏日版
v1.01:未完成
v0.10:彩虹天堂
v0.01:阳光海岸
MIRROR INFO
Meta
  • Log in
  • Entries RSS
  • Comments RSS
  • WordPress.org
Recent Posts
  • Investigating and Solving the Issue of Failed Certificate Request with ZeroSSL and Cert-Manager
    In this blog post, I will walk ...
  • A Comprehensive Study of Kotlin for Java Developers
    Introduction Purpose of the Study Understanding the Mo ...
  • 背诵营笔记
    Day 1 Find Your Greatness 原文 Greatness. It’s just ...
  • 利用LangChain和语言模型交互
    LangChain是什么 从名字上可以看出来,LangChain可以用来构建自然语言处理能力的链条。它是一个库 ...
  • 享学营笔记
    Unit 1 At home Lesson 1 In the ...
  • K8S集群跨云迁移
    要将K8S集群从一个云服务商迁移到另外一个,需要解决以下问题: 各种K8S资源的迁移 工作负载所挂载的数 ...
  • Terraform快速参考
    简介 Terraform用于实现基础设施即代码(infrastructure as code)—— 通过代码( ...
  • 草缸2021
    经过四个多月的努力,我的小小荷兰景到达极致了状态。

  • 编写Kubernetes风格的APIServer
    背景 前段时间接到一个需求做一个工具,工具将在K8S中运行。需求很适合用控制器模式实现,很自然的就基于kube ...
  • 记录一次KeyDB缓慢的定位过程
    环境说明 运行环境 这个问题出现在一套搭建在虚拟机上的Kubernetes 1.18集群上。集群有三个节点: ...
  • eBPF学习笔记
    简介 BPF,即Berkeley Packet Filter,是一个古老的网络封包过滤机制。它允许从用户空间注 ...
  • IPVS模式下ClusterIP泄露宿主机端口的问题
    问题 在一个启用了IPVS模式kube-proxy的K8S集群中,运行着一个Docker Registry服务 ...
  • 念爷爷
      今天是爷爷的头七,十二月七日、阴历十月廿三中午,老人家与世长辞。   九月初,回家看望刚动完手术的爸爸,发

  • 6 杨梅坑

  • liuhuashan
    深圳人才公园的网红景点 —— 流花山

  • 1 2020年10月拈花湾

  • 内核缺陷触发的NodePort服务63秒延迟问题
    现象 我们有一个新创建的TKE 1.3.0集群,使用基于Galaxy + Flannel(VXLAN模式)的容 ...
  • Galaxy学习笔记
    简介 Galaxy是TKEStack的一个网络组件,支持为TKE集群提供Overlay/Underlay容器网 ...
TOPLINKS
  • Zitahli's blue 91 people like this
  • 梦中的婚礼 64 people like this
  • 汪静好 61 people like this
  • 那年我一岁 36 people like this
  • 为了爱 28 people like this
  • 小绿彩 26 people like this
  • 杨梅坑 6 people like this
  • 亚龙湾之旅 1 people like this
  • 汪昌博 people like this
  • 彩虹姐姐的笑脸 24 people like this
  • 2013年11月香山 10 people like this
  • 2013年7月秦皇岛 6 people like this
  • 2013年6月蓟县盘山 5 people like this
  • 2013年2月梅花山 2 people like this
  • 2013年淮阴自贡迎春灯会 3 people like this
  • 2012年镇江金山游 1 people like this
  • 2012年徽杭古道 9 people like this
  • 2011年清明节后扬州行 1 people like this
  • 2008年十一云龙公园 5 people like this
  • 2008年之秋忆 7 people like this
  • 老照片 13 people like this
  • 火一样的六月 16 people like this
  • 发黄的相片 3 people like this
  • Cesium学习笔记 90 people like this
  • IntelliJ IDEA知识集锦 59 people like this
  • Bazel学习笔记 38 people like this
  • 基于Kurento搭建WebRTC服务器 38 people like this
  • PhoneGap学习笔记 32 people like this
  • NaCl学习笔记 32 people like this
  • 使用Oracle Java Mission Control监控JVM运行状态 29 people like this
  • Ceph学习笔记 27 people like this
  • 基于Calico的CNI 27 people like this
  • Three.js学习笔记 24 people like this
Tag Cloud
ActiveMQ AspectJ CDT Ceph Chrome CNI Command Cordova Coroutine CXF Cygwin DNS Docker eBPF Eclipse ExtJS F7 FAQ Groovy Hibernate HTTP IntelliJ IO编程 IPVS JacksonJSON JMS JSON JVM K8S kernel LB libvirt Linux知识 Linux编程 LOG Maven MinGW Mock Monitoring Multimedia MVC MySQL netfs Netty Nginx NIO Node.js NoSQL Oracle PDT PHP Redis RPC Scheduler ServiceMesh SNMP Spring SSL svn Tomcat TSDB Ubuntu WebGL WebRTC WebService WebSocket wxWidgets XDebug XML XPath XRM ZooKeeper 亚龙湾 单元测试 学习笔记 实时处理 并发编程 彩姐 性能剖析 性能调优 文本处理 新特性 架构模式 系统编程 网络编程 视频监控 设计模式 远程调试 配置文件 齐塔莉
Recent Comments
  • qg on Istio中的透明代理问题
  • heao on 基于本地gRPC的Go插件系统
  • 黄豆豆 on Ginkgo学习笔记
  • cloud on OpenStack学习笔记
  • 5dragoncon on Cilium学习笔记
  • Archeb on 重温iptables
  • C/C++编程:WebSocketpp(Linux + Clion + boostAsio) – 源码巴士 on 基于C/C++的WebSocket库
  • jerbin on eBPF学习笔记
  • point on Istio中的透明代理问题
  • G on Istio中的透明代理问题
  • 绿色记忆:Go语言单元测试和仿冒 on Ginkgo学习笔记
  • point on Istio中的透明代理问题
  • 【Maven】maven插件开发实战 – IT汇 on Maven插件开发
  • chenlx on eBPF学习笔记
  • Alex on eBPF学习笔记
  • CFC4N on eBPF学习笔记
  • 李运田 on 念爷爷
  • yongman on 记录一次KeyDB缓慢的定位过程
  • Alex on Istio中的透明代理问题
  • will on Istio中的透明代理问题
  • will on Istio中的透明代理问题
  • haolipeng on 基于本地gRPC的Go插件系统
  • 吴杰 on 基于C/C++的WebSocket库
©2005-2025 Gmem.cc | Powered by WordPress | 京ICP备18007345号-2