当前位置: 首页 > Linux

Toa内核模块分析

时间:2023-04-06 01:35:00 Linux

TOA我们知道LVS之前有过三种负载均衡模式:DR、NAT和Tunnel,但是它们都有各自的缺陷。例如DR和NAT要求虚拟服务器和真实服务器在同一个子网,而Tunnel的运维就比较复杂。因此,为了灵活部署,开发了第四种模式FULLNAT。FULLNAT模式是NAT模式的扩展,不仅替换了目的IP,还替换了源IP。好处是虚拟服务器和真实服务器摆脱了后端网络的束缚,不再需要在同一个子网下。但是,这种模式也带来了一个问题。真实服务器无法获取真实客户端IP地址。在很多业务场景中,当我们对外提供服务时,需要查询服务请求方的IP地址,以IP地址为目标。做一些业务处理,最常见的例子是:做白名单验证,只有白名单中的IP地址,我们才允许它访问我们的服务;还有一种应用场景,就是根据客户端的请求IP来进行调度,比如CDN服务,那么就需要根据客户端的请求IP,调度最合适的资源来提供服务。为了解决以上问题,TOA应运而生,它实际上是一个TCP选项字段,使用8个字节(kind=0xfe,Length=0x08,Value=4B客户端IP+2B端口),源码如下,/*必须是4字节对齐*/structtoa_data{__u8opcode;__u8优化;__u16端口;__u32ip;};服务器机器打好补丁后,可以在lvsFULLNAT模式下调用getsockopt获取真实的客户端IP地址。TOA的使用为了支持TOA,FULLNAT直接修改了内核代码。如果要重新编译内核,使用起来会很麻烦。我们可以将它以.ko文件的形式载入内核。使用如下命令查看当前机器是否加载toa模块,lsmod|greptoatoa模块编译可以参考文档TOA插件配置。TOA的实现原理TOA主要使用hook系统函数,然后从tcp选项中解析出toa数据。注:以下说明使用的linux源码版本为3.2.101。toa_init函数是toa模块的初始化函数,/*moduleinit*/staticint__inittoa_init(void){.../*hookfuncsforparseandgettoa*/hook_toa_functions();...}上面省略了一些处理细节,关键代码是钩子处理函数hook_toa_functions,这里以ipv4协议为例进行说明。/*用我们的函数替换函数*/staticinlineinthook_toa_functions(void){/*hookinet_getnameforipv4*/structproto_ops*inet_stream_ops_p=(structproto_ops*)&inet_stream_ops;/*为ipv4挂钩tcp_v4_syn_recv_sock*/structinet_connection_sock_af_ops*ipv4_specific_p=(structinet_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;return0;}linux源码中ipv4协议的各个处理函数定义如下,/4/ipv/tcp_ipv4.c*/conststructinet_connection_sock_af_opsipv4_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);stream类socket各处理数有如下定义,/*net/ipv4/af_inet.c*/conststructproto_opsinet_stream_ops={family=PF_INET,.bind=inet_bind,.connect=inet_stream_connect,.accept=inet_accept,.getname=inet_getname,.listen=inet_listen,.shutdown=inet_shutdown,...};EXPORT_SYMBOL(inet_stream_ops);结合linux源码和toa代码,发现两个关键hooks:syn_recv_sock函数指针tcp_v4_syn_recv_sock->tcp_v4_syn_recv_sock_toa;getname函数指针inet_getname->inet_getname_toasyn_recv_sock调用syn_recv_sock函数,在服务器收到第三次握手的ack包后触发调用逻辑,调用路径为tcp_v4_do_cp_>tcp_check_req->syn_recv_sock。/*net/ipv4/tcp_minisocks.c*/structsock*tcp_check_req(structsock*sk,structsk_buff*skb,structrequest_sock*req,structrequest_sock**prev){...child=inet_csk(sk)->icsk_af_ops->syn_recv_sock(sk,skb,req,NULL);if(child==NULL)gotolisten_overflow;...}另外,在阅读这部分linux源码的时候,发现serversocket在收到第三次握手时的状态还是TCP_LISTEN。inttcp_v4_do_rcv(structsock*sk,structsk_buff*skb){...if(sk->sk_state==TCP_LISTEN){structsock*nsk=tcp_v4_hnd_req(sk,skb);如果(!nsk)转到丢弃;/*第三次握手产生新的socket,进入逻辑*/if(nsk!=sk){sock_rps_save_rxhash(nsk,skb);if(tcp_child_process(sk,nsk,skb)){rsk=nsk;转到重置;}返回0;}}...}第三次握手会产生一个新的socket,初始状态为TCP_SYN_RECV,然后转换为TCP_ESTABLISHED。我们来看看替换函数tcp_v4_syn_recv_sock_toa的代码逻辑,staticstructsock*tcp_v4_syn_recv_sock_toa(structsock*sk,structsk_buff*skb,structrequest_sock*req,structdst_entry*dst){structsock*newsock=NULL;/*先走逻辑*/newsock=tcp_v4_syn_recv_sock(sk,skb,req,dst);/*解析toa数据放入newsock->sk_user_data*/if(NULL!=newsock&&NULL==newsock->sk_user_data){newsock->sk_user_data=get_toa_data(skb);..}returnnewsock;}解析toa数据的函数是get_toa_data。代码的关键是找到tcpoption对应的字段,解析成一个toa_data类型的变量sk_user_data,这里不再分析。inet_getname调用当我们需要从socket中获取客户端ip时,我们会调用inet_getname函数。使用它的一种方法是通过接受系统调用。#includeintaccept(intsockfd,structsockaddr*restrictaddr,socklen_t*restrictaddrlen);如果传入sockaddr类型变量,会触发inet_getname函数调用逻辑,/*net/socket.c*/SYSCALL_DEFINE4(accept4,int,fd,structsockaddr__user*,upeer_sockaddr,int__user*,upeer_addrlen,int,flags){...if(upeer_sockaddr){if(newsock->ops->getname(newsock,(structsockaddr*)&address,&len,2)<0){err=-ECONNABORTED;转到out_fd;}...}...}此外,还可以通过getpeername、getsockopt等系统调用触发。那么,我们来看看替换函数inet_getname_toa的实现逻辑。staticintinet_getname_toa(structsocket*sock,structsockaddr*uaddr,int*uaddr_len,intpeer){intretval=0;袜子结构*sk=袜子->sk;结构sockaddr_in*sin=(structsockaddr_in*)uaddr;结构toa_datatdata;/*调用原逻辑*/retval=inet_getname(sock,uaddr,uaddr_len,peer);/*sk_user_data如果有数据就会复制数据*/if(retval==0&&NULL!=sk->sk_user_data&&peer){if(sk_data_ready_addr==(unsignedlong)sk->sk_data_ready){memcpy(&tdata,&sk->sk_user_data,sizeof(tdata));如果(TCOPT_TOA==tdata.opcode&&TCPOLEN_TOA==tdata.opsize){sin->sin_port=tdata.port;sin->sin_addr.s_addr=tdata.ip;}...}...}returnretval;}当sk_user_data变量中有数据并且是toa数据时,会替换对应的ip和端口,这样就可以正常获取客户端ip和端口了。从上面的分析我们可以看出,toa模块的工作方式是在第三次握手时将toa数据解析到sk_user_data变量中,然后在每次需要的时候进行相应的替换。