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