TOA 的由来
我们知道 LVS 之前有三种负载均衡模式:DR、NAT 和 Tunnel,但都有各自的缺陷,比如 DR 和 NAT 要求 virtual server 与 real server 在同一子网下,而 Tunnel 运维起来比较复杂。因此,为了灵活部署,开发了第四种模式,即 FULLNAT。
FULLNAT 模式是 NAT 模式的一种扩展,不仅会替换目的 IP,也会替换源 IP。带来的好处是,使得 virtual server 和 real server 摆脱后端网络的束缚,不再要求它们位于同一子网下。
但是,这种模式也带来了一个问题,real server 无法获取真实的客户端 IP 地址,而在很多业务场景下,我们在对外提供服务时,需要检查服务请求方的 IP 地址,来针对IP地址做一些业务处理,最常见的一个例子就是:做白名单校验,只有在白名单列表中的 IP 地址,我们才允许它访问我们的服务;还有一种应用场景,那就是基于客户端的请求 IP 来进行调度,譬如 CDN 服务,那么就需要根据客户端的请求 IP,来调度最近最适合的资源提供服务。
为了解决上述问题,TOA 应运而生,它实际是一个 TCP option filed,使用了 8 字节(kind = 0xfe,Length = 0x08,Value = 4B client's IP + 2B port),源码如下,
/* MUST be 4 bytes alignment */
struct toa_data {
__u8 opcode;
__u8 opsize;
__u16 port;
__u32 ip;
};
服务端机器打上 patch 后,在 lvs FULLNAT 模式下能够通过系统调用 getsockopt
拿到真实的 client IP 地址。
TOA 的使用
为了支持 TOA,FULLNAT 直接修改了内核代码,如果要重新编译内核,那使用起来就很麻烦了,我们可以以 .ko 文件的形式加载到内核,通过以下命令查看当前机器是否加载了 toa 模块,
lsmod | grep toa
toa 模块的编译可以参考文档 TOA插件配置。
TOA 的实现原理
TOA 主要通过 hook 系统函数,进而从 tcp option 解析出 toa data。
注意:以下说明中用到的 linux 源码版本为 3.2.101。
toa_init
函数是 toa 模块的初始化函数,
/* module init */
static int __init
toa_init(void)
{
...
/* hook funcs for parse and get toa */
hook_toa_functions();
...
}
以上省略了一些处理细节,重点代码是 hook 的处理函数 hook_toa_functions
,以 ipv4 协议为例进行说明。
/* replace the functions with our functions */
static inline int
hook_toa_functions(void)
{
/* hook inet_getname for ipv4 */
struct proto_ops *inet_stream_ops_p =
(struct proto_ops *)&inet_stream_ops;
/* hook tcp_v4_syn_recv_sock for ipv4 */
struct inet_connection_sock_af_ops *ipv4_specific_p =
(struct inet_connection_sock_af_ops *)&ipv4_specific;
...
inet_stream_ops_p->getname = inet_getname_toa;
...
ipv4_specific_p->syn_recv_sock = tcp_v4_syn_recv_sock_toa;
return 0;
}
在linux 源码中 ipv4 协议各处理函数有如下定义,
/* net/ipv4/tcp_ipv4.c */
const struct inet_connection_sock_af_ops ipv4_specific = {
..
.send_check = tcp_v4_send_check,
.conn_request = tcp_v4_conn_request,
.syn_recv_sock = tcp_v4_syn_recv_sock,
.get_peer = tcp_v4_get_peer,
};
EXPORT_SYMBOL(ipv4_specific);