<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0"
	xmlns:content="http://purl.org/rss/1.0/modules/content/"
	xmlns:wfw="http://wellformedweb.org/CommentAPI/"
	xmlns:dc="http://purl.org/dc/elements/1.1/"
	xmlns:atom="http://www.w3.org/2005/Atom"
	xmlns:sy="http://purl.org/rss/1.0/modules/syndication/"
	xmlns:slash="http://purl.org/rss/1.0/modules/slash/"
	>

<channel>
	<title>绿色记忆 &#187; IPVS</title>
	<atom:link href="https://blog.gmem.cc/tag/ipvs/feed" rel="self" type="application/rss+xml" />
	<link>https://blog.gmem.cc</link>
	<description></description>
	<lastBuildDate>Thu, 16 Apr 2026 07:10:45 +0000</lastBuildDate>
	<language>en-US</language>
		<sy:updatePeriod>hourly</sy:updatePeriod>
		<sy:updateFrequency>1</sy:updateFrequency>
	<generator>http://wordpress.org/?v=3.9.14</generator>
	<item>
		<title>IPVS模式下ClusterIP泄露宿主机端口的问题</title>
		<link>https://blog.gmem.cc/nodeport-leak-under-ipvs-mode</link>
		<comments>https://blog.gmem.cc/nodeport-leak-under-ipvs-mode#comments</comments>
		<pubDate>Tue, 05 Jan 2021 10:50:51 +0000</pubDate>
		<dc:creator><![CDATA[Alex]]></dc:creator>
				<category><![CDATA[C]]></category>
		<category><![CDATA[Linux]]></category>
		<category><![CDATA[Network]]></category>
		<category><![CDATA[PaaS]]></category>
		<category><![CDATA[IPVS]]></category>
		<category><![CDATA[K8S]]></category>

		<guid isPermaLink="false">https://blog.gmem.cc/?p=35061</guid>
		<description><![CDATA[<p>问题 在一个启用了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协议。 在宿主机上尝试[crayon-69e0b2660fa9b333093119-i/]，居然可以连通。检查后发现节点上使用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）： [crayon-69e0b2660faa1370493199/] <a class="read-more" href="https://blog.gmem.cc/nodeport-leak-under-ipvs-mode">[...]</a></p>
<p>The post <a rel="nofollow" href="https://blog.gmem.cc/nodeport-leak-under-ipvs-mode">IPVS模式下ClusterIP泄露宿主机端口的问题</a> appeared first on <a rel="nofollow" href="https://blog.gmem.cc">绿色记忆</a>.</p>
]]></description>
				<content:encoded><![CDATA[<div class="wri_content_clear_both"><div class="blog_h1"><span class="graybg">问题</span></div>
<p>在一个启用了IPVS模式kube-proxy的K8S集群中，运行着一个Docker Registry服务。我们尝试通过docker manifest命令（带上--insecure参数）来推送manifest时，出现TLS timeout错误。</p>
<p>这个Registry通过ClusterIP类型的Service暴露访问端点，且仅仅配置了HTTP/80端口。docker manifest命令的--insecure参数的含义是，在Registry不支持HTTPS的情况下，允许使用不安全的HTTP协议通信。从报错上来看，很明显docker manifest认为Registry支持HTTPS协议。</p>
<p>在宿主机上尝试<pre class="crayon-plain-tag">telnet RegistryClusterIP 443</pre>，居然可以连通。检查后发现节点上使用443端口的，只有Ingress Controller的NodePort类型的Service，它在0.0.0.0上监听。删除此NodePort服务后，RegistryClusterIP:443就不通了，docker manifest命令恢复正常。</p>
<div class="blog_h1"><span class="graybg">定义</span></div>
<p>如果kube-proxy启用了IPVS模式，并且宿主机在0.0.0.0:NonServicePort上监听，那么可以在宿主机上、或者Pod内，通过任意ClusterIP:NonServicePort访问到宿主机的NonServicePort。</p>
<p>这一行为显然不符合预期，我们期望仅仅在Service对象中声明的端口，才可能通过Cluster连通。如果ClusterIP上的未知端口，内核应该丢弃报文或者返回适当的ICMP。</p>
<p>如果kube-proxy使用iptables模式，不会出现这种异常行为。</p>
<div class="blog_h1"><span class="graybg">原因</span></div>
<p>启用IPVS的情况下，所有ClusterIP都会绑定在kube-ipvs0这个虚拟的网络接口上。例如对于kube-dns服务的ClusterIP 10.96.0.10（ServicePort为TCP 53 / TCP 9153）：</p>
<pre class="crayon-plain-tag">5: kube-ipvs0: &lt;BROADCAST,NOARP&gt; 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</pre>
<p>这种绑定是必须的，因为IPVS的工作原理是，<span style="background-color: #c0c0c0;">在netfilter挂载点LOCAL_IN上注册钩子ip_vs_in，拦截目的地是VIP（ClusterIP）的封包</span>。而要使得封包进入到LOCAL_IN，它的目的地址必须是本机地址。</p>
<p>每当为网络接口添加一个IP地址，内核都会<span style="background-color: #c0c0c0;">自动</span>在local路由表中增加一条规则，对于上面的10.96.0.10，会增加：</p>
<pre class="crayon-plain-tag"># 对于目的地址是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</pre>
<p>上述自动添加路由的一个副作用是，<span style="background-color: #c0c0c0;">对于任意一个端口Port，如果不存在匹配ClusterIP:Port的IPVS规则，同时宿主机上某个应用在0.0.0.0:Port上监听，封包就会交由此应用处理</span>。</p>
<p>在宿主机上执行<pre class="crayon-plain-tag">telnet 10.96.0.10 22</pre>，会发生以下事件序列：</p>
<ol>
<li>出站选路，根据local表路由规则，从kube-ipvs0接口发出封包</li>
<li>由于kube-ipvs0是dummy的，封包<span style="background-color: #c0c0c0;">立刻从kube-ipvs0的出站队列移动到入站队列</span></li>
<li>目的地址是本地地址，因此进入LOCAL_IN挂载点</li>
<li>由于22不是ServicePort，封包被转发给本地进程处理，即监听了22的那个进程</li>
</ol>
<p>如果删除内核自动在local表中添加的路由：</p>
<pre class="crayon-plain-tag">ip route del table local local 10.96.0.10 dev kube-ipvs0 proto kernel scope host src 10.96.0.10</pre>
<p>则会出现以下现象：</p>
<ol>
<li>无法访问10.96.0.10:22。这是我们期望的，因为10.96.0.10这个服务没有暴露22端口，此端口理当不通</li>
<li>无法ping 10.96.0.10。这不是我们期望的，但是一般情况下不会有什么问题。iptables模式下ClusterIP就是无法ping的，IPVS模式下可以在本机ping仅仅是绑定ClusterIP到kube-ipvs0的一个副作用。通常应用程序不应该对ClusterIP做ICMP检测，来判断服务是否可用，因为这依赖了kube-proxy的特定工作模式</li>
<li>在宿主机上，可以访问10.96.0.10:53。这是我们期望的，宿主机上可以访问ClusterIP</li>
<li>在某个容器的网络命名空间下，无法访问10.96.0.10:53。这不是我们期望的，相当于Pod无法访问ClusterIP了</li>
</ol>
<p>以上4条，惟独3难以理解。<span style="background-color: #c0c0c0;">为什么路由没了，宿主机仍然能访问ClusterIP:ServicePort</span>？这个我们还没有从源码级别深究，但是很明显和IPVS有关。IPVS在LOCAL_OUT上挂有钩子，它可能在此钩子中检测到来自本机（主网络命名空间）的、访问ClusterIP+ServicePort（即IPVS虚拟服务）的封包，并进行了某种“魔法”处理，从而避开了没有路由的问题。</p>
<p>下面我们进一步验证上述“魔法”处理的可能性。使用<pre class="crayon-plain-tag">tcpdump -i any host 10.96.0.10</pre>来捕获流量，从容器命名空间访问ClusterIP:ServicePort时，可以看到：</p>
<pre class="crayon-plain-tag">#                  容器IP
11:32:00.448470 IP 172.27.0.24.56378 &gt; 10.96.0.10.53: Flags [S], seq 2946888109, win 28200, options...</pre>
<p>但是从宿主机访问ClusterIP:ServicePort时，则捕获不到任何流量。但是，通过iptables logging，我们可以确定，内核的确<span style="background-color: #c0c0c0;">以ClusterIP为源地址和目的地址</span>，发起了封包：</p>
<pre class="crayon-plain-tag">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 </pre>
<p>回顾一下数据报出站、入站的处理过程：</p>
<ol>
<li>出站，依次经过 <strong><span style="background-color: #99cc00;">netfilter/iptables</span></strong> ⇨ <strong><span style="background-color: #cc99ff;">tcpdump</span></strong> ⇨ 网络接口 ⇨网线</li>
<li>入站，依次经过 网线 ⇨ 网络接口 ⇨ tcpdump ⇨ netfilter/iptables</li>
</ol>
<p>只有当IPVS在宿主机请求10.96.0.10的封包出站时，在netfilter中对匹配IPVS虚拟服务的封包进行如下处理，才能解释<span style="background-color: #99cc00;"><strong>iptables</strong></span>中能看到10.96.0.10，而紧随其后的<strong><span style="background-color: #cc99ff;">tcpdump</span></strong>中却又看不到的现象：</p>
<ol>
<li>修改目的地址为Service的Endpoint地址，这就是NAT模式的IPVS（即kube-proxy使用NAT模式）应有的行为</li>
<li>修改了源地址为当前宿主机的地址，不这样做，回程报文就无法路由回来</li>
</ol>
<p>另外注意一下，如果从宿主机访问ClusterIP:NonServicePort，则tcpdump能捕获到源或目的地址为ClusterIP的流量。这是因为IPVS发现它不匹配任何虚拟服务，会直接返回NF_ACCEPT，然后封包就按照常规流程处理了。</p>
<div class="blog_h1"><span class="graybg">后果</span></div>
<div class="blog_h2"><span class="graybg">安全问题</span></div>
<p>如果宿主机上有一个在0.0.0.0上监听的、存在安全漏洞的服务，则可能被恶意的工作负载利用。</p>
<div class="blog_h2"><span class="graybg">行为异常</span></div>
<p>少部分的应用程序，例如docker manifest，其行为取决于端口探测的结果，会无法正常工作。</p>
<div class="blog_h1"><span class="graybg">解决</span></div>
<p>可能的解决方案有：</p>
<ol>
<li>在iptables中匹配哪些针对ClusterIP:NonServicePort的流量，Drop或Reject掉</li>
<li>使用基于fwmark的IPVS虚拟服务，这需要在iptables中对针对ClusterIP:ServicePort的流量打fwmark，而且每个ClusterIP都需要占用独立的fwmark，难以管理</li>
</ol>
<p>对于解决方案1，可以使用如下iptables规则： </p>
<pre class="crayon-plain-tag">#                 如果目的地址是ClusterIP    但是目的端口不是ServicePort           则拒绝
iptables -A INPUT -d  10.96.0.0/12 -m set ! --match-set KUBE-CLUSTER-IP dst,dst -j REJECT</pre>
<p>这个规则能够为容器解决宿主机端口泄露的问题，但是会导致宿主机上无法访问ClusterIP。</p>
<p>引起此问题的原因是，在宿主机访问ClusterIP时，会同时使用ClusterIP作为源地址/目的地址。这样，来自Endpoint的回程报文，unNATed后的目的地址，就会匹配到上面的iptables规则，从而导致封包被Reject掉。</p>
<p>要解决此问题，我们可以修改内核自动添加的路由，提示使用其它地址作为源地址：</p>
<pre class="crayon-plain-tag"># 这条路由给出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</pre>
<div class="blog_h1"><span class="graybg">深入</span></div>
<p>上文我们提到了一个“魔法”处理的猜想，这里我们对IPVS的实现细节进行深入学习，证实此猜想。</p>
<p>本节牵涉到的内核源码均来自linux-3.10.y分支。</p>
<div class="blog_h2"><span class="graybg">Netfilter</span></div>
<p>这是从2.4.x引入内核的一个框架，用于实现防火墙、NAT、封包修改、记录封包日志、用户空间封包排队之类的功能。</p>
<p>netfilter运行在内核中，允许内核模块在Linux网络栈的<span style="background-color: #c0c0c0;">不同位置注册钩子（回调函数），当每个封包穿过网络栈时，这些钩子函数会被调用</span>。</p>
<p><a href="/iptables">iptables</a>是经典的，基于netfilter的用户空间工具。它的继任者是nftables，它更加灵活、可扩容、性能好。</p>
<div class="blog_h3"><span class="graybg">钩子挂载点</span></div>
<p>netfilter提供了5套钩子（的挂载点）：</p>
<table class="full-width fixed-word-wrap">
<thead>
<tr>
<td style="width: 180px; text-align: center;">挂载点</td>
<td style="text-align: center;">说明</td>
</tr>
</thead>
<tbody>
<tr>
<td>NF_IP_PER_ROUTING</td>
<td>
<p>当封包进入网络栈时调用。封包的目的地可能是本机，或者需要转发</p>
<p>ip_rcv / ipv6_rcv是内核接受并处理IP数据报的入口，此函数会调用这类钩子：</p>
<pre class="crayon-plain-tag">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);
}</pre>
</td>
</tr>
<tr>
<td>NF_IP_LOCAL_IN</td>
<td>
<p>当路由判断封包应该由本机处理时（目的地址是本机地址）调用
<p>ip_local_deliver / ip6_input负责将IP数据报向上层传递，此函数会调用这类钩子</p>
</td>
</tr>
<tr>
<td>NF_IP_FORWARD</td>
<td>
<p>当路由判断封包应该被转发给其它机器（或者网络命名空间）时调用</p>
<p>ip_forward / ip6_forward负责封包转发，此函数会调用这类钩子</p>
</td>
</tr>
<tr>
<td>NF_IP_POST_ROUTING</td>
<td>
<p>在封包即将离开网络栈（进入网线）时调用，不管是转发的、还是本机发出的，都需要经过此挂载点</p>
<p>ip_output / ip6_finish_output2会调用这类钩子</p>
</td>
</tr>
<tr>
<td>NF_IP_LOCAL_OUT</td>
<td>
<p>当封包由本机产生，需要往外发送时调用</p>
<p>__ip_local_out / __ip6_local_out会调用这类钩子</p>
</td>
</tr>
</tbody>
</table>
<p>这些挂载点，和iptables的各链是对应的。</p>
<div class="blog_h3"><span class="graybg">注册钩子</span></div>
<p>要在内核中使用netfilter的钩子，你需要调用函数：</p>
<pre class="crayon-plain-tag">// 注册钩子
int nf_register_hook(struct nf_hook_ops *reg){}
// 反注册钩子
void nf_unregister_hook(struct nf_hook_ops *reg){}</pre>
<p>入参nf_hook_ops是一个结构：</p>
<pre class="crayon-plain-tag">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 *)); // 如果通过钩子检查，则调用此函数，通常用不到</pre>
<div class="blog_h3"><span class="graybg">钩子返回值</span></div>
<pre class="crayon-plain-tag">/* 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 </pre>
<div class="blog_h3"><span class="graybg">钩子优先级</span></div>
<p>优先级通常以下面的枚举为基准+/-：</p>
<pre class="crayon-plain-tag">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,
};</pre>
<div class="blog_h2"><span class="graybg"><a id="ipvs"></a>IPVS</span></div>
<div class="blog_h3"><span class="graybg">钩子列表</span></div>
<p> ip_vs模块初始化时，会通过ip_vs_init函数，调用nf_register_hook，注册以下netfilter钩子：</p>
<pre class="crayon-plain-tag">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,
	}
};</pre>
<div class="blog_h3"><span class="graybg">ip_vs_in</span></div>
<p>从上面的钩子我们可以看到：</p>
<ol>
<li>针对外部发起的、本机发起的，对IPVS的请求（目的是VIP的SYN），钩子的位置是不一样的：
<ol>
<li>对于外部的请求，在LOCAL_IN中处理，钩子函数为ip_vs_remote_request4</li>
<li>对于本机的请求，在LOCAL_OUT中处理，钩子函数为ip_vs_local_request4</li>
</ol>
</li>
<li> 尽管钩子的位置不同，但是函数ip_vs_remote_request4、ip_vs_local_request4都是调用ip_vs_in。实际上，这两个函数的逻辑完全一样：<br />
<pre class="crayon-plain-tag">/*
 *	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);
}</pre>
</li>
</ol>
<p>回顾一下上文我们关于“魔法”处理的疑惑。对于从宿主机发起对10.96.0.10:53的请求，我们通过iptables logging证实了使用的源IP地址是10.96.0.10：</p>
<ol>
<li>这个请求为什么tcpdump捕获不到？</li>
<li>为什么删除路由不影响宿主机对ClusterIP的请求（却又导致容器无法请求ClusterIP）？</li>
</ol>
<p>这两个问题的答案，很可能就隐藏在ip_vs_in函数中，因为它是处理进入IPVS的数据报的统一入口。如果该函数同时修改了原始封包的源/目的地址，就解释了问题1；如果该函数在内部进行了选路操作，则解释了问题2。</p>
<p>下面分析一下ip_vs_in的代码：</p>
<pre class="crayon-plain-tag">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-&gt;ipvs_property)
		return NF_ACCEPT;


	// 如果封包目的地不是本机且当前不在LOCAL_OUT
	// 或者封包的dst_entry不存在，不做处理，继续netfilter常规流程
	if (unlikely((skb-&gt;pkt_type != PACKET_HOST &amp;&amp;
		      hooknum != NF_INET_LOCAL_OUT) ||
		     !skb_dst(skb))) {
		ip_vs_fill_iph_skb(af, skb, &amp;iph);
		IP_VS_DBG_BUF(12, "packet type=%d proto=%d daddr=%s"
			      " ignored in hook %u\n",
			      skb-&gt;pkt_type, iph.protocol,
			      IP_VS_DBG_ADDR(af, &amp;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-&gt;enable))
		return NF_ACCEPT;

	// 使用封包的IP头填充IPVS的IP头
	ip_vs_fill_iph_skb(af, skb, &amp;iph);

	// 如果是RAW套接字，不做处理，继续netfilter常规流程
	if (unlikely(skb-&gt;sk != NULL &amp;&amp; hooknum == NF_INET_LOCAL_OUT &amp;&amp;
		     af == AF_INET)) {
		struct sock *sk = skb-&gt;sk;
		struct inet_sock *inet = inet_sk(skb-&gt;sk);

		if (inet &amp;&amp; sk-&gt;sk_family == PF_INET &amp;&amp; inet-&gt;nodefrag)
			return NF_ACCEPT;
	}

	// 处理ICMP报文，和我们的场景无关
	if (unlikely(iph.protocol == IPPROTO_ICMP)) {
		int related;
		int verdict = ip_vs_in_icmp(skb, &amp;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-&gt;pp;
	// 尝试获取封包所属的IPVS连接对象
	cp = pp-&gt;conn_in_get(af, skb, &amp;iph, 0);
	// 如果封包属于既有IPVS连接，且此连接的RS（dest）已经设置，且RS的权重为0
	// 认为是无效连接，设为过期
	if (unlikely(sysctl_expire_nodest_conn(ipvs)) &amp;&amp; cp &amp;&amp; cp-&gt;dest &amp;&amp;
	    unlikely(!atomic_read(&amp;cp-&gt;dest-&gt;weight)) &amp;&amp; !iph.fragoffs &amp;&amp;
	    is_new_conn(skb, &amp;iph)) {
		ip_vs_conn_expire_now(cp);
		__ip_vs_conn_put(cp);
		cp = NULL;
	}

	// 调度一个新的IPVS连接，这里牵涉到RS的LB算法
	if (unlikely(!cp) &amp;&amp; !iph.fragoffs) {
		int v;
		if (!pp-&gt;conn_schedule(af, skb, pd, &amp;v, &amp;cp, &amp;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-&gt;dest &amp;&amp; !(cp-&gt;dest-&gt;flags &amp; 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-&gt;packet_xmit)
		// 调用packet_xmit将封包发走，实际上是重入netfilter的LOCAL_OUT，封包控制权转移走，后续不该再操控skb
		ret = cp-&gt;packet_xmit(skb, cp, pp, &amp;iph);
	else {
		IP_VS_DBG_RL("warning: packet_xmit is null");
		ret = NF_ACCEPT;
	}

	if (cp-&gt;flags &amp; IP_VS_CONN_F_ONE_PACKET)
		pkts = sysctl_sync_threshold(ipvs);
	else
		pkts = atomic_add_return(1, &amp;cp-&gt;in_pkts);

	if (ipvs-&gt;sync_state &amp; IP_VS_STATE_MASTER)
		ip_vs_sync_conn(net, cp, pkts);

	// 放回连接对象，重置连接定时器
	ip_vs_conn_put(cp);
	return ret;
}</pre>
<p>上面这段代码中，“魔法”处理最可能发生在：</p>
<ol>
<li>conn_schedule：在这里需要进行IPVS连接的调度</li>
<li>packet_xmit：在这里发送经过IPVS处理的封包</li>
</ol>
<p>二者都是函数指针，在TCP协议下，conn_schedule指向tcp_conn_schedule。在NAT模式下，packet_xmit指向ip_vs_nat_xmit。packet_xmit指针是在conn_schedule过程中初始化的。</p>
<div class="blog_h3"><span class="graybg">tcp_conn_schedule</span></div>
<p>我们看一下TCP协议下IPVS连接的调度过程。</p>
<pre class="crayon-plain-tag">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-&gt;len, sizeof(_tcph), &amp;_tcph);
	if (th == NULL) {
		*verdict = NF_DROP;
		return 0;
	}
	net = skb_net(skb);
	rcu_read_lock();
	if (th-&gt;syn &amp;&amp;
	    // 根据封包特征，去查找匹配的虚拟服务
	    (svc = ip_vs_service_find(net, af, skb-&gt;mark, iph-&gt;protocol,
				      &amp;iph-&gt;daddr, th-&gt;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, &amp;ignored, iph);
		if (!*cpp &amp;&amp; ignored &lt;= 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;
}</pre>
<p>到这里我们还没有看到IPVS对封包地址进行更改，需要进一步阅读ip_vs_schedule。 </p>
<div class="blog_h3"><span class="graybg">ip_vs_schedule</span></div>
<p>这是IPVS调度的核心函数，它支持TCP/UDP，它为虚拟服务选择一个RS，创建IPVS连接对象。</p>
<pre class="crayon-plain-tag">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-&gt;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-&gt;scheduler);
	// 调度器就是选择一个RS（ip_vs_dest）
	dest = sched-&gt;schedule(svc, skb);
	if (dest == NULL) {
		IP_VS_DBG(1, "Schedule: no dest found.\n");
		return NULL;
	}

	flags = (svc-&gt;flags &amp; IP_VS_SVC_F_ONEPACKET
		 &amp;&amp; iph-&gt;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-&gt;net, svc-&gt;af, iph-&gt;protocol,
				      &amp;iph-&gt;saddr, pptr[0], &amp;iph-&gt;daddr,
				      pptr[1], &amp;p);
		// 操控ip_vs_conn的逻辑包括：
		//   初始化定时器
		//   设置网络命名空间
		//   设置地址、fwmark、端口
		//   根据IP版本、IPVS模式（NAT/DR/TUN）为连接设置一个packet_xmit
		cp = ip_vs_conn_new(&amp;p, &amp;dest-&gt;addr,
				    dest-&gt;port ? dest-&gt;port : pptr[1],
				    flags, dest, skb-&gt;mark);
		if (!cp) {
			*ignored = -1;
			return NULL;
		}
	}

	// ...
	return cp;
}</pre>
<p>到这里我们可以看到， conn_schedule仍然没有对封包做任何修改。看来关键在packet_xmit函数中。</p>
<div class="blog_h3"><span class="graybg">ip_vs_nat_xmit</span></div>
<pre class="crayon-plain-tag">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-&gt;flags &amp; IP_VS_CONN_F_NO_CPORT)) {
		__be16 _pt, *p;

		p = skb_header_pointer(skb, ipvsh-&gt;len, sizeof(_pt), &amp;_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-&gt;dest, cp-&gt;daddr.ip,
				   IP_VS_RT_MODE_LOCAL |
				   IP_VS_RT_MODE_NON_LOCAL |
				   IP_VS_RT_MODE_RDR, NULL);
	if (local &lt; 0)
		goto tx_error;
	rt = skb_rtable(skb);

	// 如果目的地是本机，RS地址是环回地址，是输入
	if (local &amp;&amp; ipv4_is_loopback(cp-&gt;daddr.ip) &amp;&amp; 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-&gt;dst.dev-&gt;hard_header_len))
		goto tx_error;

	// 修改封包，dnat_handler指向tcp_dnat_handler
	if (pp-&gt;dnat_handler &amp;&amp; !pp-&gt;dnat_handler(skb, pp, cp, ipvsh))
		goto tx_error;
	// 更改目的地址
	ip_hdr(skb)-&gt;daddr = cp-&gt;daddr.ip;
	// 为出站封包生成chksum
	ip_send_check(ip_hdr(skb));

	IP_VS_DBG_PKT(10, AF_INET, pp, skb, 0, "After DNAT");

	skb-&gt;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-&gt;ipvs_property = 1;
	if (likely(!(cp-&gt;flags &amp; 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)-&gt;dev, dst_output);
	} else
		ret = NF_ACCEPT;
	return ret;
}</pre>
<p>在ip_vs_nat_xmit中，我们可以了解到，对于宿主机发起的针对ClusterIP:ServicePort的请求</p>
<ol>
<li>封包的目的地址被修改为Endpoint（通常是Pod，IPVS中的RS）的地址</li>
<li>修改后的封包，重新被塞入netfilter（内层），注意当前就正在netfilter（外层）中
<ol>
<li>外层钩子的返回值是NF_STOLEN：封包处理权转移给内层钩子，停止后续netfilter流程</li>
<li>内层钩子的返回值是NF_ACCEPT：不做IPVS相关处理，继续后续netfilter流程。IPVS前、后的LOCAL_OUT、POSTROUTING钩子都会正常执行。也就是说，对于修改后的封包，内核会进行完整、常规的netfilter处理，就像没有IPVS存在一样</li>
</ol>
</li>
</ol>
<p>到这里，我们确定了，IPVS会在LOCAL_OUT中进行DNAT。但是只有同时进行SNAT，才能解释上文的中的疑惑。</p>
<div class="blog_h2"><span class="graybg">SNAT</span></div>
<p>花费了不少时间在IPVS上探究后，我们意识到走错了方向。我们忘记了SANT是kube-proxy会去做的事情。查看一下iptables规则就一目了然了：</p>
<pre class="crayon-plain-tag"># 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 */</pre>
<p>由于ip_vs_local_request4挂钩在LOCAL_OUT，优先级为NF_IP_PRI_NAT_DST+2 ，因此它是发生在上面nat表OUTPUT链中MARK之后的。也就是说在IPVS处理之前，kube-proxy已经给原始的封包打上标记。</p>
<p>重入的、DNAT后的封包进入LOCAL_OUT，随后进入POSTROUTING。由于标记的缘故，封包被kube-proxy的规则SNAT。</p>
<p>经过POSTROUTING的封包，经过tcpdump，但是由于源、目的IP地址，以及目的端口都改变了，因而我们看到tcpdump没有任何输出。</p>
<div class="blog_h1"><span class="graybg">总结</span></div>
<p>这里做一下小结。</p>
<p>为什么IPVS模式下，能够ping通ClusterIP？ 这是因为IPVS模式下，ClusterIP被配置为宿主机上一张虚拟网卡kube-ipvs0的IP地址。</p>
<p>为什么IPVS模式下，宿主机端口被ClusterIP泄漏？每当添加一个ClusterIP给网络接口后，内核自动在local表中增加一条路由，此路由保证了针对ClusterIP的访问，在没有IPVS干涉的情况下，路由到本机处理。这样，在0.0.0.0上监听的进程，就接收到报文并处理。</p>
<p>为什么删除内核添加的路由后：</p>
<ol>
<li>宿主机上访问ClusterIP:NonServicePort不通了？因为没有路由了</li>
<li>没有路由了，为什么宿主机上访问ClusterIP:ServicePort仍然畅通？如上文分析，IPVS在ip_vs_nat_xmit中仍然会进行选路操作</li>
<li>那为什么从容器网络命名空间访问ClusterIP:ServicePort不通呢？IPVS处理本地、远程客户端的代码路径不一样。容器网络命名空间是远程客户端，需要首先进入PER_ROUTING，然后选路，路由目的地是本机，才会进入LOCAL_IN，IPVS才有介入的时机。由于路由被删掉了，选路那一步就会出问题</li>
</ol>
<p>为什么通过--match-set KUBE-CLUSTER-IP匹配目的地址，如果封包目的端口是NonServicePort则Reject：</p>
<ol>
<li>这种方案对容器命名空间有效？容器请求的源地址不会是ClusterIP，因此回程报文的目的地址不会因为匹配规则而Reject</li>
<li>这种方案导致宿主机无法访问ClusterIP？宿主机发起请求时用的是ClusterIP，请求端口是随机的。这种请求的回程报文必然匹配规则导致Reject</li>
</ol>
</div><p>The post <a rel="nofollow" href="https://blog.gmem.cc/nodeport-leak-under-ipvs-mode">IPVS模式下ClusterIP泄露宿主机端口的问题</a> appeared first on <a rel="nofollow" href="https://blog.gmem.cc">绿色记忆</a>.</p>
]]></content:encoded>
			<wfw:commentRss>https://blog.gmem.cc/nodeport-leak-under-ipvs-mode/feed</wfw:commentRss>
		<slash:comments>0</slash:comments>
		</item>
		<item>
		<title>IPVS和Keepalived</title>
		<link>https://blog.gmem.cc/ipvs-and-keepalived</link>
		<comments>https://blog.gmem.cc/ipvs-and-keepalived#comments</comments>
		<pubDate>Wed, 28 Sep 2016 02:23:06 +0000</pubDate>
		<dc:creator><![CDATA[Alex]]></dc:creator>
				<category><![CDATA[Linux]]></category>
		<category><![CDATA[IPVS]]></category>
		<category><![CDATA[LB]]></category>

		<guid isPermaLink="false">https://blog.gmem.cc/?p=22825</guid>
		<description><![CDATA[<p>IPVS 关于IPVS，可以参考这个网站：http://www.austintek.com/LVS/LVS-HOWTO/ 关于IPVS在内核中的实现，可以参考：IPVS模式下ClusterIP泄露宿主机端口的问题 简介 IPVS在内核中实现了传输层负载均衡，是一个L4的交换机。IPVS在一群真实服务器的前面，运行一个LB角色的主机，该主机面向客户端，提供了单一IP地址的虚拟服务。 和netfilter的交互 在Director上，LVS钩子在netfilter框架中的位置，不管是来程、回程报文，均从左侧进入、右侧出去。对于DR/TUN模式，回程报文不经过Director：  &#160; 当Director接收到封包时，如果它的目的地址是VIP，则PREROUTING后路由决策，会导致封包被发送到LOCAL_IN。这是因为VIP是Local地址，ip_vs_in挂钩到LOCAL_IN，是VIP必须在Director上的原因，因为只有目标是本地地址，才能走到LOCAL_IN LVS在LOCAL_IN注册钩子ip_vs_in，需要注意的是，钩子具有优先级。最低优先级的钩子，最先看见封包。LVS的钩子的优先级，比iptables规则高，因此iptables规则首先应用到封包，然后是LVS 上一步中，如果ip_vs_in得到封包，它会根据负载均衡算法，选择一个RS，然后如变魔法一般，将封包直接瞬移到POSTROUTING链 LVS不会在FORWARD中寻找入站封包，仅当NAT模式下，收到来自RS的回程报文时，LVS才和FORWARD链相关（NAT模式下回程报文的目的IP是客户端IP地址，是进不了LOCAL_IN的），封包在此unNATed（源地址从RS改为Director）。同样需要注意，LVS在任何iptables规则之后看到封包 负载均衡模式 只有fullNAT或者NAT才支持端口映射，也就是说VIP和RIP使用不同的端口。 fullNAT 不是标准内核的一部分。 来程报文： ClientIP: ClientPort - VIP:VPORT ⇨ IPVS Director ⇨ <a class="read-more" href="https://blog.gmem.cc/ipvs-and-keepalived">[...]</a></p>
<p>The post <a rel="nofollow" href="https://blog.gmem.cc/ipvs-and-keepalived">IPVS和Keepalived</a> appeared first on <a rel="nofollow" href="https://blog.gmem.cc">绿色记忆</a>.</p>
]]></description>
				<content:encoded><![CDATA[<div class="wri_content_clear_both"><div class="blog_h1"><span class="graybg">IPVS</span></div>
<p>关于IPVS，可以参考这个网站：<a href="http://www.austintek.com/LVS/LVS-HOWTO/">http://www.austintek.com/LVS/LVS-HOWTO/</a></p>
<p>关于IPVS在内核中的实现，可以参考：<a href="/nodeport-leak-under-ipvs-mode#ipvs">IPVS模式下ClusterIP泄露宿主机端口的问题</a></p>
<div class="blog_h2"><span class="graybg">简介</span></div>
<p>IPVS在内核中实现了传输层负载均衡，是一个L4的交换机。IPVS在一群真实服务器的前面，运行一个LB角色的主机，该主机面向客户端，提供了单一IP地址的虚拟服务。</p>
<div class="blog_h2"><span class="graybg">和netfilter的交互</span></div>
<p>在Director上，LVS钩子在netfilter框架中的位置，不管是来程、回程报文，均从左侧进入、右侧出去。对于DR/TUN模式，回程报文不经过Director： <a href="https://cdn.gmem.cc/wp-content/uploads/2020/07/ipvs_netfilter.jpg"><img class="wp-image-33885 aligncenter" src="https://cdn.gmem.cc/wp-content/uploads/2020/07/ipvs_netfilter.jpg" alt="ipvs_netfilter" width="872" height="421" /></a></p>
<p>&nbsp;</p>
<ol>
<li>当Director接收到封包时，如果它的目的地址是VIP，则PREROUTING后路由决策，会导致封包被发送到LOCAL_IN。这是因为VIP是Local地址，ip_vs_in挂钩到LOCAL_IN，是VIP必须在Director上的原因，因为只有目标是本地地址，才能走到LOCAL_IN</li>
<li>LVS在LOCAL_IN注册钩子ip_vs_in，需要注意的是，钩子具有优先级。<span style="background-color: #c0c0c0;">最低优先级的钩子，最先看见封包</span>。LVS的钩子的优先级，比iptables规则高，<span style="background-color: #c0c0c0;">因此iptables规则首先应用到封包，然后是LVS</span></li>
<li>上一步中，如果ip_vs_in得到封包，它会根据负载均衡算法，选择一个RS，然后如变魔法一般，<span style="background-color: #c0c0c0;">将封包直接瞬移</span>到POSTROUTING链</li>
<li>LVS不会在FORWARD中寻找入站封包，仅当NAT模式下，收到来自RS的回程报文时，LVS才和FORWARD链相关（NAT模式下回程报文的目的IP是客户端IP地址，是进不了LOCAL_IN的），封包在此unNATed（源地址从RS改为Director）。同样需要注意，LVS在任何iptables规则之后看到封包</li>
</ol>
<div class="blog_h2"><span class="graybg"><a id="ipvs-mode"></a>负载均衡模式</span></div>
<p>只有fullNAT或者NAT才支持端口映射，也就是说VIP和RIP使用不同的端口。</p>
<div class="blog_h3"><span class="graybg">fullNAT</span></div>
<p>不是标准内核的一部分。</p>
<p>来程报文： ClientIP: ClientPort - VIP:VPORT ⇨<em> IPVS Director</em> ⇨ <strong><span style="background-color: #339966;">DirectorIP</span></strong>: <strong><span style="background-color: #339966;">DirectorPort</span></strong> - <span style="background-color: #ff0000;"><strong>RIP</strong></span>：<span style="background-color: #ff0000;"><strong>RPORT</strong></span></p>
<p>回程报文：RIP: RPORT - DirectorIP: DirectorPort ⇨ IPVS Director ⇨ <span style="background-color: #ff0000;"><strong>VIP</strong></span>:<span style="background-color: #ff0000;"><strong>VPORT</strong></span> - <strong><span style="background-color: #339966;">ClientIP</span></strong>: <strong><span style="background-color: #339966;">ClientPort</span></strong></p>
<p>和NAT模式相比，IPVS Director在进行DNAT的同时，还进行SNAT。这样获得以下优势：</p>
<ol>
<li>客户端可以和Director、真实服务器在同一个局域网内</li>
</ol>
<p>缺点是：</p>
<ol>
<li>真实服务器<span style="background-color: #c0c0c0;">看不到客户端的真实IP</span>地址。只能由Director通过TCP Options携带真实IP，但是TCP Option只有40字节，可能客户端已经占用导致空间不足</li>
</ol>
<div class="blog_h3"><span class="graybg">NAT</span></div>
<p>来程报文： ClientIP: ClientPort - VIP:VPORT ⇨<em> IPVS Director</em> ⇨ ClientIP: ClientPort - <strong><span style="background-color: #ff0000;">RIP</span></strong>：<strong><span style="background-color: #ff0000;">RPORT</span></strong></p>
<p>回程报文：RIP: RPORT - ClientIP: ClientPort ⇨ IPVS Director ⇨ <strong><span style="background-color: #ff0000;">VIP</span></strong>:<strong><span style="background-color: #ff0000;">VPORT</span></strong> - ClientIP: ClientPort</p>
<p>基于NAT的虚拟服务器，当LB具有两个网络接口时：</p>
<ol>
<li>一个接口分配面向外部的IP地址</li>
<li>另一个接口分配私有的、面向内部的IP地址</li>
</ol>
<p>LB从外部接口接受流量，然后经过NAT，转发给内部网络中的真实服务器。</p>
<p>该模式的缺陷：</p>
<ol>
<li>LB是性能瓶颈，它需要转发两个方向的流量</li>
<li><span style="background-color: #c0c0c0;">RS上必须进行适当的路由</span>，确保针对ClientIP的路由转发给IPVS Director处理</li>
<li><span style="background-color: #c0c0c0;">客户端必须和RS不在同一网段</span>。这个限制可以通过配置解决：
<ol>
<li>在Director上关闭ICMP重定向：<br />
<pre class="crayon-plain-tag">echo 0 &gt; /proc/sys/net/ipv4/conf/all/send_redirects
echo 0 &gt; /proc/sys/net/ipv4/conf/default/send_redirects
echo 0 &gt; /proc/sys/net/ipv4/conf/eth0/send_redirects</pre>
</li>
<li>在RS上，把Director配置为出口包的唯一网关</li>
</ol>
</li>
</ol>
<div class="blog_h3"><span class="graybg">Tunneling</span></div>
<p>在此模式下，LB通过IP隧道将请求发送给真实服务器。</p>
<p>好处是可扩容，LB将请求转发给真实服务器后，<span style="background-color: #c0c0c0;">真实服务器直接将响应发送给客户端，不再需要经过LB</span>。由于大部分情况下都是请求包小、应答包大，这种模式下LB的性能压力较小。</p>
<p>该模式的缺陷：对网络结构依赖很大，<span style="background-color: #c0c0c0;">真实服务器必须支持IP隧道协议</span>。</p>
<div class="blog_h3"><span class="graybg">Direct Routing</span></div>
<p>用户请求LB上的VIP，LB将请求直接转发（通过修改L2封包的方式，L3保持不变）给真实服务器。</p>
<p>好处是可扩容，而且没有Tunneling模式的封包解包开销。</p>
<p>真实服务器要直接应答客户端，则应答包的源IP必须是VIP。这就导致LB和所有真实服务器都需要共享VIP+MAC的组合。不经过合理配置，则真实服务器可能直接接收到请求，绕过LB。</p>
<p>该模式的缺陷：</p>
<ol>
<li>RS和LVS的VIP，必须使用<span style="background-color: #c0c0c0;">相同端口</span></li>
<li><span style="background-color: #c0c0c0;">RS和LVS不能在同一机器上</span>，否则该RS访问VIP不经过LVS的负载均衡，而是直接访问自己</li>
<li>RS和LVS必须位于<span style="background-color: #c0c0c0;">同一VLAN</span>或局域网</li>
</ol>
<div class="blog_h2"><span class="graybg">负载均衡算法</span></div>
<div class="blog_h3"><span class="graybg">rr</span></div>
<p>简单的轮询算法，均等地对待每一台服务器,而不管服务器上实际的连接数和系统负载。</p>
<div class="blog_h3"><span class="graybg">wrr</span></div>
<p>带权重的轮询算法，可以保证处理能力强的服务器处理更多的访问流量?调度器可以自动问询真实服务器的负载情况,并动态地调整其权值。</p>
<div class="blog_h3"><span class="graybg">lc</span></div>
<p>IPVS表存储了所有活动的连接，基于此信息可以将请求发送给具有<span style="background-color: #c0c0c0;">最少已建立连接</span>的RS。如果集群系统的<span style="background-color: #c0c0c0;">真实服务器具有相近的系统性能，采用"最小连接"调度算法可以较好地均衡负载</span>。</p>
<div class="blog_h3"><span class="graybg">wlc</span></div>
<p>考虑权重的最少连接算法，具有较高权值的服务器将承受较大比例的活动连接负载。</p>
<div class="blog_h3"><span class="graybg">lblc</span></div>
<p>通常用于缓存集群，将来自同一个目的地址的请求分配给同一台RS，如果RS满载则将这个请求分配给连接数最小的RS。</p>
<div class="blog_h3"><span class="graybg">lblcr</span></div>
<p>根据请求的目标IP地址找出该目标IP地址对应的服务器组，按“最小连接”原则从该服务器组中选出一台服务器，若</p>
<ol>
<li>服务器没有<span style="background-color: #c0c0c0;">超载， 将请求发送到该服务器</span></li>
<li>若服务器超载；则按“最小连接”原则从整个集群中选出一台服务器，将该服务器加入到服务器组中，将请求发送到该服务器</li>
</ol>
<p>当该服务器组有一段时间没有增减成员，将<span style="background-color: #c0c0c0;">最忙的服务器从服务器组中删除</span></p>
<div class="blog_h3"><span class="graybg">sh</span></div>
<p>源地址哈希调度以源地址为关键字查找一个静态hash表来获得需要的RS</p>
<div class="blog_h3"><span class="graybg">dh</span></div>
<p>目的地址哈希调度以目的地址为关键字查找一个静态hash表来获得需要的RS </p>
<div class="blog_h3"><span class="graybg">nq</span></div>
<p>如果存在空闲服务器，则请求转发给它；否则，按seq算法</p>
<div class="blog_h3"><span class="graybg">seq</span></div>
<p>最短期望延迟（Shortest Expected Delay）。期望延迟公式： (Ci + 1) / Ui ，其中Ci为服务器i的当前连接数，Ui为服务器i的固定服务速率（权重）</p>
<div class="blog_h2"><span class="graybg">ipvsadm命令</span></div>
<p>管理Linux虚拟服务器的底层命令行工具，可以创建、修改、查看位于Linux内核中的虚拟服务器表</p>
<div class="blog_h3"><span class="graybg">格式</span></div>
<pre class="crayon-plain-tag"># 添加或修改虚拟服务
ipvsadm -A|E virtual-service [-s scheduler] [-p [timeout]] [-M netmask] [-b sched-flags]
# virtual-service可以是：
#  -t, --tcp-service service-address
#  -u, --udp-service service-address
# 示例
ipvsadm -A -t 10.0.10.100:80 -s rr -p 600

# 删除虚拟服务
ipvsadm -D virtual-service

# 清空虚拟服务表
ipvsadm -C

# 从标准输入还原虚拟服务表
ipvsadm -R

# 将虚拟服务表导出到标准输出
ipvsadm -S [-n]

# 为虚拟服务添加、编辑一个真实服务器
# 封包转发方式：
# -g --gatewaying 直接路由
# -i, --ipip  隧道（IPIP封装）
# -m, --masquerading NAT
ipvsadm -a|e virtual-service -r server-address [-g|i|m] [-w weight] [-x upper] [-y lower]
# 示例
ipvsadm -a -t 10.0.10.100:80 -r 10.1.0.10:80 -m

# 为虚拟服务删除一个真实服务器
ipvsadm -d virtual-service -r server-address
# 示例
ipvsadm -d -t 10.0.10.100:80 -r 10.1.0.10:80

# 列出虚拟服务
ipvsadm -L|l [virtual-service] [options]

# 清零包、字节、速率计数器
ipvsadm -Z [virtual-service]

# 设置IPVS连接的超时值，三个值，分别用于TCP会话、接收到FIN后的TCP会话、UDP包
ipvsadm --set tcp tcpfin udp

# 启动连接同步守护进程，state可以为master或backup。连接同步守护进程在内核中运行
# 主守护进程周期性的组播连接的变更，备份守护进程则接收组播消息并创建对应的连接。一旦主宕机，则从具有几乎全部的连接，不会出现已创建连接损坏的情况
ipvsadm --start-daemon state [--mcast-interface interface] [--syncid syncid]

# 停止连接同步守护进程
ipvsadm --stop-daemon state</pre>
<div class="blog_h3"><span class="graybg">选项</span></div>
<table class="full-width fixed-word-wrap">
<thead>
<tr>
<td style="width: 150px; text-align: center;">选项</td>
<td style="text-align: center;">说明</td>
</tr>
</thead>
<tbody>
<tr>
<td>-s alg</td>
<td>负载均衡算法，即分别TCP连接或UDP报到RS的方法</td>
</tr>
<tr>
<td>-p [timeout]</td>
<td>指示虚拟服务是否“持久的”。如果指定该选项，则同一客户端的后续请求，被转发给同一个RS</td>
</tr>
<tr>
<td>-M netmask</td>
<td>持久虚拟服务的客户端被分组的粒度，可以将某个网络的所有客户端都转发给同一RS </td>
</tr>
<tr>
<td>-r server-address</td>
<td>指定真实服务器的地址 </td>
</tr>
<tr>
<td>-g</td>
<td>使用DR模式 </td>
</tr>
<tr>
<td>-i</td>
<td>使用隧道模式，即ipip封装</td>
</tr>
<tr>
<td>-m</td>
<td>使用NAT模式</td>
</tr>
<tr>
<td>-w</td>
<td>指定服务器的相对权重，整数</td>
</tr>
<tr>
<td>-t service-address</td>
<td>使用TCP服务</td>
</tr>
<tr>
<td>-u service-address</td>
<td>使用UDP服务</td>
</tr>
</tbody>
</table>
<div class="blog_h3"><span class="graybg">示例</span></div>
<pre class="crayon-plain-tag"># 创建TCP虚拟服务207.175.44.110:80，使用轮询调度
ipvsadm -A -t 207.175.44.110:80 -s rr
# 添加两台RS
ipvsadm -a -t 207.175.44.110:80 -r 192.168.10.1:80 -m
ipvsadm -a -t 207.175.44.110:80 -r 192.168.10.2:80 -m


# 打印连接列表，可以看到IPVS建立的连接的状态
ipvsadm -lc

# IPVS connection entries
# pro expire state       source             virtual            destination
# TCP 01:19  FIN_WAIT    zircon:33568       k8s:6443           neon:6443
# TCP 119:57 ESTABLISHED zircon:33570       k8s:6443           xenon:6443
# TCP 01:19  FIN_WAIT    zircon:33569       k8s:6443           radon:6443</pre>
<div class="blog_h1"><span class="graybg">Keepalived</span></div>
<div class="blog_h2"><span class="graybg">简介</span></div>
<p>Keepalived提供负载均衡和高可用的框架。</p>
<p>负载均衡基于IP虚拟服务器（IPVS）内核模块，支持L4负载均衡。Keepalived提供了一组健康检查器，可以动态、自适应的管理上游服务器池。</p>
<p>高可用基于虚拟路由冗余协议（VRRP）。Keepalived实现了一系列钩子，和VRRP状态机进行底层/高层交互。</p>
<p>出于健壮性的考虑，Keepalived被拆分为三个进程：</p>
<ol>
<li>一个最小化的父进程，负责监控子进程</li>
<li>两个子进程，分别负责VRRP、上游服务健康检查</li>
</ol>
<div class="blog_h2"><span class="graybg">安装</span></div>
<p>通过包管理器安装：</p>
<pre class="crayon-plain-tag"># CentOS
yum install keepalived
# Ubuntu
apt install libipset-dev
apt install keepalived</pre>
<p>完成配置工作后，执行下面的命令启动：</p>
<pre class="crayon-plain-tag">systemctl restart keepalived.service</pre>
<div class="blog_h2"><span class="graybg">keepalived命令</span></div>
<div class="blog_h3"><span class="graybg">选项</span></div>
<table class="full-width fixed-word-wrap">
<thead>
<tr>
<td style="width: 25%; text-align: center;">选项</td>
<td style="text-align: center;">说明</td>
</tr>
</thead>
<tbody>
<tr>
<td>f, –use-file=FILE</td>
<td>指定配置文件路径</td>
</tr>
<tr>
<td>-P, –vrrp</td>
<td>仅仅运行VRRP子系统，也就是仅仅提供HA</td>
</tr>
<tr>
<td>-C, –check</td>
<td>仅仅运行健康检查子系统，也就是仅仅提供LB</td>
</tr>
<tr>
<td>-l, –log-console</td>
<td>打印日志到控制台，默认打印到syslog</td>
</tr>
<tr>
<td>-D, –log-detail</td>
<td>记录详细日志</td>
</tr>
<tr>
<td>-S, –log-facility=[0-7]</td>
<td>日志级别</td>
</tr>
<tr>
<td>-V, –dont-release-vrrp</td>
<td>进程退出后，不移除VRRP虚IP和VROUTE</td>
</tr>
<tr>
<td>-I, –dont-release-ipvs</td>
<td>进程退出后，不移除IPVS拓扑 </td>
</tr>
<tr>
<td>-n, –dont-fork </td>
<td>前台运行</td>
</tr>
<tr>
<td>-d, –dump-conf</td>
<td>导出配置文件</td>
</tr>
</tbody>
</table>
<div class="blog_h2"><span class="graybg">配置语法2.0</span></div>
<p>在Ubuntu 16.04上，配置文件的位置为/etc/keepalived/keepalived.conf。</p>
<div class="blog_h3"><span class="graybg">全局定义</span></div>
<pre class="crayon-plain-tag">global_defs {
    # 通知邮件配置
    notification_email {
        email
        email
    }
    notification_email_from email
    smtp_server host
    smtp_connect_timeout num
    # LVS Director的名字
    lvs_id string
}</pre>
<div class="blog_h3"><span class="graybg">虚拟服务器定义</span></div>
<pre class="crayon-plain-tag"># 提示此虚拟服务器是一个FWMARK
virtual_server (@IP PORT)|(fwmark num) {
    # 健康检查周期，秒
    delay_loop num
    # 负载均衡算法（调度算法）
    lb_algo rr|wrr|lc|wlc|sh|dh|lblc
    # IPVS模式
    lb_kind NAT|DR|TUN
    (nat_mask @IP)
    # 持久化连接，即会话亲和，让一段时间内同一客户端的连接（短）都发送到同一个RS
    # 持久化连接的超时
    persistence_timeout num
    # 持久化连接的粒度掩码
    persistence_granularity @IP
    # 用于HTTP/SSL_GET的虚拟主机名
    virtualhost string
    # 协议类型
    protocol TCP|UDP
    # 如果所有RS宕机，将下面的服务器加入池中
    sorry_server @IP PORT
    # 定义一个真实服务器（RS）
    real_server @IP PORT {
        # 负载均衡权重
        weight num
        # 通过TCP进行健康检查
        TCP_CHECK {
            connect_port num
            connect_timeout num
        }
    }
    real_server @IP PORT {
        weight num
        # 通过脚本进行健康检查
        MISC_CHECK {
            misc_path /path_to_script/script.sh
            (or misc_path “ /path_to_script/script.sh &lt;arg_list&gt;”)
        }
    }
}
real_server @IP PORT {
    weight num
    # 通过HTTP/HTTPS进行健康检查
    HTTP_GET|SSL_GET {
        # 可以指定多个url块
        url {
            # URL路径
            path alphanum
            # 摘要信息
            digest alphanum
        }
        # 端口
        connect_port num
        # 超时
        connect_timeout num
        # 重试次数
        retry num
        # 重试延迟
        delay_before_retry num
    }
}</pre>
<div class="blog_h3"><span class="graybg">VRRP定义</span></div>
<pre class="crayon-plain-tag"># 组
vrrp_sync_group string {
    group {
        string
        string
    }
    # 可以为脚本传递参数，整体用引号包围
    notify_master /path_to_script/script_master.sh
    notify_backup /path_to_script/script_backup.sh
    notify_fault /path_to_script/script_fault.sh
}

# 默认情况下，Keepavelid只能对网络故障、Keepalived自身故障进行监控，并依此进行Master切换
# 使用vrrp_script则可以通过脚本进行自定义的（对本节点）健康检查

vrrp_script chk {
   script "/bin/bash -c 'curl -m1 -k -s https://127.0.0.1:6443/healthz -o/dev/null'"
   # 每两秒执行一次
   interval 2
   # 如果检测成功（脚本结果为0），且weight大于0，则当前实例的优先级升高
   # 如果检测失败（脚本结果为非0），且weight小于0，则当前实例的优先级降低
   weight -10
   # 连续3次为0才认为成功
   fall 3
   # 连续1次失败则认为失败
   rise 1
}
# 实例
vrrp_instance string {
    # 实例状态
    # 如果所有实例都配置为BACKUP，则初始时高优先级的成为第一个Master
    # 如果一个设置为MASTER其它设置为BACKUP，那么不设置nopreempt时每当Master恢复都会强占成为主
    state MASTER|BACKUP
    # 高优先级的实例恢复正常后，不会去强占，成为Master
    nopreempt
    # 使用的网络接口
    interface string
    # &lt;span style="color: #404040;" data-mce-style="color: #404040;"&gt;VRRP通知的IP头上的IP地址&lt;/span&gt;
    mcast_src_ip @IP
    # LVS sync_daemon在什么网络接口上运行
    lvs_sync_daemon_interface string
    # 此实例所属的虚拟路由器ID
    virtual_router_id num
    # 优先级，其运行时值可以随着vrrp_script的检测结果动态变化
    # 如果接收到Peer的vrrp广播包，发现自己的优先级最高，则自动切换为Master
    priority num
    # VRRP通知间隔
    advert_int num
    # 当MASTER状态变化时进行邮件通知
    smtp_alert
    # VRRP身份验证配置
    authentication {
        auth_type PASS|AH
        auth_pass string
    }
    # 定义虚IP
    virtual_ipaddress {
        @IP
        @IP
        @IP
    }
    # 排除虚IP
    virtual_ipaddress_excluded {
        @IP
        @IP
        @IP
    }
    # 进入MASTER状态后执行的脚本
    notify_master /path_to_script/script_master.sh
    # 进入BACKUP状态后执行的脚本
    notify_backup /path_to_script/script_backup.sh
    # 进入FAULT状态后执行的脚本
    notify_fault /path_to_script/script_fault.sh
}</pre>
<div class="blog_h2"><span class="graybg">配置语法1.2</span></div>
<div class="blog_h3"><span class="graybg">全局定义</span></div>
<pre class="crayon-plain-tag">global_defs {
    notification_email {
        admin@example1.com
    }
    smtp_server 127.0.0.1 [&lt;PORT&gt;]
    smtp_helo_name &lt;HOST_NAME&gt;
    smtp_connect_timeout 30
    router_id my_hostname    # 机器标识
    vrrp_mcast_group4 224.0.0.18  # VRRP组播地址
    vrrp_mcast_group6 ff02::12
    default_interface eth0   # 静态地址的默认网络接口
    # LVS连接同步守护进程的配置
    #               监听接口     对应VRRP实例      lvs syncd的ID  最大包长度       使用的UDP端口               组播地址
    lvs_sync_daemon &lt;INTERFACE&gt; &lt;VRRP_INSTANCE&gt; [id &lt;SYNC_ID&gt;] [maxlen &lt;LEN&gt;] [port &lt;PORT&gt;] [ttl &lt;TTL&gt;] [group &lt;IP ADDR&gt;]
    lvs_timeouts tcp 7200 tcpfin 120 udp 300  # LVS超时配置
    lvs_flush # 启动时清空所有LVS配置
    vrrp_garp_master_delay 5   # 转变为MASTER后多久发起ARP宣告
    vrrp_garp_master_repeat 5  # 发起ARP宣告的次数
    vrrp_garp_master_refresh 0 # ARP重新宣告的周期，默认0表示不重复宣告
    vrrp_garp_master_refresh_repeat 1 # 重新宣告时发送ARP的次数
    vrrp_version 2 # 使用的VRRP协议版本
}</pre>
<div class="blog_h1"><span class="graybg">DR模式示例</span></div>
<p>本示例的Keepalived版本为1.2.24。</p>
<div class="blog_h2"><span class="graybg">关于DR模式</span></div>
<p>使用DR模式时，必须保证：</p>
<ol>
<li>RS本机必须有网络接口，配置为VIP。DR模式下LB节点仅仅修改以太网帧的目标MAC地址，IP包不做任何修改，因此RS必须作为IP包的Destination</li>
<li>RS不去响应针对VIP的ARP请求</li>
<li>RS不去发送关于VIP的ARP通知</li>
</ol>
<p>要保证上面三点，需要调整RS内核的行为。具体实施方法有几种：</p>
<div class="blog_h3"><span class="graybg">添加arptables规则</span></div>
<pre class="crayon-plain-tag">arptables -A IN -d 10.0.10.1 -j DROP
arptables -A OUT -s 10.0.10.1 -j mangle --mangle-ip-s 10.0.10.1 </pre>
<div class="blog_h3"><span class="graybg">修改内核参数</span></div>
<p><span class="graybg">关于内核参数/proc/sys/net/ipv4/conf/*/arp_ignore：</span></p>
<p>DR模式下，每个真实服务器节点都要在环回网卡上绑定VIP。如果客户端对于VIP的ARP请求广播到了各个真实服务器节点，当：</p>
<ol>
<li>arp_ignore=0，则各个真实服务器节点都会响应该arp请求，此时客户端就无法正确获取LVS（LB）节点上正确的VIP所在网卡的MAC地址。客户端可能将以太网帧绕过LB直接发给RS</li>
<li>arp_ignore=1，只响应目的IP地址为接收网卡上的本地地址的arp请求</li>
</ol>
<p>关于内核参数/proc/sys/net/ipv4/conf/*/arp_announce：</p>
<p>每台服务器或者交换机中都有一张arp表，该表用于存储对端通信节点IP地址和MAC地址的对应关系。当</p>
<ol>
<li>收到一个未知IP地址的arp请求，就会在本机的arp表中新增对端的IP和MAC记录</li>
<li>当收到一个已知IP地址（arp表中已有记录的地址）的arp请求，则会根据arp请求中的源MAC刷新自己的arp表</li>
</ol>
<p>如果arp_announce参数配置为0，则网卡在发送arp请求时，可能选择的源IP地址并不是该网卡自身的IP地址，这时候收到该arp请求的其他节点或者交换机上的arp表中记录的该网卡IP和MAC的对应关系就不正确，可能会引发一些未知的网络问题，存在安全隐患。</p>
<p>DR模式下要求arp_ignore参数配置为1，arp_announce参数配置为2：</p>
<pre class="crayon-plain-tag"># 修改所有RS的内核参数
echo "1" &gt;/proc/sys/net/ipv4/conf/lo/arp_ignore
echo "2" &gt;/proc/sys/net/ipv4/conf/lo/arp_announce
echo "1" &gt;/proc/sys/net/ipv4/conf/all/arp_ignore
echo "2" &gt;/proc/sys/net/ipv4/conf/all/arp_announce

# 在环回网卡上绑定虚拟IP
ifconfig lo:k8s  10.0.10.1 netmask 255.255.255.255 broadcast 10.0.10.1
ifconfig lo:etcd 10.0.10.1 netmask 255.255.255.255 broadcast 10.0.10.1
ifconfig lo:ceph 10.0.10.1 netmask 255.255.255.255 broadcast 10.0.10.1</pre>
<div class="blog_h2"><span class="graybg">keepalived配置</span></div>
<pre class="crayon-plain-tag">global_defs {
    router_id oxygen

    # 增加LVS超时
    # 可以防止TCP连接静置一段时间后服务器返回RST
    # 可通过ipvsadm -l --timeout确保生效
    # 设置超时大于7200，因为TCP保活定时器默认2小时执行，保持行为一致
    lvs_timeouts tcp 7200 tcpfin 120 udp 300
    lvs_sync_daemon eth0 51
}

vrrp_instance apiserver {
    state MASTER
    interface eth0
    virtual_router_id 51
    priority 100
    advert_int 1
    authentication {
        auth_type PASS
        auth_pass gmem
    }
    virtual_ipaddress {
        10.0.10.1/32 brd 10.0.10.1 dev eth0 label eth0:k8s
        10.0.10.2/32 brd 10.0.10.2 dev eth0 label eth0:etcd
        10.0.10.3/32 brd 10.0.10.3 dev eth0 label eth0:ceph
    }
}

virtual_server 10.0.10.1 6443 {
    delay_loop 5
    lb_algo wlc
    lb_kind DR
    protocol TCP
    real_server 10.0.5.1 6443 {
        weight 10
        SSL_GET {
            url {
                path /healthz
                status_code 200
            }
            connect_timeout 3
            nb_get_retry 3
            delay_before_retry 3
        }

    }
    real_server 10.0.2.1 6443 {
        weight 10
        SSL_GET {
            url {
                path /healthz
                status_code 200
            }
            connect_timeout 3
            nb_get_retry 3
            delay_before_retry 3
        }

    }
    real_server 10.0.3.1 6443 {
        weight 10
        SSL_GET {
            url {
                path /healthz
                status_code 200
            }
            connect_timeout 3
            nb_get_retry 3
            delay_before_retry 3
        }

    }

}

virtual_server 10.0.10.2 2379 {
    delay_loop 5
    lb_algo wlc
    lb_kind DR
    protocol TCP
    real_server 10.0.5.1 2379 {
        weight 10
        TCP_CHECK {
          connect_timeout 3
        }
    }
    real_server 10.0.2.1 2379 {
        weight 10
        TCP_CHECK {
          connect_timeout 3
        }
    }
    real_server 10.0.3.1 2379 {
        weight 10
        TCP_CHECK {
          connect_timeout 3
        }
    }

}

virtual_server 10.0.10.3 6789 {
    delay_loop 5
    lb_algo wlc
    lb_kind DR
    protocol TCP
    # 要启用持久化连接，必须配置
    persistence_timeout 300
    real_server 10.0.5.1 6789 {
        weight 10
        TCP_CHECK {
          connect_timeout 3
        }
    }
    real_server 10.0.2.1 6789 {
        weight 10
        TCP_CHECK {
          connect_timeout 3
        }
    }
    real_server 10.0.3.1 6789 {
        weight 10
        TCP_CHECK {
          connect_timeout 3
        }
    }

}</pre>
<p>在此配置下：</p>
<ol>
<li>LVS节点无法访问VIP的6443端口</li>
<li>RS节点可以访问VIP的6443端口，但是只会访问本机的服务，不会走负载均衡</li>
</ol>
<div class="blog_h1"><span class="graybg">常见问题</span></div>
<div class="blog_h2"><span class="graybg">DR模式相关</span></div>
<div class="blog_h3"><span class="graybg">连接卡在SYN_RECV</span></div>
<p>可能原因是：</p>
<ol>
<li>RS没有配置虚IP</li>
<li>RS上的服务没有监听虚IP所在的网络接口</li>
</ol>
</div><p>The post <a rel="nofollow" href="https://blog.gmem.cc/ipvs-and-keepalived">IPVS和Keepalived</a> appeared first on <a rel="nofollow" href="https://blog.gmem.cc">绿色记忆</a>.</p>
]]></content:encoded>
			<wfw:commentRss>https://blog.gmem.cc/ipvs-and-keepalived/feed</wfw:commentRss>
		<slash:comments>0</slash:comments>
		</item>
	</channel>
</rss>
