概述
目录
1 钩子函数
1.1 PRE_ROUTING 钩子 nf_nat_ipv4_in()
1.2 POST_ROUTING 钩子 nf_nat_ipv4_out()
1.3 OUTPUT 钩子 nf_nat_ipv4_local_fn()
1.4 LOCAL_IN 钩子 nf_nat_ipv4_fn()
1.5 公共钩子调用 nf_nat_ipv4_fn()
1.5.1 NAT转换判断 nf_nat_initialized()
1.5.2 NAT 转换操作 nf_nat_rule_find()
1.5.3 数据包进行 NAT 转换 nf_nat_packet()
1.6 连接跟踪的地址转换 nf_nat_setup_info()【核心】
2 target 函数
2.1 SNAT 功能
2.2 DNAT 功能
3 实例分析
3.1 SNAT
3.1.1 环境说明
3.1.2 数据SNAT转换分析
3.2 DNAT
3.2.1 环境说明
3.2.2 数据的DNAT分析
4 nat表配置演练(iptables )
4.1 配置转发设备
4.2 本地loopback重定向
4.2.1 loopback 接口(火星报文)
4.2.2 route_localnet 路由逻辑 ip_route_input_slow()
4.3 本地地址重定向
1 钩子函数
NAT模块主要是在NF_IP_PREROUTING、NF_IP_POSTROUTING、NF_IP_LOCAL_OUT、NF_IP_LOCAL_IN四个节点上进行NAT操作,在上一节中我们知道nat表中只有PREROUTING、POSTROUTING、LOCAL_OUT三条链,而没有NF_IP_LOCAL_IN链,所以不能创建在LOCAL_IN hook点的SNAT操作。
而NAT模块在注册hook函数时又在LOCAL_IN点注册了hook函数,且hook函数也调用了NAT转换的通用处理函数,难道也要对LOCAL_IN的数据包进行NAT转换吗?
其实,在LOCAL_IN注册hook函数主要不是为了进行NAT转换,因为在系统为一个源ip为A的转发数据包进行了SNAT后,可能会对源端口获取一个随机的值,这时如果源ip为A的数据包要发送给网关时,可能源端口就是刚才NAT转换的那个源端口,此时为了保证连接跟踪项的原始方向的tuple变量的唯一性,就需要在LOCAL_IN的hook点通过调用NAT转换的通用处理函数,改变源端口值,重新获取一个新的唯一的且未被使用的tuple变量。这应该就是LOCAL_IN也需要hook回调函数的原因吧。
总结一下就是 LOCAL_IN 钩子函数,主要是用来修改端口号,使得连接跟踪的 tuple 唯一。
1.1 PRE_ROUTING 钩子 nf_nat_ipv4_in()
这个函数是NAT模块在PRE_ROUTING hook点上注册的回调函数,该函数主要是实现DNAT功能,该函数的定义如下,主要实现如下两个功能:
- 调用函数 nf_nat_ipv4_fn 实现DNAT转换
- 当转换后数据包的目的 ip 地址改变后,需要调用 skb_dst_drop() ,将 skb 对 dst_entry 的引用减一,然后将 skb->_skb_refdst 置为NULL
static unsigned int
nf_nat_ipv4_in(unsigned int hooknum,
struct sk_buff *skb,
const struct net_device *in,
const struct net_device *out,
int (*okfn)(struct sk_buff *))
{
unsigned int ret;
__be32 daddr = ip_hdr(skb)->daddr;
ret = nf_nat_ipv4_fn(hooknum, skb, in, out, okfn);
if (ret != NF_DROP && ret != NF_STOLEN &&
daddr != ip_hdr(skb)->daddr)
skb_dst_drop(skb);
return ret;
}
该函数主要是通过调用 nf_nat_ipv4_fn ,该函数是一个通用 NAT 转换函数,待会着重分析这个函数
1.2 POST_ROUTING 钩子 nf_nat_ipv4_out()
这个函数是 NAT 模块在 POST_ROUTING hook点的hook回调函数,该函数实现如下功能:
调用函数 nf_nat_ipv4_fn 实现SNAT转换
static unsigned int
nf_nat_ipv4_out(unsigned int hooknum,
struct sk_buff *skb,
const struct net_device *in,
const struct net_device *out,
int (*okfn)(struct sk_buff *))
{
#ifdef CONFIG_XFRM
const struct nf_conn *ct;
enum ip_conntrack_info ctinfo;
int err;
#endif
unsigned int ret;
/* root is playing with raw sockets. */
if (skb->len < sizeof(struct iphdr) ||
ip_hdrlen(skb) < sizeof(struct iphdr))
return NF_ACCEPT;
ret = nf_nat_ipv4_fn(hooknum, skb, in, out, okfn);
#ifdef CONFIG_XFRM
if (ret != NF_DROP && ret != NF_STOLEN &&
!(IPCB(skb)->flags & IPSKB_XFRM_TRANSFORMED) &&
(ct = nf_ct_get(skb, &ctinfo)) != NULL) {
enum ip_conntrack_dir dir = CTINFO2DIR(ctinfo);
if ((ct->tuplehash[dir].tuple.src.u3.ip !=
ct->tuplehash[!dir].tuple.dst.u3.ip) ||
(ct->tuplehash[dir].tuple.dst.protonum != IPPROTO_ICMP &&
ct->tuplehash[dir].tuple.src.u.all !=
ct->tuplehash[!dir].tuple.dst.u.all)) {
err = nf_xfrm_me_harder(skb, AF_INET);
if (err < 0)
ret = NF_DROP_ERR(err);
}
}
#endif
return ret;
}
1.3 OUTPUT 钩子 nf_nat_ipv4_local_fn()
这个函数是 NAT 模块在 OUTPUT hook点的hook回调函数,该函数实现如下功能:
- 功能:实现DNAT转换功能
- 调用函数 nf_nat_ipv4_fn实现 DNAT 转换,调用 ip_route_me_harder,重新进行路由操作(与PRE_ROUTING不同的是,对于 OUTPUT的hook回调函数,当目的地址改变后,需要在该函数里调用 ip_route_me_harder重新查找路由,而在PRE_ROUTING链中,则是将skb->dst置为空, 然后在数据包往下执行时会自行重新查找路由。OUTPUT链接收的数据均是已经路由 的数据包,且后续调用函数中不会再有查找路由的操作,所以要nf_nat_local_fn 里实现路由查找)。
static unsigned int
nf_nat_ipv4_local_fn(unsigned int hooknum,
struct sk_buff *skb,
const struct net_device *in,
const struct net_device *out,
int (*okfn)(struct sk_buff *))
{
const struct nf_conn *ct;
enum ip_conntrack_info ctinfo;
unsigned int ret;
int err;
/* root is playing with raw sockets. */
if (skb->len < sizeof(struct iphdr) ||
ip_hdrlen(skb) < sizeof(struct iphdr))
return NF_ACCEPT;
ret = nf_nat_ipv4_fn(hooknum, skb, in, out, okfn);
if (ret != NF_DROP && ret != NF_STOLEN &&
(ct = nf_ct_get(skb, &ctinfo)) != NULL) {
enum ip_conntrack_dir dir = CTINFO2DIR(ctinfo);
if (ct->tuplehash[dir].tuple.dst.u3.ip !=
ct->tuplehash[!dir].tuple.src.u3.ip) {
err = ip_route_me_harder(skb, RTN_UNSPEC);
if (err < 0)
ret = NF_DROP_ERR(err);
}
#ifdef CONFIG_XFRM
else if (!(IPCB(skb)->flags & IPSKB_XFRM_TRANSFORMED) &&
ct->tuplehash[dir].tuple.dst.protonum != IPPROTO_ICMP &&
ct->tuplehash[dir].tuple.dst.u.all !=
ct->tuplehash[!dir].tuple.src.u.all) {
err = nf_xfrm_me_harder(skb, AF_INET);
if (err < 0)
ret = NF_DROP_ERR(err);
}
#endif
}
return ret;
}
1.4 LOCAL_IN 钩子 nf_nat_ipv4_fn()
NAT模块在NF_LOCAL_IN的hook回调函数就是直接调用 nf_nat_ipv4_fn,此处需要注意以下信息:
对于NF_LOCAL_IN链来说,因为nat表中并没有INPUT链,所以对于NF_LOCAL_IN点来说,并不会修改数据包的ip地址,也就是调用alloc_null_binding实现NAT转换,最大的可能就是修改数据包的源端口号,以实现数据连接跟踪项的reply的nf_conntrack_tuple变量是唯一的,且没有被其他连接跟踪项使用。
这也就是为什么需要在NF_LOCAL_IN HOOK点注册HOOK回调函数而又没有在nat表中注册INPUT链的原因。
1.5 公共钩子调用 nf_nat_ipv4_fn()
该函数主要功能就是实现数据的NAT操作(包括SNAT与DNAT),具体来说,就是对一个数据流对应的连接跟踪项仅执行一次SNAT、DNAT,而当数据流对应的连接跟踪项的NAT操作执行完成以后,对于后续的数据包,则直接根据连接跟踪项的 reply 方向的 nf_conntrac_tuple 变量的值进行NAT转换,然后将数据再交给协议栈处理。核心流程如下:
- 首先判断数据包是否符合要求(必须不是分段的),数据包对应的连接跟踪项是否符合转换要求等
- 对于期望连接来说,对于icmp报文,需要对报文进行NAT转换
- 只对new状态的且未进行NAT转换的连接跟踪项,调用nf_nat_rule_find进行连接跟踪项的NAT转换
- 进行了上述3的操作后,则会调用 nf_nat_packet() 对数据包进行NAT转换操作。
注意:连接跟踪项的NAT转换只会发生在连接跟踪项刚被创建且还没有进行confirm时,且每个NAT类型的连接跟踪项只会执行一次NAT转换。
/* SRC manip occurs POST_ROUTING or LOCAL_IN */
#define HOOK2MANIP(hooknum) ((hooknum) != NF_INET_POST_ROUTING &&
(hooknum) != NF_INET_LOCAL_IN)
static unsigned int
nf_nat_ipv4_fn(unsigned int hooknum,
struct sk_buff *skb,
const struct net_device *in,
const struct net_device *out,
int (*okfn)(struct sk_buff *))
{
struct nf_conn *ct;
enum ip_conntrack_info ctinfo;
struct nf_conn_nat *nat;
/* maniptype == SRC for postrouting. */
enum nf_nat_manip_type maniptype = HOOK2MANIP(hooknum);
/* We never see fragments: conntrack defrags on pre-routing
* and local-out, and nf_nat_out protects post-routing.
*/
NF_CT_ASSERT(!ip_is_fragment(ip_hdr(skb)));
ct = nf_ct_get(skb, &ctinfo);
/* Can't track? It's not due to stress, or conntrack would
* have dropped it. Hence it's the user's responsibilty to
* packet filter it out, or implement conntrack/NAT for that
* protocol. 8) --RR
*/
if (!ct)
return NF_ACCEPT;
/* Don't try to NAT if this packet is not conntracked */
//对于连接跟踪项为nf_conntrack_untracked,则说明不对该数据包进行连接跟踪,此时直接返回ACCEPT
if (nf_ct_is_untracked(ct))
return NF_ACCEPT;
nat = nfct_nat(ct);
if (!nat) {
/* NAT module was loaded late. */
if (nf_ct_is_confirmed(ct))
return NF_ACCEPT;
nat = nf_ct_ext_add(ct, NF_CT_EXT_NAT, GFP_ATOMIC);
if (nat == NULL) {
pr_debug("failed to add NAT extensionn");
return NF_ACCEPT;
}
}
/*
1、对于期望连接original与reply方向的数据包,对于icmp协议的数据包,进行nat操作;
2、对于期望连接、及非期望连接的NEW状态下的连接跟踪项,只有连接跟踪项的NAT操作没有进行
的情况下才进行NAT转换操作,具体通过nf_nat_rule_find,查找iptables的nat表中有没有匹配该数据流的NAT规则,若有则根据
NAT类型,调用相应的target进行NAT操作(SNAT target 、DNAT target)
*/
switch (ctinfo) {
case IP_CT_RELATED:
case IP_CT_RELATED_REPLY:
if (ip_hdr(skb)->protocol == IPPROTO_ICMP) {
if (!nf_nat_icmp_reply_translation(skb, ct, ctinfo,
hooknum))
return NF_DROP;
else
return NF_ACCEPT;
}
/* Fall thru... (Only ICMPs can be IP_CT_IS_REPLY) */
case IP_CT_NEW:
/* Seen it before? This can happen for loopback, retrans,
* or local packets.
*/
if (!nf_nat_initialized(ct, maniptype)) {
unsigned int ret;
ret = nf_nat_rule_find(skb, hooknum, in, out, ct);
if (ret != NF_ACCEPT)
return ret;
} else {
pr_debug("Already setup manip %s for ct %pn",
maniptype == NF_NAT_MANIP_SRC ? "SRC" : "DST",
ct);
if (nf_nat_oif_changed(hooknum, ctinfo, nat, out))
goto oif_changed;
}
break;
default:
/* ESTABLISHED */
NF_CT_ASSERT(ctinfo == IP_CT_ESTABLISHED ||
ctinfo == IP_CT_ESTABLISHED_REPLY);
if (nf_nat_oif_changed(hooknum, ctinfo, nat, out))
goto oif_changed;
}
//调用nf_nat_packet,根据连接跟踪项的reply tuple变量实现对数据包的NAT操作
return nf_nat_packet(ct, ctinfo, hooknum, skb);
oif_changed:
nf_ct_kill_acct(ct, ctinfo, skb);
return NF_DROP;
}
1.5.1 NAT转换判断 nf_nat_initialized()
这个函数主要是判断传递的连接跟踪项,有没有进行过manip类型的NAT转换。
若 manip 的值为 IP_NAT_MANIP_SRC,则判断连接跟踪项的 status 的 IPS_SRC_NAT_DONE_BIT 位是否为 1,若为 1,则说明该连接跟踪项已经进行了 SNAT 转换,不需要再次转换;对于 DNAT 的判断与上述 SNAT 的判断类似。根据这个函数,就可以避免多次对一个连接跟踪项进行SNAT或者DNAT操作。
enum nf_nat_manip_type {
NF_NAT_MANIP_SRC,
NF_NAT_MANIP_DST
};
static inline int nf_nat_initialized(struct nf_conn *ct,
enum nf_nat_manip_type manip)
{
if (manip == NF_NAT_MANIP_SRC)
return ct->status & IPS_SRC_NAT_DONE;
else
return ct->status & IPS_DST_NAT_DONE;
}
1.5.2 NAT 转换操作 nf_nat_rule_find()
功能:实现对数据包关联的连接跟踪项的NAT转换操作。
1.调用 ipt_do_table,查找 nat 表中有没有匹配该连接跟踪项的 nat 规则,若有则根据 NAT 类型调用相应的 target 实现对连接跟踪项的NAT操作(SNAT target 、DNAT target),且将该连接跟踪项的status值中设置已进行NAT转换标志(关于ipt_do_table函数的分析,参见 《linux内核协议栈 netfilter 之 ip 层的 filter 表注册及规则的添加以及钩子函数》)。
2.在调用完ipt_do_table后,该连接跟踪项还没有进行NAT转换,则调用alloc_null_binding进行NAT转换。alloc_null_binding 并不会修改连接跟踪项的reply方向的tupl 、e变量的三层 ip 地址,只有在该连接跟踪项使用的tuple变量值不唯一时,则更新连接跟踪项的reply方向的tuple变量的四层协议相关的关键字(也就是端口号之类的)即可。
static unsigned int nf_nat_rule_find(struct sk_buff *skb, unsigned int hooknum,
const struct net_device *in,
const struct net_device *out,
struct nf_conn *ct)
{
struct net *net = nf_ct_net(ct);
unsigned int ret;
ret = ipt_do_table(skb, hooknum, in, out, net->ipv4.nat_table);
if (ret == NF_ACCEPT) {
if (!nf_nat_initialized(ct, HOOK2MANIP(hooknum)))
ret = alloc_null_binding(ct, hooknum);
}
return ret;
}
1.5.3 数据包进行 NAT 转换 nf_nat_packet()
当一个连接跟踪项已经被NAT转换后,后续的数据包则直接进入函数nf_nat_packet,对数据包中的ip地址、端口等进行NAT转换操作。
当一个连接跟踪项刚被被NAT转换后,则其第一个数据包也要接进入函数nf_nat_packet,对数据包中的ip地址、端口等进行NAT转换操作。
代码具体实现如下:
- 当为SNAT操作,且是reply方向的PREROUTING时,经过下面的异或后同样可以调用manip_pkt,而因为此时为DNAT,因此就实现了De-SNAT;
- 当为DNAT操作,且是reply方向的PREROUTING时,经过下面的异或后同样可以调用manip_pkt,而因为此时为SNAT因此就实现了De-DNAT;
- 当为SNAT操作,且是original方向的POSTROUTING时,则调用manip_pkt执行SNAT操作;
- 当为DNAT操作,且是original方向的PREROUTING/OUTPUT时,则调用manip_pkt执行DNAT操作。
/* Do packet manipulations according to nf_nat_setup_info. */
unsigned int nf_nat_packet(struct nf_conn *ct,
enum ip_conntrack_info ctinfo,
unsigned int hooknum,
struct sk_buff *skb)
{
const struct nf_nat_l3proto *l3proto;
const struct nf_nat_l4proto *l4proto;
enum ip_conntrack_dir dir = CTINFO2DIR(ctinfo);
unsigned long statusbit;
enum nf_nat_manip_type mtype = HOOK2MANIP(hooknum);
//根据hook点设置statusbit的值
if (mtype == NF_NAT_MANIP_SRC)
statusbit = IPS_SRC_NAT;
else
statusbit = IPS_DST_NAT;
/* Invert if this is reply dir. */
//对于reply方向,需要执行异或操作
if (dir == IP_CT_DIR_REPLY)
statusbit ^= IPS_NAT_MASK;
/* Non-atomic: these bits don't change. */
//当连接跟踪项的status变量与statusbit进行位与的结果不为0时:
//调用函数manip_pkt根据NAT类型修改数据包的ip地址。
if (ct->status & statusbit) {
struct nf_conntrack_tuple target;
/* We are aiming to look like inverse of other direction. */
nf_ct_invert_tuplepr(&target, &ct->tuplehash[!dir].tuple);
l3proto = __nf_nat_l3proto_find(target.src.l3num);
l4proto = __nf_nat_l4proto_find(target.src.l3num,
target.dst.protonum);
if (!l3proto->manip_pkt(skb, 0, l4proto, &target, mtype))
return NF_DROP;
}
return NF_ACCEPT;
}
1.6 连接跟踪的地址转换 nf_nat_setup_info()【核心】
这个函数会对4个hook点进来的连接跟踪项进行NAT转换,所以这个函数至此SNAT、DNAT转换,根据HOOK点的类型能够决定转换的类型。该函数最精髓的地方就是调用函数get_unique_tuple,获取一个唯一的且未被其他已经进行NAT转换的连接跟踪项使用的nf_conntrack_tuple变量。当转换成功后,置标记位。
该函数执行的步骤如下:
1.判断传入的hook点是否是NAT相关的hook点,NAT只在PRE_ROUTING、POST_ROUTING、LOCAL_OUT、LOCAL_IN这四个hook点起作用
2.若此时连接跟踪项的status变量中的 IPS_SRC_NAT_DONE_BIT或者IPS_DST_NAT_DONE_BIT位已经被置位了,则打印bug信息,并调用kernel panic
3.根据reply方向的nf_conntrack_tuple结构的变量,获取其反方向的nf_conntrack_tuple结构的变量
4. 调用get_unique_tuple,根据传递的tuple变量,获取一个新的且经过NAT转换的tuple变量,其方向依然是原始方向
5.当新的tuple变量的值与当前的原始方向的tuple变量的值不相等时,进行NAT转换(因为只有在两个值不同时才需要NAT操作):
- a) 对传递过来的新的 tuple 变量的值,调用 get_unique_tuple,获取该 tuple 变量反方向的 tuple 变量值,即新的 reply 方向的值
- b) 调用 nf_conntrack_alter_reply 将连接跟踪项的reply方向的 tuplehash[IP_CT_DIR_REPLY].tuple替换为 a)中得到的reply方向的tuple变量,当连接跟踪项不是期望连接项,且还没有创建期望连接时,对根据新的reply反向的tuple变量,在helpers链表中查找新的符合要求的helper变量,并替换调用连接跟踪项中的原来的nf_conntrack_helper变量
- c) 根据连接跟踪项的NAT类型,设置连接跟踪项的status中相应位(IPS_SRC_NAT/IPS_DST_NAT)
6.若连接跟踪项的当前status变量的IPS_DST_NAT_DONE 与 IPS_SRC_NAT_DONE位均没有置位,则需要将经过NAT操作后的连接跟踪项添加到bysource[]相应的链表中去(调用hash_by_src根据传入的原始方向的tuple变量计算hash值,根据该hash值获取相应的链表bysourece[hash])
7. 根据NAT类型,将连接跟踪项的status变量的IPS_DST_NAT_DONE或者IPS_SRC_NAT_DONE位置位。
unsigned int
nf_nat_setup_info(struct nf_conn *ct,
const struct nf_nat_range *range,
enum nf_nat_manip_type maniptype)
{
struct net *net = nf_ct_net(ct);
struct nf_conntrack_tuple curr_tuple, new_tuple;
struct nf_conn_nat *nat;
/* nat helper or nfctnetlink also setup binding */
nat = nfct_nat(ct);
if (!nat) {
nat = nf_ct_ext_add(ct, NF_CT_EXT_NAT, GFP_ATOMIC);
if (nat == NULL) {
pr_debug("failed to add NAT extensionn");
return NF_ACCEPT;
}
}
NF_CT_ASSERT(maniptype == NF_NAT_MANIP_SRC ||
maniptype == NF_NAT_MANIP_DST);
BUG_ON(nf_nat_initialized(ct, maniptype));
/* What we've got will look like inverse of reply. Normally
* this is what is in the conntrack, except for prior
* manipulations (future optimization: if num_manips == 0,
* orig_tp = ct->tuplehash[IP_CT_DIR_ORIGINAL].tuple)
*/
//根据输入的nf_conntrack_tuple变量,获取其反方向的nf_conntrack_tuple变量。
nf_ct_invert_tuplepr(&curr_tuple,
&ct->tuplehash[IP_CT_DIR_REPLY].tuple);
//该函数根据传递的 curr_tuple 与range变量,得到一个新的 new_tuple,
//此new_tuple的ip地址或者端口号已经进行了NAT转换。
get_unique_tuple(&new_tuple, &curr_tuple, range, ct, maniptype);
if (!nf_ct_tuple_equal(&new_tuple, &curr_tuple)) {
struct nf_conntrack_tuple reply;
/* Alter conntrack table so will recognize replies. */
nf_ct_invert_tuplepr(&reply, &new_tuple);
nf_conntrack_alter_reply(ct, &reply);
/* Non-atomic: we own this at the moment. */
if (maniptype == NF_NAT_MANIP_SRC)
ct->status |= IPS_SRC_NAT;
else
ct->status |= IPS_DST_NAT;
}
if (maniptype == NF_NAT_MANIP_SRC) {
unsigned int srchash;
srchash = hash_by_src(net, nf_ct_zone(ct),
&ct->tuplehash[IP_CT_DIR_ORIGINAL].tuple);
spin_lock_bh(&nf_nat_lock);
/* nf_conntrack_alter_reply might re-allocate extension aera */
nat = nfct_nat(ct);
nat->ct = ct;
hlist_add_head_rcu(&nat->bysource,
&net->ct.nat_bysource[srchash]);
spin_unlock_bh(&nf_nat_lock);
}
/* It's done. */
//在函数的最后有个置位IPS_DST_NAT_DONE_BIT、IPS_SRC_NAT_DONE_BIT的操作,
//这就是为了保证一个数据连接跟踪项在某一个NAT转换类型(SNAT、DNAT)上只能初始化一次。
if (maniptype == NF_NAT_MANIP_DST)
ct->status |= IPS_DST_NAT_DONE;
else
ct->status |= IPS_SRC_NAT_DONE;
return NF_ACCEPT;
}
EXPORT_SYMBOL(nf_nat_setup_info);
2 target 函数
2.1 SNAT 功能
1.调用nf_ct_get,获取传入数据包关联的nf_conn变量
2.此处进行SNAT只是设置连接跟踪项中的reply方向的nf_conntrack_tuple变量,因此:
- 对于主连接,仅设置连接跟踪项的状态为NEW的SNAT操作,因为对于状态不为NEW的连接跟踪项,其reply方向的nf_conntrack_tuple结构的变量的目的地址和端口号已经修改过了,不需要再次修改了;
- 对于期望连接来说,当期望连接刚建立时,其状态仅为IP_CT_RELATED或者IP_CT_RELATED+IP_CT_IS_REPLY,所以也只对这两种情况的期望连接,进行SNAT操作。
3.调用nf_nat_setup_info,根据targinfo中的地址范围与端口值修改连接跟踪项的reply方向的 nf_conntrack_tuple 变量中的值。
执行这个target只是修改了数据包对应的连接跟踪项的reply方向的tuple变量,并没有修改数据包的ip地址,而修改数据包的ip地址是nat模块的hook函数中执行的(在执行了target操作后才会执行,调用函数nf_nat_packet实现)。
//revision = 0,
static unsigned int
xt_snat_target_v0(struct sk_buff *skb, const struct xt_action_param *par)
{
const struct nf_nat_ipv4_multi_range_compat *mr = par->targinfo;
struct nf_nat_range range;
enum ip_conntrack_info ctinfo;
struct nf_conn *ct;
ct = nf_ct_get(skb, &ctinfo);
NF_CT_ASSERT(ct != NULL &&
(ctinfo == IP_CT_NEW || ctinfo == IP_CT_RELATED ||
ctinfo == IP_CT_RELATED_REPLY));
xt_nat_convert_range(&range, &mr->range[0]);
return nf_nat_setup_info(ct, &range, NF_NAT_MANIP_SRC);
}
//revision = 1,
static unsigned int
xt_snat_target_v1(struct sk_buff *skb, const struct xt_action_param *par)
{
const struct nf_nat_range *range = par->targinfo;
enum ip_conntrack_info ctinfo;
struct nf_conn *ct;
ct = nf_ct_get(skb, &ctinfo);
NF_CT_ASSERT(ct != NULL &&
(ctinfo == IP_CT_NEW || ctinfo == IP_CT_RELATED ||
ctinfo == IP_CT_RELATED_REPLY));
return nf_nat_setup_info(ct, range, NF_NAT_MANIP_SRC);
}
2.2 DNAT 功能
1.调用nf_ct_get,获取传入数据包关联的nf_conn变量
2.此处进行DNAT只是设置连接跟踪项中的reply方向的nf_conntrack_tuple变量,因此:
- 对于主连接,仅设置连接跟踪项的状态为NEW的DNAT操作,因为对于状态不为NEW的连接跟踪项,其reply方向的nf_conntrack_tuple结构的变量的目的地址和端口号已经修改过了,不需要再次修改了;
- 对于期望连接来说,当期望连接刚建立时,其状态仅为IP_CT_RELATED,才进行DNAT操作。
3.调用nf_nat_setup_info,根据targinfo中的地址范围与端口值修改连接跟踪项的reply方向的nf_conntrack_tuple变量中的值。
执行这个target只是修改了数据包对应的连接跟踪项的reply方向的tuple变量,并没有修改数据包的ip地址,而修改数据包的ip地址是nat模块的hook函数中执行的(在执行了target操作后才会执行)。
//revision = 0
static unsigned int
xt_dnat_target_v0(struct sk_buff *skb, const struct xt_action_param *par)
{
const struct nf_nat_ipv4_multi_range_compat *mr = par->targinfo;
struct nf_nat_range range;
enum ip_conntrack_info ctinfo;
struct nf_conn *ct;
ct = nf_ct_get(skb, &ctinfo);
NF_CT_ASSERT(ct != NULL &&
(ctinfo == IP_CT_NEW || ctinfo == IP_CT_RELATED));
xt_nat_convert_range(&range, &mr->range[0]);
return nf_nat_setup_info(ct, &range, NF_NAT_MANIP_DST);
}
//revision = 1
static unsigned int
xt_dnat_target_v1(struct sk_buff *skb, const struct xt_action_param *par)
{
const struct nf_nat_range *range = par->targinfo;
enum ip_conntrack_info ctinfo;
struct nf_conn *ct;
ct = nf_ct_get(skb, &ctinfo);
NF_CT_ASSERT(ct != NULL &&
(ctinfo == IP_CT_NEW || ctinfo == IP_CT_RELATED));
return nf_nat_setup_info(ct, range, NF_NAT_MANIP_DST);
}
3 实例分析
对于nat相关的函数,我们都分析完了,那我们就分别以SNAT与DNAT两种情况,来分析下数据包在网关中是如何实现地址转换的。
3.1 SNAT
这个就是典型的路由器工作机制,路由器的lan侧设备需要访问到互联网,而又只有路由器上的wan连接存在一个公网地址,此时lan侧pc发来的数据就需要进行SNAT转换。
3.1.1 环境说明
lan1 pc ip:192.168.1.123
route wan ip为115.22.112.12
需要访问的外网的地址为ip 14.17.88.99
网关通过iptables做了SNAT,命令如下:
iptables -t nat -A POSTROUTING -s 192.168.1.123/32 -o wan0 -j SNAT --to-source 115.22.112.12
3.1.2 数据SNAT转换分析
当第一个lan侧数据进入到路由器的wan接口时,在 PRE_ROUTING 创建一个nf_conn和两个nf_conntrack_tuple(origin 与reply)。其中origin tuple.src=192.168.1.123 origin tuple.dst=14.17.88.99; reply tuple.src=14.17.88.99 reply tuple.dst=192.168.1.3。
当查找路由成功,要转发该数据包时,进入到 POST_ROUTING 链时,进入到NAT的hook函数时,查看到有SNAT的规则,经过SNAT后,会将tuple里的值修改如下:其中origin tuple.src=192.168.1.123 origin tuple.dst=14.17.88.99; reply tuple.src=14.17.88.99 reply tuple.dst=115.22.112.12。
当服务器14.17.88.99回复了一个数据包后(src=14.17.88.99 dst=115.22.112.12),进入到wan侧接口的PRE_ROUTING链时,则在调用其nat相关的hook函数后,会调用函数ip_nat_packet获取到origin tuple值,然后再根据origin tuple,计算出反方向的tuple,即为new_tuple.src = 14.17.88.99 new_tuple.dst = 192.168.1.123,然后就会根据这个新的 tuple 修改其目的ip地址,修改后的数据包的目的地址即为192.168.1.123 。然后再查找路由,将数据发送到正常的lan口。这就是 nat 的 De-SNAT(反向SNAT)
3.2 DNAT
即路由器的lan侧设备中,有一个设备要作为server使用,这时候就需要使用dnat了。
3.2.1 环境说明
lan1 pc ip:192.168.1.183
route wan ip为115.22.123.12(外网看到的server的ip地址)
外网的地址为ip 14.17.88.22
iptables -t nat -A PREROUTING -i wan0 -j DNAT --to-destination 192.168.1.183
3.2.2 数据的DNAT分析
当外网client发送一个到server的请求数据。(其src ip 14.17.88.22 dst 115.22.123.12)
当数据到达路由器的wan0口,进入到PRE_ROUTING时,会先建立一个nf_conn结构,和两个nf_conntrack_tuple(origin 与reply)其中origin tuple.src=14.17.88.22 origin tuple.dst=115.22.123.12 ;reply tuple.src=115.22.123.12 reply tuple.dst=14.17.88.22;然后又会进入到 PRE_ROUTING 的hook点的nat hook中,然后调用nat hook,查找nat表的DNAT规则,刚好找到了我们上面创建的规则,接着就会修改reply tuple。将reply tuple.src=115.22.123.12 reply tuple.dst=14.17.88.22修改为reply tuple.src=192.168.1.183 reply tuple.dst=14.17.88.22,然后再根据修改后的reply tuple,取反获取到新的tuple,即new_tuple.src=14.17.88.22,new_tuple.dst=192.168.1.183。然后就会根据这个tuple值将数据包的目的地址修改为192.168.1.183,接着查找路由,将数据包发送给lan侧server。
当lan侧发送一个回应的报文时(数据包的src为192.168.1.183 dst为14.17.88.22),然后当数据进入wan0的PRE_ROUTING链时,由于查找到的nf_conn没有SNAT标志,则会继续查找路由,然后forward这个数据包;当数据包到达POST_ROUTING时,根据nf_conn的flag置位为DNAT,且为reply方向,就会查找origin tuple,然后根据origin tuple的值,取反得到新的tuple:new_tuple.src=115.22.123.12, new_tuple.dst=14.17.88.22,然后根据这个新的tuple,修改数据包的src地址,修改后的数据包的地址为src=115.22.123.12,dst=14.17.88.22,这就是nat的De-DNAT功能。
4 nat表配置演练(iptables )
4.1 配置转发设备
配置一个转发设备或者说是重定,将tcp的客户端和服务器彼此隐藏起来,互相看不见对方。
=========================tcp_client 10.228.90.11==================================
[root@localhost tcp]# ./tcp_client 10.228.90.11 10.228.90.4 22222
bind succ and connect start
connect succ and send start
recv=data ack
recv=data ack
^C
[root@localhost tcp]#
=========================10.228.90.4转发配置======================================
//1、将客户端发过来的包转发到真实服务器(10.228.90.12:11111)地址上,隐藏服务器信息
[root@Node_B ~]# iptables -t nat -A PREROUTING -d 10.228.90.4 -p tcp --dport 22222 -j DNAT --to-destination 10.228.90.12:11111
//2、本地向服务器传输的报文,替换源地址信息,即替换真实客户端的地址,影藏客户端信息
[root@Node_B ~]# iptables -t nat -A POSTROUTING -d 10.228.90.12 -p tcp --dport 11111 -j SNAT --to-source 10.228.90.4
//3、转发功能使能
[root@Node_B ~]# echo 1 > /proc/sys/net/ipv4/ip_forward
=========================tcp_server 10.228.90.12==================================
[root@localhost wq]# ./tcp_server 10.228.90.12 11111
wait for clients connect----------
client IP :10.228.90.4 8003//服务器看到客户端就是转发设备
msg_no=1,msg_version=1,msg_len=1,msg_bit1=0,msg_bit2=1,msg_bit3=1,local_id=100,remote_id=111
msg_no=1,msg_version=1,msg_len=1,msg_bit1=0,msg_bit2=1,msg_bit3=1,local_id=100,remote_id=111
recv: Connection reset by peer
[root@localhost wq]#
注意:当转发设备的本地也存在一个客户端向服务器建链,如果此时和待转发客户端使用相同端口时,即tuple产生冲突,此时本地的转发设备会为后启动客户端重新分配一个端口,
[root@localhost ~]# netstat -anp | grep 11111
tcp 0 0 10.228.90.12:11111 0.0.0.0:* LISTEN 21567/./tcp_server
//转发设备先启动本地的客户端
tcp 0 0 10.228.90.12:11111 10.228.90.4:8003 ESTABLISHED 21567/./tcp_server //
//转发设备重定向的客户端后启动
tcp 2000 0 10.228.90.12:11111 10.228.90.4:1024 ESTABLISHED -
[root@localhost ~]#
说明:转发设备先重定向客户端,本地再启动客户端,效果也是一样的,此时本地绑定的8003端口被改写成1024
4.2 本地loopback重定向
本地提供一个对外的 ip 地址 ip_addr1(100.100.100.100:30000),实际监听服务是在监听地址为本地 looback 地址 ,配置信息如下:
[root@localhost ~]# echo 1 > /proc/sys/net/ipv4/conf/eth3/route_localnet
[root@localhost ~]# iptables -t nat -A PREROUTING -p tcp -d 100.100.100.100 --dport 30000 -j DNAT --to-destination 127.0.0.1:30000
[root@localhost ~]#
地址转发并不默认,关键不要忘了网口的 route_localnet 配置(非常非常非常重要),实验如下:
客户端:
[root@localhost ~]# telnet 100.100.100.100 30000
Trying 100.100.100.100...
telnet: connect to address 100.100.100.100: Connection timed out //缺少route_local
[root@localhost ~]# telnet 100.100.100.100 30000
Trying 100.100.100.100...
telnet: connect to address 100.100.100.100: Connection refused //缺少nat转换
[root@localhost ~]# telnet 100.100.100.100 30000
Trying 100.100.100.100...
Connected to 100.100.100.100.//连接成功
Escape character is '^]'.
^C^Z
Connection closed by foreign host.
[root@localhost ~]#
服务器:
[root@localhost ~]# echo 0 > /proc/sys/net/ipv4/conf/eth3/route_localnet
[root@localhost ~]# cat /proc/sys/net/ipv4/conf/eth3/route_localnet
0
[root@localhost ~]# echo 1 > /proc/sys/net/ipv4/conf/eth3/route_localnet
[root@localhost ~]# iptables -t nat -D PREROUTING -p tcp -d 100.100.100.100 --dport 30000 -j DNAT --to-destination 127.0.0.1:30000
[root@localhost ~]# iptables -t nat -A PREROUTING -p tcp -d 100.100.100.100 --dport 30000 -j DNAT --to-destination 127.0.0.1:30000
[root@localhost ~]#
===================================
[root@localhost wq]# ./tcp_server 127.0.0.1 30000
wait for clients connect----------
client IP :100.100.100.101 34490
msg_no=13,msg_version=10,msg_len=48,msg_bit1=2,msg_bit2=3,msg_bit3=1,local_id=774910001,remote_id=774910001
msg_no=255,msg_version=244,msg_len=255,msg_bit1=1,msg_bit2=7,msg_bit3=3,local_id=774909958,remote_id=774910001
msg_no=255,msg_version=237,msg_len=255,msg_bit1=1,msg_bit2=7,msg_bit3=3,local_id=774909958,remote_id=774910001
msg_no=13,msg_version=10,msg_len=255,msg_bit1=1,msg_bit2=7,msg_bit3=3,local_id=774909958,remote_id=774910001
^C
[root@localhost wq]#
===================================
[root@localhost wq]# ifconfig
eth3: flags=4163<UP,BROADCAST,RUNNING,MULTICAST> mtu 1500
inet 100.100.100.100 netmask 255.255.255.0 broadcast 100.100.100.255
inet6 2019::2200:1 prefixlen 112 scopeid 0x0<global>
inet6 fe80::f816:3eff:fee1:f82c prefixlen 64 scopeid 0x20<link>
ether fa:16:3e:e1:f8:2c txqueuelen 1000 (Ethernet)
RX packets 1333 bytes 135792 (132.6 KiB)
RX errors 0 dropped 0 overruns 0 frame 0
TX packets 2090 bytes 341764 (333.7 KiB)
TX errors 0 dropped 0 overruns 0 carrier 0 collisions 0
eth3:10: flags=4163<UP,BROADCAST,RUNNING,MULTICAST> mtu 1500
inet 10.10.10.10 netmask 255.255.255.0 broadcast 10.10.10.255
ether fa:16:3e:e1:f8:2c txqueuelen 1000 (Ethernet)
4.2.1 loopback 接口(火星报文)
根据维基百科上针对Loopback得到相关loopback 接口的定义:在同一台机器上执行时的网络应用程序可以进行通信。它完全在操作系统的网络软件中实现,并且不将任何数据包传递给任何网络接口控制器。计算机程序发送到环回IP地址的任何流量都将被简单地立即传回网络软件堆栈,就像从另一个设备接收到的一样。
另外,源地址或目标地址设置为环回地址的任何IP 数据报都不得出现在计算机系统之外(通俗就是说网络上不允许出现源或目的为127.0.0.1的包),也不得由任何路由设备进行路由。必须删除在网络接口上收到具有环回目的地址的报文。这样的分组有时被称为火星分组。与其他虚假数据包一样,它们可能是恶意的,可以通过应用bogon过滤来避免任何可能导致的问题。
看过定义之后才知道原来这个包文是直接被删除了。根据这个帖子所说,可以修改内核的参数,将eth0(即本例中的外网卡)设置为不删除环回目的地址的报文。
默认是禁止的。内核参数如下:
route_localnet - BOOLEAN
Do not consider loopback addresses as martian source or destination
while routing. This enables the use of 127/8 for local routing purposes.
default FALSE
备注:大致意思是不要将来自或发送到 环回接口上的数据包视为火星报文(这里称这个包为火星报文),既不丢弃它,启用用于127.0.0.1的本地路由。
使用以下命令(针对eth0)进行修改该内核参数的值为TRUE:
# sysctl -w net.ipv4.conf.eth0.route_localnet=1
net.ipv4.conf.eth0.route_localnet = 1
永久保存该设置
# echo "net.ipv4.conf.eth0.route_localnet=1" >> /etc/sysctl.conf
# sysctl -p
4.2.2 route_localnet 路由逻辑 ip_route_input_slow()
低版本内核并没有 route_localnet 网口配置选项,如下代码为 linux 3.10.0
static int ip_route_input_slow(struct sk_buff *skb, __be32 daddr, __be32 saddr,
u8 tos, struct net_device *dev)
{
struct fib_result res;
struct in_device *in_dev = __in_dev_get_rcu(dev);
struct flowi4 fl4;
unsigned int flags = 0;
u32 itag = 0;
struct rtable *rth;
int err = -EINVAL;
struct net *net = dev_net(dev);
bool do_cache;
/* IP on this device is disabled. */
if (!in_dev)
goto out;
/* Check for the most weird martians, which can be not detected
by fib_lookup.
*/
if (ipv4_is_multicast(saddr) || ipv4_is_lbcast(saddr))
goto martian_source;
res.fi = NULL;
if (ipv4_is_lbcast(daddr) || (saddr == 0 && daddr == 0))
goto brd_input;
/* Accept zero addresses only to limited broadcast;
* I even do not know to fix it or not. Waiting for complains :-)
*/
if (ipv4_is_zeronet(saddr))
goto martian_source;
if (ipv4_is_zeronet(daddr))
goto martian_destination;
/* Following code try to avoid calling IN_DEV_NET_ROUTE_LOCALNET(),
* and call it once if daddr or/and saddr are loopback addresses
*/
if (ipv4_is_loopback(daddr)) {
if (!IN_DEV_NET_ROUTE_LOCALNET(in_dev, net))//route_localnet未使能直接丢弃
goto martian_destination;
} else if (ipv4_is_loopback(saddr)) {
if (!IN_DEV_NET_ROUTE_LOCALNET(in_dev, net))
goto martian_source;
}
...
}
以上参见:《为什么不能将客户端的连接请求跳转或转发到本机lo回环接口上?》
4.3 本地地址重定向
本地提供一个对外的 ip 地址 ip_addr1(100.100.100.100:30000),实际监听服务是在监听地址为本地地址 ip_addr2(10.10.10.10:30001),配置信息如下:
[root@localhost ~]# iptables -t nat -A PREROUTING -p tcp -d 100.100.100.100 --dport 30000 -j DNAT --to-destination 10.10.10.10:30001
客户端:
[root@localhost ~]# telnet 100.100.100.100 30000
Trying 100.100.100.100...
telnet: connect to address 100.100.100.100: Connection refused//缺少nat规则
[root@localhost ~]# telnet 100.100.100.100 30000
Trying 100.100.100.100...
Connected to 100.100.100.100.
Escape character is '^]'.
^C
服务器:
[root@localhost wq]# ./tcp_server 10.10.10.10 30001
wait for clients connect----------
client IP :100.100.100.101 34494
msg_no=13,msg_version=10,msg_len=48,msg_bit1=2,msg_bit2=3,msg_bit3=1,local_id=774910001,remote_id=774910001
msg_no=255,msg_version=244,msg_len=255,msg_bit1=1,msg_bit2=7,msg_bit3=3,local_id=774909958,remote_id=774910001
^Z
[1]+ 已停止 ./tcp_server 10.10.10.10 30001
[root@localhost wq]#
最后
以上就是秀丽爆米花为你收集整理的linux内核协议栈 netfilter 之 ip 层 netfilter 的 NAT 模块 hook 及 target 代码剖析1 钩子函数2 target 函数3 实例分析4 nat表配置演练(iptables )的全部内容,希望文章能够帮你解决linux内核协议栈 netfilter 之 ip 层 netfilter 的 NAT 模块 hook 及 target 代码剖析1 钩子函数2 target 函数3 实例分析4 nat表配置演练(iptables )所遇到的程序开发问题。
如果觉得靠谱客网站的内容还不错,欢迎将靠谱客网站推荐给程序员好友。
发表评论 取消回复