内核代码之SIP ALG分析

    xiaoxiao2025-03-04  32

    以连接跟踪为入口,只分析SIP ALG主要的逻辑实现(以LAN侧为client分析),不重要的函数略过,重要的函数在函数头里写分析

    需要的预备知识: 1,了解网络协议族IPv4/IPv6,了解传输层协议udp/tcp,了解应用层协议SIP。 2,了解内核加载模块的机制,注册/proc/xxx, fileoperations等机制 3,了解内核网络模块hook机制,不同网络包的走向和hook点的优先级 4,了解连接跟踪,nat的基本概念

    约定:a函数调b函数和c函数,b函数调用了d函数,d函数深入分析,简写作:

    a() --->b() ------->d() { dosomething(); } --->c()

    用于分析的代码基于linux kernel 3.4

    重要的结构体及其作用:

    /** * 连接跟踪结构体,每一个连接只会有一个 * 重要成员tuplehash[IP_CT_DIR_MAX] * 和ext,ext里通常包函有nat扩展,help扩展和timeout扩展 * ct->ext 在添加nat, helper, timeout, 等 extend 的时候会初始化或更新, * ct->ext->offset[NF_CT_EXT_NAT] 指向 nf_conn_nat 数据的偏移地址,由nf_nat_init初始化,nf_nat_setup_info()添加 * ct->ext->offset[NF_CT_EXT_HELPER] 指向 nf_conn_help数据的偏移地址,由nf_conntrack_helper_init初始化,nf_ct_helper_ext_add()添加 * ct->ext->offset[NF_CT_EXT_TIMEOUT] 指向 nf_conn_timeout数据的偏移地址,由nf_conntrack_timeout_init初始化,nf_ct_timeout_ext_add()添加 * */ struct nf_conn { ... struct nf_conntrack_tuple_hash tuplehash[IP_CT_DIR_MAX]; ... nf_ct_ext *ext; ... }; /** * 五元组,每个传输层数据包都可以提取出一个五元组 * 以ipv4,udp协议为例,对于一个转发包: * orignal方向: * src:192.168.1.10:1234, dst:192.168.2.10:5678 * 那么,reply方向: * src:192.168.2.10:5678, dst:192.168.1.10:1234 * * 对于一个snat包(假设WAN口地址为202.n.n.n): * orignal方向: * src:192.168.1.10:1234, dst:8.8.8.8:5678 * snat后: * src:202.n.n.n:nnnn, dst:8.8.8.8:5678 * 那么,reply方向: * src:8.8.8.8:5678, dst:202.n.n.n:nnnn * 对于每一个连接,内核都会用一个nf_conn结构体去跟踪, * ct->tuplehash[IP_CT_DIR_ORIGINAL].tuple记录原始方向 * ct->tuplehash[IP_CT_DIR_REPLY].tuple记录这条连接的回应方向 */ struct nf_conntrack_tuple { src, { l3num;//网络层协议号,大多数情况下是AF_INET(ipv4)或AF_INET6(ipv6) u3;//源地址,可以是ipv4或ipv6地址 u;//源端口号 } dst { protonum;//传输层协议号,IPPROTO_TCP, IPPROTO_UDP, IPPROTO_ICMP等 dir;//方向original, 或reply u3;//目的地址,可以是ipv4或ipv6地址 u;//目的端口号 } } /** * help结构体,是 ct->ext 的一个扩展 * 一个连接的ext可以有多种扩展: help, nat, timeout等 * help只是ext的一种,基于连接。 * 而helper是基于协议的,当一个连接的tuple匹配上helper的tuple, * help与helper 就会关联起来 */ struct nf_conn_help { struct nf_conntrack_helper __rcu *helper;//在tuple匹配上后,对应的helper后会放入这里 union nf_conntrack_help help; struct hlist_head expectations; u8 expecting[NF_CT_MAX_EXPECT_CLASSES]; } /** * helper结构体,例如 ftp, sip 等 helper * 当一个数据包的tuple匹配上helper的 tuple就会执行help函数指针指向的函数 * 例如nf_conntrack_sip.c 里向内核注册了sip协议的helper,helper.tuple.src.u.udp.port ==5060 * 这里src port为5060,但是在给一个数据包添加helper的时候,是以IP_CT_DIR_REPLY方向来匹配的, * 即,sip包的helper是如果期待的返回端口为5060就给这个连接添加一个sip helper. */ struct nf_conntrack_helper { struct nf_conntrack_expect_policy expect_policy;//连接期望策略 struct nf_conntrack_tuple tuple;//五元组 (*help)()//函数指针,匹配上五元组即会在ipv4/6_confirm时被调用 } //net/ipv4/netfilter/nf_conntrack_l3proto_ipv4.c struct ipv4_conntrack_ops[]//连接跟踪"数据处理"模块向内核注册的结构体。ipv6也类似 { { .hook = ipv4_conntrack_in,//数据包勾子函数 数据 连接跟踪 入口 .owner = THIS_MODULE, .pf = NFPROTO_IPV4, .hooknum = NF_INET_PRE_ROUTING,//pre-routing .priority = NF_IP_PRI_CONNTRACK,//-200 conntrack,高于iptable的各个表 }, { .hook = ipv4_conntrack_local,// 数据 conntrack 入口 .owner = THIS_MODULE, .pf = NFPROTO_IPV4, .hooknum = NF_INET_LOCAL_OUT,//本机发出的包也要 conntrack .priority = NF_IP_PRI_CONNTRACK, }, ... }//像这样的结构体还有很多,但是它们都会调用到 nf_conntrack_in()函数

    连接跟踪模块 功能初始化的 (系统起动时初始化)主要过程:

    nf_conntrack_standalone_init()//nf_conntrack_net_ops//模块入口 --->nf_conntrack_net_init() ------->nf_conntrack_init() ----------->nf_conntrack_init_init_net() --------------->nf_conntrack_proto_init() --------------->nf_conntrack_helper_init()//初始化helper的功能 ----------->nf_conntrack_init_net() --------------->nf_conntrack_expect_init()//初始化expect的功能 --------------->nf_conntrack_timeout_init()//初始化timeout的功能 //nat也依赖于conntrack, nat功能在哪里初始化呢?在nf_nat_standalone.c里面

    下面分析helper功能的初始化:

    //net/netfilter/nf_conntrack_helper.c /** * 初始化一个 hashtable, 申请内存用于存放各种helper * 将helper_extend加入nf_ct_ext_types[NF_CT_EXT_HELPER] * 而 helper_extend 则指示了 nf_conn_help 需要的空间 */ int nf_conntrack_helper_init(void) { nf_ct_helper_hash = nf_ct_alloc_hashtable(); nf_ct_extend_register(&helper_extend); //注册extend { nf_ct_ext_types[NF_CT_EXT_HELPER] = helper_extend } } static struct nf_ct_ext_type helper_extend __read_mostly = { .len = sizeof(struct nf_conn_help), .align = __alignof__(struct nf_conn_help),//用于指示以这个结构体为宽度申请ct->ext的内存 .id = NF_CT_EXT_HELPER, }; /** 注册helper, ftp/sip/snmp/tftp都会使用这个注册函数, * 注册时计算tuple的hash值,放入hashTable nf_ct_helper_hash[h] * nf_conntrack_ftp.c注册 nf_conntrack_helper ftp[][] * nf_conntrack_sip.c注册 nf_conntrack_helper sip[][] */ int nf_conntrack_helper_register(struct nf_conntrack_helper *me) { unsigned int h = helper_hash(&me->tuple); hlist_add_head_rcu(&me->hnode, &nf_ct_helper_hash[h]); }

    下面分析数据流:

    数据流调用关系: ipv4_conntrack_in()ipv4_conntrack_local()__ipv6_conntrack_in() --->nf_conntrack_in() { l3proto = __nf_ct_l3proto_find(pf);//pf为PF_INET或PF_INET6,返回网络(IP)层的处理工具 l3proto->get_l4proto(skb, ...);//使用网络(IP)层处理工具解析网络层,实际调用的是ipv4_get_l4proto,或ipv6_get_l4proto l4proto = __nf_ct_l4proto_find(pf, protonum);//IP层解析后,传输层协议号(protonum)已知,代入此函数后返回相应的传输协议处理工具 ct = resolve_normal_ct(skb,l3proto,l4proto, ... );//下面单独分析 timeouts = l4proto->get_timeouts(net); l4proto->packet(ct, skb, dataoff, ctinfo, pf, hooknum, timeouts);//传输层处理 } resolve_normal_ct() { nf_ct_get_tuple(skb, &tuple, ...);//从数据中分析出tuple,方向设为IP_CT_DIR_ORIGINAL hash = hash_conntrack_raw(&tuple, zone); /**这里尝试去查找是否有匹配的tuple存在,这里original和reply方向都会查找。 * 如果找不到会新建一个ct。 * 存入发生在:ipv4_confirm()->nf_conntrack_confirm()->__nf_conntrack_confirm()->__nf_conntrack_hash_insert()里 * 下面的函数会返回insert时的hash, 注意hash不是一个数值, * 而是一个nf_conntrack_tuple_hash结构体,带了方向 */ h = __nf_conntrack_find_get(net, zone, &tuple, hash); if (!h) { h = init_conntrack();//下面单独分析 } ct = nf_ct_tuplehash_to_ctrack(h);//根据h的地址偏移量,由ct->tuplehash[h->tuple.dst.dir]反向推出ct的地址 ... //此段主要标记连接的状态,略 ... } init_conntrack() { //填一个repl_tuple, 与tuple地址相反,端口号相反,方向为 IP_CT_DIR_REPLY nf_ct_invert_tuple(&repl_tuple, tuple, l3proto, l4proto); ct = __nf_conntrack_alloc(); { ct = kmem_cache_alloc(); ct->tuplehash[IP_CT_DIR_ORIGINAL].tuple = *orig; ct->tuplehash[IP_CT_DIR_REPLY].tuple = *repl; setup_timer(&ct->timeout, death_by_timeout, (unsigned long)ct); } timeouts = l4proto->get_timeouts(net);//传输层协议相关的timeouts集合 l4proto->new(ct, skb, dataoff, timeouts);//传输层处理开始 ... //查找是否有LAN侧的包期望着这个包 //注意期望的匹配方法只用了协议族,协议类型,端口,没有用到IP地址 //这就是说,不需要IP匹配 exp = nf_ct_find_expectation(net, zone, tuple); if (exp){ ct->master = exp->master; help = nf_ct_helper_ext_add(ct, GFP_ATOMIC); rcu_assign_pointer(help->helper, exp->helper);//返回的包也要指定helper } else { __nf_ct_try_assign_helper(ct, tmpl, GFP_ATOMIC); { help = nfct_help(ct); //如果找到这个数据包的目的地址在内核有helper注册过,就给这个数据包添加helper //例如,如果这个包的目的地址是5060,那这个ct就会被加上SIP helper. helper = __nf_ct_helper_find(&ct->tuplehash[IP_CT_DIR_REPLY].tuple); help = nf_ct_helper_ext_add(ct, flags);//添加EXT_HELPER rcu_assign_pointer(help->helper, helper); } } if (exp) { if (exp->expectfn) //函数指针,对于SIP ALG而言,指向ip_nat_sip_expected() //如果匹配上exp, 则说明这个包是LAN侧期望的包,函数设置DNAT(将返回包的目的地址和端口还原回LAN侧的IP和端口) exp->expectfn(ct, exp); nf_ct_expect_put(exp); } return &ct->tuplehash[IP_CT_DIR_ORIGINAL]; }

    以上是数据包到达时的处理,主要包括: 1,如果是新包,设置helper, (helper处理时会加上期望). 2,如果是包已经被期望,执行期望处理函数。

    下面是数据在confirm时的处理:

    /** 关于连接期望, 以SIP为例子,有如下过程, * 在ipv4_confirm()/ipv6_confirm()被调用时,会执行在helper的help()函数,SIP 的help函数即:sip_help_tcp/udp() * ipv4_confirm()是钩子函数,注册于 NF_INET_POST_ROUTING,和 NF_INET_LOCAL_IN, * 而help函数在初始包到达时(这里是register包)里会申请expect,当然rtp也会在相应的包里申请expect. */ sip_help_tcp/udp()->process_sip_msg() { process_sip_request() { process_register_request();//下面单独分析 ... } process_sip_response()//略 if (ret == NF_ACCEPT && ct->status & IPS_NAT_MASK)//下一个函数有分析,数据走到此处已经nat过了 { nf_nat_sip(skb, dataoff, dptr, datalen);//函数指针,指向ip_nat_sip,下面单独分析 } } /** SIP ALG不光可以用于LAN侧挂客户端,也可以是LAN侧挂服务器(需要配合port mapping支持), * 这里为了方便,我以LAN侧挂客户端为例子。 */ process_register_request() { exp = nf_ct_expect_alloc(ct); saddr = &ct->tuplehash[!dir].tuple.src.u3;//期待返回包的源地址,通常即 SIP outbound proxy 的地址 nf_ct_expect_init(exp, SIP_EXPECT_SIGNALLING, nf_ct_l3num(ct), saddr, &daddr, proto, NULL, &port); { exp->class = class; exp->tuple.src.l3num = family;//IPv4 or IPv6 exp->tuple.dst.protonum = proto;//UDP or TCP memcpy(&exp->tuple.src.u3, saddr, len);//SIP outbound proxy 的地址 exp->tuple.src.u.all = 0; memcpy(&exp->tuple.dst.u3, daddr, len);//这个地址是从 SIP_HDR_CONTACT字段中解析出来的,即LAN侧源地址. exp->tuple.dst.u.all = *dst;//这个是地址从 SIP_HDR_CONTACT字段中解析出来的,即LAN侧源端口. } exp->timeout.expires = sip_timeout * HZ; exp->helper = nfct_help(ct)->helper; exp->flags = NF_CT_EXPECT_PERMANENT | NF_CT_EXPECT_INACTIVE; /*下面的nf_nat_sip_expect是函数指针,ip_nat_sip_expect是真正执行的函数 * 函数注册于nf_nat_sip.c * 执行条件是ct已经做过SNAT或DNAT,以SNAT为例,需要在PostRouting时换掉tuple[reply] * 而SNAT的优先级是NF_IP_PRI_NAT_SRC=100,高于ipv4_confirm()的优先级 NF_IP_PRI_CONNTRACK_CONFIRM=MAX * 所以下面的函数在执行时ct->status & IPS_NAT_MASK为true. */ if (nf_nat_sip_expect && ct->status & IPS_NAT_MASK) { nf_nat_sip_expect()->ip_nat_sip_expect() { //以下都以udp写的代码,但udp和tcp实际上在tuple里是一个地址,所以tcp也支持 newip = ct->tuplehash[!dir].tuple.dst.u3.ip;//因为已经做过SNAT所以这里是WAN IP. port = ntohs(exp->tuple.dst.u.udp.port);//端口还是用的LAN侧端口,但注意还没有最终确定。 exp->saved_ip = exp->tuple.dst.u3.ip;//将LAN侧源IP保留下来 exp->tuple.dst.u3.ip = newip; exp->saved_proto.udp.port = exp->tuple.dst.u.udp.port;//将LAN侧的源端口保留下来 exp->dir = !dir;//期待返回包 exp->expectfn = ip_nat_sip_expected; for (; port != 0; port++) { //WAN port很可能已经被占用了,比如LAN侧有两个客户端来自两个不同的IP, //都用5060去注册,但是WAN IP只有一个5060端口,所以这里不停的尝试, //找到可用的端口为止 exp->tuple.dst.u.udp.port = htons(port); ret = nf_ct_expect_related(exp);//加入net的期望列表,开始倒计时 } if (exp->tuple.dst.u3.ip != exp->saved_ip || exp->tuple.dst.u.udp.port != exp->saved_proto.udp.port) { //如果期待的包ip或port有变化,修改将要发出的包的源ip和端口, //这样返回的包才会匹配上期望 mangle_packet(skb, dataoff, dptr, datalen, xxx); } } } else { nf_ct_expect_related(exp);//没允许nat sip, 或没开nat,异常情况 } nf_ct_expect_put(exp); } ip_nat_sip() { //改包,将LAN侧地址和端口改为nat后的地址和端口,略 }

    总结: 1, SIP ALG不光可以用于LAN侧client,也可用于LAN侧做SIP Server. 2, 连接跟踪在helper里设置期望,在helper里改包,期望函数注册后的处理函数只是做DNAT。 3,

    最新回复(0)