LVS 学习: netfilter 与 ipvs 无秘密
此文章较长
一、介绍
Netfilter是Linux 2.4内核的一个子系统,Netfiler使得诸如数据包过滤、网络地址转换(NAT)以及网络连接跟踪等技巧成为可能,这些功能仅通过使用内核网络代码提供的各式各样的hook既可以完成。这些hook位于内核代码中,要么是静态链接的,要么是以动态加载的模块的形式存在。
Linux Virtual Server(LVS) 针对高可伸缩、高可用网络服务的需求,给出了基于IP层和基于内容请求分发的负载平衡调度解决方法,并在Linux内核中实现了这些方法,将一组服务器构成一个实现可伸缩的、高可用网络服务的虚拟服务器。由于负载调度技术是在Linux内核中实现的,我们称之为Linux虚拟服务器(Linux Virtual Server)。
LVS 项目的目标 :使用集群技术和Linux操作系统实现一个高性能、高可用的服务器,它具有很好的可伸缩性(Scalability)、可靠性(Reliability)和可管理性(Manageability)
在LVS框架中,提供了含有IP负载均衡技术的。
二、Netfilter
Netfilter中定义了五个关于IPv4的hook,对这些符号的声明可以在 中找到,可用的 IP HOOK 如下:
#define NF_IP_PRE_ROUTING 0 // 在进行完整性检查之后,可以截获接收的所有报文,包括目的地址是自己的报文和需要转发的报文;目的IP地址转换在此点
#define NF_IP_LOCAL_IN 1 // 路由决策后,可以截获目的地址是自己的报文,INPUT 包过滤在这里进行
#define NF_IP_FORWARD 2 // 截获所有转发的报文,FORWARD 在这里进行过滤
#define NF_IP_LOCAL_OUT 3 // 可以截获自身发出的所有报文(不包括转发),OUTPUT 过滤在这里进行
#define NF_IP_POST_ROUTING 4 // 可以截获发送的所有报文,包括自身发出的报文和转发的报文
在hook函数完成了对数据包所需的任何的操作之后,它们必须返回下列预定义的Netfilter返回值中的一个:
#define NF_DROP 0 // 丢弃数据包,不在继续
#define NF_ACCEPT 1 // 正常传输报文
#define NF_STOLEN 2 // Netfilter 模块接管该报文,不再继续传输
#define NF_QUEUE 3 // 对该数据报进行排队,通常用于将数据报提交给用户空间进程处理
#define NF_REPEAT 4 // 再次调用该钩子函数
#define NF_STOP 5 // 继续正常传输报文
:NF_ACCEPT和NF_STOP都表示报文通过了检查,可以正常向下流通。
表示报文通过了某个 函数的处理,下一个 函数可以接着处理了
表示报文通过了某个 函数的处理,后面的 函数你们就不要处理了
场景解释:假设有两个 分别是 、, > 优先级。 设定的处理结果是,那么报文就会有 提交给应用程序或者其他处理,因为放行了,根本不会给处理的机会。数据包依然有效
处理代码体现:
// https://elixir.bootlin.com/linux/v5.11.2/source/net/netfilter/nf_queue.c#L237
static unsigned int nf_iterate(struct sk_buff *skb, struct nf_hook_state *state,
const struct nf_hook_entries *hooks, unsigned int *index)
{
const struct nf_hook_entry *hook;
unsigned int verdict, i = *index;
while (i < hooks->num_hook_entries) {
hook = &hooks->hooks[i];
repeat:
verdict = nf_hook_entry_hookfn(hook, skb, state); // 调用 hook 函数
if (verdict != NF_ACCEPT) {
*index = i;
if (verdict != NF_REPEAT) // 不是重试直接返回
return verdict;
goto repeat; // 重试
...
}
2.1 注册/注销 hook
注册一个hook函数是围绕nf_hook_ops数据结构,数据结构的定义如下:
// https://elixir.bootlin.com/linux/v5.11.2/source/include/linux/netfilter.h#L77
typedef unsigned int nf_hookfn(void *priv,
struct sk_buff *skb,
const struct nf_hook_state *state);
struct nf_hook_ops {
nf_hookfn *hook;
struct net_device *dev;
void *priv;
u_int8_t pf;
unsigned int hooknum;
int priority;
};
struct nf_hook_state {
unsigned int hook;
u_int8_t pf;
struct net_device *in;
struct net_device *out;
struct sock *sk;
struct net *net;
int (*okfn)(struct net *, struct sock *, struct sk_buff *);
};
priv:私有数据
skb:正在处理的报文
state:将相关参数都将存储到state中
hook:hook 函数
dev:设备
pf:协议族
hooknum:hook 触发点的编号
priority:优先级
net_device *in:用于描述数据包到达的接口
net_device *out:用于描述数据包离开的接口
参数 只用于和,参数只用于和
注册一个Netfilter 需要调用 函数,以及用到一个 数据结构。函数以一个 数据结构的地址作为参数并且返回一个整型的值。 代码如下:
// https://elixir.bootlin.com/linux/v5.11.2/source/net/netfilter/core.c#L557
int nf_register_net_hooks(struct net *net, const struct nf_hook_ops *reg,
unsigned int n)
{
...
for (i = 0; i < n; i++) {
err = nf_register_net_hook(net, ®[i]); // 注册 hook 函数
if (err)
goto err; // 注册失败
}
return 0;
err:
if (i > 0) // 注销 hook 函数
nf_unregister_net_hooks(net, reg, i);
return err;
}
2.2 触发
因为制作 hook 触发需要将程序加载到内核中,所以先了解下 linux 内核模块化,加载和卸载。
创建两个个源代码文件:
// hds.c
#include // 任何模块都必须包含,定义了可动态加载到内核的模块所需要的必要信息
#include // 必须包含,包含了宏__init(指定初始化函数)和__exit(指定清除函数)
#include //里面包含常用的内核API,例如内核打印函数printk()
static int __init hds_init(void) //__init将函数hds_init()标记为初始化函数,在模块被装载到内核时调用hds_init()
{
printk(KERN_CRIT "Hello Kernell\n");
return 0;
}
static void __exit hds_exit(void) //清除函数,在模块被卸载之前调用
{
printk(KERN_ALERT "GoodBye Kernel\n");
}
module_init(hds_init);
module_exit(hds_exit);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("jony");
MODULE_DESCRIPTION("for fun");
// Makefile
obj-m:=hds.o #根据make的自动推导原则,make会自动将源程序hds.c编译成目标程序hds.o
#所有在配置文件中标记为-m的模块将被编译成可动态加载进内核的模块。即后缀为.ko的文件
CURRENT_PATH:=$(shell pwd) #参数化,将模块源码路径保存在CURRENT_PATH中
LINUX_KERNEL:=$(shell uname -r) #参数化,将当前内核版本保存在LINUX_KERNEL中
LINUX_KERNEL_PATH:=/usr/src/linux-headers-$(LINUX_KERNEL) #参数化,将内核源代码的绝对路径保存在LINUX_KERNEL_PATH中
all:
make -C $(LINUX_KERNEL_PATH) M=$(CURRENT_PATH) modules #编译模块
clean:
make -C $(LINUX_KERNEL_PATH) M=$(CURRENT_PATH) clean #清理
两个文件创建完成后,执行编译加载命令:
$ make # 执行编译
$ sudo insmod hds.ko # 模块加载到内核
$ lsmod |grep hds # 查看是否加载成功
$ dmesg # 查看内核输出
$ sudo rmmod hds # 卸载内核模块
制作 HOOK
制作一个轻量级防火墙,根据 中的 字段来制作防火墙。比如当 等于 的时候我们就返回 ,数据包会自动销毁。
// drop_if_lo.c
#include
#include
#include
#include
#include
#include
#include
#include
static struct nf_hook_ops nf_drop;
static char *if_name = "eth0";
unsigned int hook_func(void *priv,
struct sk_buff *skb,
const struct nf_hook_state *state)
{
if(state->out != NULL && strcmp(state->out->name,if_name) == 0)
{
return NF_ACCEPT;
}
else
{
return NF_DROP;
}
return 0;
}
static int __init drop_if_lo_init(void)
{
printk(KERN_CRIT "drop_if_lo_init");
nf_drop.hook = &hook_func;
nf_drop.pf = PF_INET;
nf_drop.hooknum = NF_INET_LOCAL_OUT;
nf_drop.priority = NF_IP_PRI_FIRST;
nf_register_net_hook(&init_net, &nf_drop);
return 0;
}
static void __exit drop_if_lo_exit(void)
{
printk(KERN_ALERT "drop if lo exit\n");
nf_unregister_net_hook(&init_net, &nf_drop);
}
module_init(drop_if_lo_init);
module_exit(drop_if_lo_exit);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("jony");
MODULE_DESCRIPTION("drop if eth0");
//Makefile
obj-m:=drop_if_lo.o
CURRENT_PATH:=$(shell pwd)
LINUX_KERNEL:=$(shell uname -r)
LINUX_KERNEL_PATH:=/usr/src/linux-headers-$(LINUX_KERNEL)
all:
make -C $(LINUX_KERNEL_PATH) M=$(CURRENT_PATH) modules
clean:
make -C $(LINUX_KERNEL_PATH) M=$(CURRENT_PATH) clean
问题:理想状态是只会删除 eth0 的数据包,不会删除其他网卡的数据包,但是实际情况是任何数据包都被删除了。查下原因本地访问会自动转为 lo 设备。
总结:Netfilter 基本上特点大概都了解了一遍,基本上 Netfilter 原理大致都了解,也可以通过编写内核模块来编写 Netfilter 插件,加载到内核中执行。
三、IPVS
学完 Netfilter 之后,在看 IPVS 基本已经没有秘密,IPVS 也是在基于 Netfilter 编写的一款插件。关于注册就不在考虑如何注册整个流程,只学习下几个核心功能。
3.1 ip_vs_service
的创建通过 函数来完成,真实代码如下:
// https://elixir.bootlin.com/linux/v5.11.2/source/net/netfilter/ipvs/ip_vs_ctl.c#L1286
static int
ip_vs_add_service(struct netns_ipvs *ipvs, // 所属哪个命名空间
struct ip_vs_service_user_kern *u, // 用户通过命令行配置的规则信息
struct ip_vs_service **svc_p
)
{
int ret = 0, i;
struct ip_vs_scheduler *sched = NULL;
struct ip_vs_pe *pe = NULL;
struct ip_vs_service *svc = NULL;
int ret_hooks = -1;
if (strcmp(u->sched_name, "none")) { // 根据调度器名称获取调度策略对象
sched = ip_vs_scheduler_get(u->sched_name);
}
if (u->pe_name && *u->pe_name) { // 根据持久化名称获取持久化管理方法
pe = ip_vs_pe_getbyname(u->pe_name);
}
if ((u->af == AF_INET && !ipvs->num_services) ||
(u->af == AF_INET6 && !ipvs->num_services6)) {
ret = ip_vs_register_hooks(ipvs, u->af); // 如果是首次创建 SVC,那么就将调度策略注册到 Netfilter
if (ret < 0)
goto out_err;
ret_hooks = ret;
}
...
svc = kzalloc(sizeof(struct ip_vs_service), GFP_KERNEL); // 申请一个 ip_vs_service 对象
svc->af = u->af; // 3 层协议
svc->protocol = u->protocol; // 4 层协议
ip_vs_addr_copy(svc->af, &svc->addr, &u->addr); // svc IP
svc->port = u->port; // svc 端口
svc->fwmark = u->fwmark; // 防火墙标记,持久化对象
svc->flags = u->flags; // 标志位
svc->timeout = u->timeout * HZ; // 超时时间
svc->netmask = u->netmask; // 网络掩码
svc->ipvs = ipvs;
INIT_LIST_HEAD(&svc->destinations);
spin_lock_init(&svc->sched_lock);
spin_lock_init(&svc->stats.lock);
if (sched) {
ret = ip_vs_bind_scheduler(svc, sched); // 绑定到指定的调度器
if (ret)
goto out_err;
sched = NULL;
}
RCU_INIT_POINTER(svc->pe, pe); // 初始化持久化方式
pe = NULL;
...
ip_vs_svc_hash(svc); // 添加 ip_vs_service 到 hash 表
*svc_p = svc; // 返回 svc
ipvs->enable = 1;
return 0;
}
上面的代码主要完成一下几个工作:
通过调用 函数来获取一个 调度器
申请 对象,并初始化。然后将上面获取到调度器,与当前 绑定
最终将 对象添加到全局 hash 表中。(思考:内存决定了 hash 表的上线,如果使用 LRU 是否可以进一步扩展至硬盘)
3.2、ip_vs_dest
真实服务器对象,主要用于创建保存真实服务器 (Real-Server) 的相关配置信息。创建 对象通过 创建,具体代码细节如下:
//https://elixir.bootlin.com/linux/v5.11.2/source/net/netfilter/ipvs/ip_vs_ctl.c#L1038
static int
ip_vs_add_dest(struct ip_vs_service *svc, struct ip_vs_dest_user_kern *udest)
{
struct ip_vs_dest *dest;
union nf_inet_addr daddr;
__be16 dport = udest->port;
int ret;
// 复制目标地址
ip_vs_addr_copy(udest->af, &daddr, &udest->addr);
// 冲当前 svc 的hash 中获取 目标RS地址,检查是否存在,如果存在就直接返回
dest = ip_vs_lookup_dest(svc, udest->af, &daddr, dport);
if (dest != NULL) {
return -EEXIST;
}
// 从删除但是没有回收的 hash 中查找 目标RS地址,如果存在就撤回删除
// 为什么删除了却没有回收,因为需要保证 conn 被正确的关闭,因为如果 conn 还在被引用
// 直接删除可能会引发不可预测的问题
dest = ip_vs_trash_get_dest(svc, udest->af, &daddr, dport);
if (dest != NULL) {
// 撤回删除状态
__ip_vs_update_dest(svc, dest, udest, 1);
ret = 0;
} else {
// 新建目标 rs 地址,映射到 hash 中
ret = ip_vs_new_dest(svc, udest, &dest);
}
return ret;
}
static int
ip_vs_new_dest(struct ip_vs_service *svc, struct ip_vs_dest_user_kern *udest,
struct ip_vs_dest **dest_p)
{
struct ip_vs_dest *dest;
unsigned int atype, i;
...
dest = kzalloc(sizeof(struct ip_vs_dest), GFP_KERNEL);
...
dest->af = udest->af; // 地址族
dest->protocol = svc->protocol; // 端口协议
dest->vaddr = svc->addr; // 虚拟 IP 地址
dest->vport = svc->port; // 虚拟端口
dest->vfwmark = svc->fwmark; // 虚拟网络掩码
ip_vs_addr_copy(udest->af, &dest->addr, &udest->addr); // 添加的 RS 地址
dest->port = udest->port; // 添加的 RS 端口
atomic_set(&dest->activeconns, 0);
atomic_set(&dest->inactconns, 0);
atomic_set(&dest->persistconns, 0); // 初始化连接状态
refcount_set(&dest->refcnt, 1);
INIT_HLIST_NODE(&dest->d_list);
spin_lock_init(&dest->dst_lock);
spin_lock_init(&dest->stats.lock);
__ip_vs_update_dest(svc, dest, udest, 1); // 更新 svc hash 桶中的 dest 地址
*dest_p = dest;
return 0;
}
ip_vs_new_dest() 函数的实现也比较简单,与 svc 一样通过调用 kmalloc() 函数申请一个 ip_vs_dest ,然后根据用户配置的规则信息来初始化 ip_vs_dest 对象的各个字段。
3.3、ip_vs_scheduler
用于从 对象的 hash 桶中获取一个合适的 对象,结构定义如下:
struct ip_vs_scheduler {
struct list_head n_list; /* 调度策略链表 */
char *name; /* scheduler 名称 */
atomic_t refcnt; /* 引用基数 */
struct module *module; /* THIS_MODULE/NULL */
/* scheduler 初始化一个 svc */
int (*init_service)(struct ip_vs_service *svc);
/* scheduling 停止一个 svc */
void (*done_service)(struct ip_vs_service *svc);
/* 连接一个目标服务 */
int (*add_dest)(struct ip_vs_service *svc, struct ip_vs_dest *dest);
/* 解除一个目标服务的连接 */
int (*del_dest)(struct ip_vs_service *svc, struct ip_vs_dest *dest);
/* 更新一个目标服务 */
int (*upd_dest)(struct ip_vs_service *svc, struct ip_vs_dest *dest);
/* 选择一个真实服务器的对象 */
struct ip_vs_dest* (*schedule)(struct ip_vs_service *svc,
const struct sk_buff *skb,
struct ip_vs_iphdr *iph);
};
上述字段中, 是指向一个函数的指针,用于从 对象的 hash 桶中获取一个合适的 对象。 定义如下:
static struct ip_vs_scheduler ip_vs_rr_scheduler = {
.name = "rr", /* 策略名称 */
.refcnt = ATOMIC_INIT(0), // 引用基数
.module = THIS_MODULE,
.n_list = LIST_HEAD_INIT(ip_vs_rr_scheduler.n_list),
.init_service = ip_vs_rr_init_svc, // 调度策略,初始化 svc 方法 ip_vs_rr_init_svc
.add_dest = NULL,
.del_dest = ip_vs_rr_del_dest, // 删除一个目标服务
.schedule = ip_vs_rr_schedule, // 调度策略
};
3.4、ip_vs_conn
对象是用于维护 和 之间的联系,因为网络都是分包发送的,如果将每个包都是使用调度策略,那么将会产生很多包失效的问题, 将会受到莫名其妙的网络包。
所以为了将来源包发送经过匹配发送到后端某一绑定实例,后端才会收到有用的数据。保持连接的方式有很多种,这里介绍比较常用的 (来源 hash)。
先了解下 的结构定义:
struct ip_vs_conn {
struct hlist_node c_list; /* hashed list heads */
/* Protocol, addresses and port numbers */
__be16 cport; // 来源端口
__be16 dport; // 目标端口
__be16 vport; // 虚拟端口
u16 af; /* 地址族 */
union nf_inet_addr caddr; /* 客户端 IP 地址 */
union nf_inet_addr vaddr; /* 虚拟 IP 地址 */
union nf_inet_addr daddr; /* 目标地址 */
volatile __u32 flags; /* 状态 */
__u16 protocol; /* 4 层协议 (TCP/UDP) */
__u16 daf; /* 目标地址族 */
struct netns_ipvs *ipvs;
/* 统计和计时器 */
refcount_t refcnt; /* 引用计数 */
struct timer_list timer; /* 定时器 */
volatile unsigned long timeout; /* 超时时长 */
/* Flags and state transition */
spinlock_t lock; /* 锁定状态转换 */
volatile __u16 state; /* state 信息 */
volatile __u16 old_state;
__u32 fwmark; /* 来自skb的防火墙标志 */
unsigned long sync_endtime; /* jiffies + sent_retries */
/* 管理字段 */
struct ip_vs_conn *control; /* Master control connection */
atomic_t n_control; /* Number of controlled ones */
struct ip_vs_dest *dest; /* real server */
atomic_t in_pkts; /* incoming packet counter */
/* 发送方式,DR、NAT、Tunnel 都会使用不同的方式进行数据包的转发,
会根据不同的转发方式,返回 netfilter 结果,如 NF_STOLEN、NF_ACCEPT */
int (*packet_xmit)(struct sk_buff *skb, struct ip_vs_conn *cp,
struct ip_vs_protocol *pp, struct ip_vs_iphdr *iph);
/* 下面几个成员主要用于 NAT 模式
*/
struct ip_vs_app *app; /* bound ip_vs_app object */
void *app_data; /* Application private data */
struct ip_vs_seq in_seq; /* incoming seq. struct */
struct ip_vs_seq out_seq; /* outgoing seq. struct */
const struct ip_vs_pe *pe;
char *pe_data;
__u8 pe_data_len;
struct rcu_head rcu_head;
};
具体转发模式的绑定使用 函数,根据每个连接的不同,绑定转发模式:
static inline void ip_vs_bind_xmit(struct ip_vs_conn *cp)
{
switch (IP_VS_FWD_METHOD(cp)) {
case IP_VS_CONN_F_MASQ:
cp->packet_xmit = ip_vs_nat_xmit; // NAT 模式
break;
case IP_VS_CONN_F_TUNNEL:
cp->packet_xmit = ip_vs_tunnel_xmit; // Tunnel 模式
break;
case IP_VS_CONN_F_DROUTE:
cp->packet_xmit = ip_vs_dr_xmit; // DR 模式
break;
case IP_VS_CONN_F_LOCALNODE:
cp->packet_xmit = ip_vs_null_xmit; // Null 模式,用于本地地址
break;
case IP_VS_CONN_F_BYPASS:
cp->packet_xmit = ip_vs_bypass_xmit; // 当目标 RS 不可用时,让数据包绕,可能只用缓存
break;
}
}
当一个 请求到达 服务器后, 会首先根据指定元数据信息和 策略,查找当前来源是否已经有存在的连接,如果有将会通过存在的连接,将请求转发到后端 ,如果没有那么就创建一个新的连接并且将新建的连接保存。
客户端请求 -> LVS -> 是否已经存在连接 -是-> 复用已存在的连接 -> 发送到 RS -> 结束
| /|\
\|/ |
否 ----> 新建连接并保存
3.5、ip_vs_xmit
常用的将 接收到的来源数据包转发到 RS 服务器上的模式核心的三种:、、
3.5.1、DR
(Director)相对容易理解。核心点在目标 地址上,通过修改目标 地址可以完成这种方案。
// https://elixir.bootlin.com/linux/v5.11.2/source/net/netfilter/ipvs/ip_vs_xmit.c#L1435
int ip_vs_dr_xmit(struct sk_buff *skb, struct ip_vs_conn *cp,
struct ip_vs_protocol *pp, struct ip_vs_iphdr *ipvsh)
{
int local;
// 根据元数据信息查找目的服务的路由信息
local = __ip_vs_get_out_rt(cp->ipvs, cp->af, skb, cp->dest, cp->daddr.ip,
IP_VS_RT_MODE_LOCAL |
IP_VS_RT_MODE_NON_LOCAL |
IP_VS_RT_MODE_KNOWN_NH, NULL, ipvsh);
if (local < 0)
goto tx_error;
if (local)
return ip_vs_send_or_cont(NFPROTO_IPV4, skb, cp, 1);
ip_send_check(ip_hdr(skb));
// 不允许分片
skb->ignore_df = 1;
// 直接将修改好的数据包发送出去
ip_vs_send_or_cont(NFPROTO_IPV4, skb, cp, 0);
return NF_STOLEN;
}
通过 方法查找实际目标路由,并替换掉当前路由信息,在通过 发送出去,发送的动作同样关联到了 的 代码如下:
// https://elixir.bootlin.com/linux/v5.11.2/source/net/netfilter/ipvs/ip_vs_xmit.c#L307
static int
__ip_vs_get_out_rt(struct netns_ipvs *ipvs, int skb_af, struct sk_buff *skb,
struct ip_vs_dest *dest,
__be32 daddr, int rt_mode, __be32 *ret_saddr,
struct ip_vs_iphdr *ipvsh)
{
struct net *net = ipvs->net;
struct ip_vs_dest_dst *dest_dst;
struct rtable *rt; /* Route to the other host */
int mtu;
int local, noref = 1;
if (dest) {
...
} else {
rt = do_output_route4(net, daddr, rt_mode, &saddr); // 查找路由信息
}
...
mtu = dst_mtu(&rt->dst); // 检测 MTU
skb_dst_drop(skb); // 释放原 dst 信息
if (noref) // 替换新的 dst 信息
skb_dst_set_noref(skb, &rt->dst);
else
skb_dst_set(skb, &rt->dst);
return local;
模式通过修改请求报文的 地址,将请求发送到 RS 服务器,RS 服务器将响应直接发送给请求方。DR 模式因为没有 IP 隧道的开销,也没有这方面的需求,所以 与 处于同一个物理网段上就可以。同时需要 RS 服务器支持 VIP 识别, 和 都配置了相同的对外服务的 , 也配有自己真实的IP,但是 的 绑定不会响应 请求,但是可以接收访问 的数据包。
3.5.2、NAT
当前 支持的 只是 模式,只会对目标地址 。DNAT 代码通过 完成。
// https://elixir.bootlin.com/linux/v5.11.2/source/net/netfilter/ipvs/ip_vs_xmit.c#L826
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;
local = __ip_vs_get_out_rt(cp->ipvs, cp->af, skb, cp->dest, cp->daddr.ip, // 查找路由信息,替换新的路由,同上
IP_VS_RT_MODE_LOCAL |
IP_VS_RT_MODE_NON_LOCAL |
IP_VS_RT_MODE_RDR, NULL, ipvsh);
rt = skb_rtable(skb);
/* 修改包采用 copy-on-write */
if (skb_ensure_writable(skb, sizeof(struct iphdr)))
goto tx_error;
/* 管理包 */
if (pp->dnat_handler && !pp->dnat_handler(skb, pp, cp, ipvsh)) // TCP 协议使用 tcp_dnat_handler,作用于修改 L4 的元数据信息,如目的端口、校验和
goto tx_error;
ip_hdr(skb)->daddr = cp->daddr.ip; // 修改目标 IP 地址
ip_send_check(ip_hdr(skb)); // 重新计算校验和
skb->ignore_df = 1; // 禁止分片
rc = ip_vs_nat_send_or_cont(NFPROTO_IPV4, skb, cp, local); // NAT 的形式发送出去
return rc;
}
查找 RS 对应的路由信息
将路由旧 dest 替换成新的 dest
修改目标 IP 和 目标端口
重新计算校验和
发出数据包
模式,是通过修改目标 ,将请求发送到目标 服务器, 服务器收到请求数据后通过经过处理,返回响应包,响应数据根据 查找服务和连接表,将 修改为 ,通过路由查找确定下一条和出口,将数据包发送到网关(LVS),由网关将数据正确发出。因为 在 模式下起到网关的作用,所以进出数据包都会经过 ,也就是 将会成为性能瓶颈,同时因为 不仅要处理进数据包还要处理出数据包所以在性能方面有所损耗,相对于 模式可能损耗在 10% 左右
3.6、ip_vs_app
是通过 LVS 实现了应用层协议的一个框架。为应用层提供了接口,对一些特殊的应用可以进行特殊的处理。目前只支持应用层 协议。
四、知识串联
通过整个分析来,LVS 核心的代码主要集中在:、、 、四个个代码文件。分别负责:连接相关、核心流程、对外接口、转发模式。
关于请求的转发与响应主要通过 和 两个函数, 运行在 的 阶段, 运行在 的 阶段
static const struct nf_hook_ops ip_vs_ops4[] = {
...
{
.hook = ip_vs_remote_request4,
.pf = NFPROTO_IPV4,
.hooknum = NF_INET_LOCAL_IN,
.priority = NF_IP_PRI_NAT_SRC - 1,
},
...
}
static unsigned int
ip_vs_in(struct netns_ipvs *ipvs, unsigned int hooknum, struct sk_buff *skb, int af)
{
struct ip_vs_iphdr iph;
struct ip_vs_protocol *pp;
struct ip_vs_proto_data *pd;
struct ip_vs_conn *cp;
int ret, pkts;
int conn_reuse_mode;
struct sock *sk;
...
pd = ip_vs_proto_data_get(ipvs, iph.protocol); // 获取协议
cp = INDIRECT_CALL_1(pp->conn_in_get, ip_vs_conn_in_get_proto,
ipvs, af, skb, &iph); // 如果存在链接对象,就直接返回现有链接对象
if (unlikely(!cp)) { // 如果链接不存在,就会新建一个链接对象
int v;
if (!ip_vs_try_to_schedule(ipvs, af, skb, pd, &v, &cp, &iph))
return v;
}
if (cp->packet_xmit) // 将包发送出去
ret = cp->packet_xmit(skb, cp, pp, &iph);
ip_vs_conn_put(cp); // 回收链接
return ret;
}
新建链接代码如下:
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;
struct ip_vs_conn *cp = NULL;
struct ip_vs_scheduler *sched;
struct ip_vs_dest *dest;
__be16 _ports[2], *pptr, cport, vport;
const void *caddr, *vaddr;
unsigned int flags;
*ignored = 1;
...
if ((!skb->dev || skb->dev->flags & IFF_LOOPBACK)) {
iph->hdr_flags ^= IP_VS_HDR_INVERSE;
cp = INDIRECT_CALL_1(pp->conn_in_get,
ip_vs_conn_in_get_proto, svc->ipvs,
svc->af, skb, iph); // 检测是否已经存有链接对象
iph->hdr_flags ^= IP_VS_HDR_INVERSE;
if (cp) {
__ip_vs_conn_put(cp);
return NULL;
}
{
struct ip_vs_conn_param p;
ip_vs_conn_fill_param(svc->ipvs, svc->af, iph->protocol,
caddr, cport, vaddr, vport, &p); // 链接参数补齐
cp = ip_vs_conn_new(&p, dest->af, &dest->addr,
dest->port ? dest->port : vport,
flags, dest, skb->mark); // 新建连接
}
ip_vs_conn_stats(cp, svc);
return cp;
}
}
LVS 在 由 博士创建的,经历住了时间的考验,是一个非常优秀的项目,目前仍然有很多公司在用。LVS 所支持的模式:NAT、TUN、DR 三种模式,各有优缺点,但是都需要对后端 RS 配置操作。后添加了 模式才解决了需要后端操作的方式,但是相对应的性能方面有所牺牲。
如果你对LVS项目感兴趣,请访问Linux Vritual Server项目的主页(http://www.LinuxVirtualServer.org/或者http://www.linux-vs.org/),你可以获得最新的 LVS 源代码和有关运行软件,及最新的文档资料。