内核代码之SIP ALG分析

以连接跟踪为入口,只分析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,

你可能感兴趣的:(内核代码之SIP ALG分析)