网络地址转换(NAT),从本质上来讲,是通过修改IP数据首部中的地址,以实现将一个地址转换成另一个地址的技术。当然,在某些情况下,修改的不仅仅是IP首部的来源或目的地址,还包括其它要素。
随着接入Internet的计算机数量的不断猛增,IP地址资源也就愈加显得捉襟见肘。目前NAT技术更多地被使用在将一个私网IP地址网段,转换为一个或几个公网IP地址,以实现私网与Internet的互相通讯。
Netfilter在连接跟踪的基础上,实现了两种类型的地址转换:源地址转换和目的地址转换。顾名思义,源地址转换就是修改IP包中的源地址(或许还有源端口),而目的地址转换,就是修改IP包中的目的地址(同样,或许还有目的端口)。前者通常用于将内网主机私网地址转换为公网地址,访问Internet,后者通常用于将公网IP地址转换为一个或几个私网地址,实现向互联网提供服务。
int __init ip_nat_rule_init(void)
{
int ret;
/*注册NAT表*/
ret = ipt_register_table(&nat_table, &nat_initial_table.repl);
if (ret != 0)
return ret;
/*注册SNAT Target */
ret = ipt_register_target(&ipt_snat_reg);
if (ret != 0)
goto unregister_table;
/*注册DNAT Target*/
ret = ipt_register_target(&ipt_dnat_reg);
if (ret != 0)
goto unregister_snat;
return ret;
}
这些函数在filter表中已经详细分析过了,读者可以对应结构成员的赋值,自行分析。需要注意的是对SNAT和DNAT两个模块的注册,它们的target处理函数分别是ipt_snat_target和ipt_dnat_target,这个target与包过滤中的target没有质的区别,都是规则的动作部份,只是完成的功能不同罢了——它们的工作是地址转换,而包过滤中的target是拦截、放行之类的。
int __init ip_nat_init(void)
{
size_t i;
/* 设置nat的hash表的大小 */
ip_nat_htable_size = ip_conntrack_htable_size;
/*同连接跟踪一样,nat的hash表也要维护一个list_head结构的hash链表*/
bysource = vmalloc(sizeof(struct list_head) * ip_nat_htable_size);
if (!bysource)
return -ENOMEM;
/* 初始化内建协议 */
WRITE_LOCK(&ip_nat_lock);
for (i = 0; i < MAX_IP_NAT_PROTO; i++)
ip_nat_protos[i] = &ip_nat_unknown_protocol;
ip_nat_protos[IPPROTO_TCP] = &ip_nat_protocol_tcp;
ip_nat_protos[IPPROTO_UDP] = &ip_nat_protocol_udp;
ip_nat_protos[IPPROTO_ICMP] = &ip_nat_protocol_icmp;
WRITE_UNLOCK(&ip_nat_lock);
/*初始化hash表*/
for (i = 0; i < ip_nat_htable_size; i++) {
INIT_LIST_HEAD(&bysource[i]);
}
/* FIXME: Man, this is a hack. */
IP_NF_ASSERT(ip_conntrack_destroyed == NULL);
ip_conntrack_destroyed = &ip_nat_cleanup_conntrack;
/* Initialize fake conntrack so that NAT will skip it */
ip_conntrack_untracked.status |= IPS_NAT_DONE_MASK;
return 0;
}
Hook的注册,我们已经反复地遇到了,NAT也不例外,它需要在各个关键的Hook点上注册自己的Hook,我们仍假设Linux做为一个网关型设备,关心它在PREROUTING和POSTROUTING两个Hook点上注册的Hook,因为它们完成了最重要的源地址转换和目的地址转换:
/* 目的地址转换的Hook,在filter包过滤之前进行 */
static struct nf_hook_ops ip_nat_in_ops = {
.hook = ip_nat_in,
.owner = THIS_MODULE,
.pf = PF_INET,
.hooknum = NF_IP_PRE_ROUTING,
.priority = NF_IP_PRI_NAT_DST,
};
/*源地址转换,在filter包过滤之后*/
static struct nf_hook_ops ip_nat_out_ops = {
.hook = ip_nat_out,
.owner = THIS_MODULE,
.pf = PF_INET,
.hooknum = NF_IP_POST_ROUTING,
.priority = NF_IP_PRI_NAT_SRC,
};
源地址转换注册在NF_IP_POST_ROUTING,数据包在包过滤之后,会进入ip_nat_out函数。在分析这个函数之前,我们需要理解的是,源地址转换如何进行?
源地址的转换最终要做的工作,就是修改IP包中的源地址,将其替换为iptables添加规则时指定的“转换后地址”,对于绝大多数应用而言,一般是将私网IP地址修改为公网IP地址,然后将数据包发送出去。但是,很自然地,这样修改后,回来的应答数据包没有办法知道它转换之前的样子,也就是不知道真实的来源主机(对于回应包,也就是不知道把数据应答给谁),数据包将被丢弃,所以有必要,维护一张地址转换表,详细记录数据包的转换情况,以使NAT后的数据能交互地传输。
对于许多无状态检测功能的NAT技术,这个记录转换情况的表就是一张NAT会话表,对于Netfilter而言,已经为进出数据包建立了一张状态跟踪表,自然也就没有必要重新多维护一张表了,也就是,合理地利用状态跟踪表,实现对NAT状态的跟踪和维护。
回忆一下连接跟踪的情况,当数据包进入连接跟踪后,会建立一个tuple以及相应的replay tuple,而应答的数据包,会查找与之匹配的repaly tuple,——对于源地址转换而言,应答包中的目的地址,将是转换后的地址,而不是真实的地址,所以,为了让应答的数据包能找到对应的replay tuple,很自然地,NAT模块应该修改replaly tuple中的目的地址,以使应答数据包能找到属于自己的replay,如下图。
3. 源地址转换的规则一般是…… -j SNAT –to X.X.X.X,SNAT用规则中预设的转换后地址X.X.X.X,修改连接跟踪表中的replay tuple;
5. 对于回来的数据包,应该能在状态跟踪表中,查找与之对应的replay tuple,也就能顺藤摸瓜地找到原始的tuple中的信息,将应答包中的目的地址改回来,这样,整个数据传送就得以顺利转发了;
ip_nat_out
static unsigned int
ip_nat_out(unsigned int hooknum,
struct sk_buff **pskb,
const struct net_device *in,
const struct net_device *out,
int (*okfn)(struct sk_buff *))
{
/* root is playing with raw sockets. */
if ((*pskb)->len < sizeof(struct iphdr)
|| (*pskb)->nh.iph->ihl * 4 < sizeof(struct iphdr))
return NF_ACCEPT;
if ((*pskb)->nh.iph->frag_off & htons(IP_MF|IP_OFFSET)) {
*pskb = ip_ct_gather_frags(*pskb, IP_DEFRAG_NAT_OUT);
if (!*pskb)
return NF_STOLEN;
}
return ip_nat_fn(hooknum, pskb, in, out, okfn);
}
在进行了IP包的长度较验和分片检查之后,函数进入ip_nat_fn,它是整个地址转换的核心函数之一:
static unsigned int
ip_nat_fn(unsigned int hooknum,
struct sk_buff **pskb,
const struct net_device *in,
const struct net_device *out,
int (*okfn)(struct sk_buff *))
{
struct ip_conntrack *ct;
enum ip_conntrack_info ctinfo;
struct ip_nat_info *info;
/* maniptype通过调用HOOK2MAINIP宏判断Hook点,指明转换类型是源地址转换还是目的地址转换,为0(IP_NAT_MANIP_SRC)表示源地址转换,为1(IP_NAT_MANIP_DST)表示目的地址转换 */
enum ip_nat_manip_type maniptype = HOOK2MANIP(hooknum);
/* 前面函数中已经处理过分片的情况,这里应该不会再出现分片包了. */
IP_NF_ASSERT(!((*pskb)->nh.iph->frag_off
& htons(IP_MF|IP_OFFSET)));
/*因为地址转换会修改数据包,所以这里先初始化将其设置为“未修改”标志,后面进行数据包修改时再来重置这个标志*/
(*pskb)->nfcache |= NFC_UNKNOWN;
/* If we had a hardware checksum before, it's now invalid */
if ((*pskb)->ip_summed == CHECKSUM_HW)
if (skb_checksum_help(*pskb, (out == NULL)))
return NF_DROP;
/*取得数据包的连接状态*/
ct = ip_conntrack_get(*pskb, &ctinfo);
/* 如果找不到对应连接,则应该直接放行它,而不再对其进行转换处理,特别地,ICMP重定向报文将会被丢弃*/
if (!ct) {
/* Exception: ICMP redirect to new connection (not in
hash table yet). We must not let this through, in
case we're doing NAT to the same network. */
if ((*pskb)->nh.iph->protocol == IPPROTO_ICMP) {
struct icmphdr _hdr, *hp;
hp = skb_header_pointer(*pskb,
(*pskb)->nh.iph->ihl*4,
sizeof(_hdr), &_hdr);
if (hp != NULL &&
hp->type == ICMP_REDIRECT)
return NF_DROP;
}
return NF_ACCEPT;
}
/*判断连接状态,调用相应的处理函数*/
switch (ctinfo) {
case IP_CT_RELATED:
case IP_CT_RELATED+IP_CT_IS_REPLY:
if ((*pskb)->nh.iph->protocol == IPPROTO_ICMP) {
if (!icmp_reply_translation(pskb, ct, maniptype,
CTINFO2DIR(ctinfo)))
return NF_DROP;
else
return NF_ACCEPT;
}
/* Fall thru... (Only ICMPs can be IP_CT_IS_REPLY) */
case IP_CT_NEW:
info = &ct->nat.info;
/* 观察这个新建封包是否已经被NAT模块修改过了,如果没有,进一步调用ip_nat_rule_find函数*/
if (!ip_nat_initialized(ct, maniptype)) {
unsigned int ret;
/* LOCAL_IN hook doesn't have a chain! */
if (hooknum == NF_IP_LOCAL_IN)
ret = alloc_null_binding(ct, info, hooknum);
else
ret = ip_nat_rule_find(pskb, hooknum,
in, out, ct,
info);
if (ret != NF_ACCEPT) {
return ret;
}
} else
DEBUGP("Already setup manip %s for ct %p\n",
maniptype == IP_NAT_MANIP_SRC ? "SRC" : "DST",
ct);
break;
default:
/* ESTABLISHED */
IP_NF_ASSERT(ctinfo == IP_CT_ESTABLISHED
|| ctinfo == (IP_CT_ESTABLISHED+IP_CT_IS_REPLY));
info = &ct->nat.info;
}
IP_NF_ASSERT(info);
return nat_packet(ct, ctinfo, hooknum, pskb);
}
我们假设这是一个刚刚进入Linux的数据包,它是一个新建状态的连接,首先调用ip_nat_initialized函数判断它是否已经被地址转换例程修改过,即是否已经设置了相应转换类型的标志位:
static inline int ip_nat_initialized(struct ip_conntrack *conntrack,
enum ip_nat_manip_type manip)
{
/*如果是源地址转换,即测试源地址转换位,否则,测试目的地址转换位*/
if (manip == IP_NAT_MANIP_SRC)
return test_bit(IPS_SRC_NAT_DONE_BIT, &conntrack->status);
return test_bit(IPS_DST_NAT_DONE_BIT, &conntrack->status);
}
对于一个中转没有被修改过的包,ip_nat_rule_find函数将会被调用,以作进一步处理。
ip_nat_rule_findip_nat_rule_find 函数,从名称上我们就可以看出,它的含义是“NAT 规则查找”,它是一个规则匹配函数,其最重要的工作,就是调用ipt_do_table函数进行规则的检测,这个函数我们在包过滤一章已经对它进行了详细的分析,当ipt_do_table函数发现数据包匹配一条源地址转换的规则时,则会调用SNAT模块的target函数。
int ip_nat_rule_find(struct sk_buff **pskb,
unsigned int hooknum,
const struct net_device *in,
const struct net_device *out,
struct ip_conntrack *ct,
struct ip_nat_info *info)
{
int ret;
/*NAT规则匹配*/
ret = ipt_do_table(pskb, hooknum, in, out, &nat_table, NULL);
if (ret == NF_ACCEPT) {
if (!ip_nat_initialized(ct, HOOK2MANIP(hooknum)))
/* NUL mapping */
ret = alloc_null_binding(ct, info, hooknum);
}
return ret;
}
前面我们已经讨论过,SNAT注册的的target函数是ipt_snat_target,这个函数,首先取得规则中“转换后地址”的信息,然后将工作交给ip_nat_setup_info进一步处理:
/* Source NAT */
static unsigned int ipt_snat_target(struct sk_buff **pskb,
const struct net_device *in,
const struct net_device *out,
unsigned int hooknum,
const void *targinfo,
void *userinfo)
{
struct ip_conntrack *ct;
enum ip_conntrack_info ctinfo;
/*取得规则中的target部份*/
const struct ip_nat_multi_range_compat *mr = targinfo;
IP_NF_ASSERT(hooknum == NF_IP_POST_ROUTING);
/*取得数据包的连接*/
ct = ip_conntrack_get(*pskb, &ctinfo);
/* Connection must be valid and new. */
IP_NF_ASSERT(ct && (ctinfo == IP_CT_NEW || ctinfo == IP_CT_RELATED
|| ctinfo == IP_CT_RELATED + IP_CT_IS_REPLY));
IP_NF_ASSERT(out);
return ip_nat_setup_info(ct, &mr->range[0], hooknum);
}
ip_nat_setup_infoip_nat_setup_info 函数是地址转换中又一个非常重要的函数:
unsigned int
ip_nat_setup_info(struct ip_conntrack *conntrack, /*数据包对应的连接*/
const struct ip_nat_range range, /*转换后的地址池*/
unsigned int hooknum) /*Hook点*/
{
struct ip_conntrack_tuple curr_tuple, new_tuple;
struct ip_nat_info *info = &conntrack->nat.info;
int have_to_hash = !(conntrack->status & IPS_NAT_DONE_MASK);
enum ip_nat_manip_type maniptype = HOOK2MANIP(hooknum);
IP_NF_ASSERT(hooknum == NF_IP_PRE_ROUTING
|| hooknum == NF_IP_POST_ROUTING
|| hooknum == NF_IP_LOCAL_IN
|| hooknum == NF_IP_LOCAL_OUT);
BUG_ON(ip_nat_initialized(conntrack, 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 =
conntrack->tuplehash[IP_CT_DIR_ORIGINAL].tuple) */
invert_tuplepr(&curr_tuple,
&conntrack->tuplehash[IP_CT_DIR_REPLY].tuple);
get_unique_tuple(&new_tuple, &curr_tuple, range, conntrack, maniptype);
if (!ip_ct_tuple_equal(&new_tuple, &curr_tuple)) {
struct ip_conntrack_tuple reply;
/* Alter conntrack table so will recognize replies. */
invert_tuplepr(&reply, &new_tuple);
ip_conntrack_alter_reply(conntrack, &reply);
/* Non-atomic: we own this at the moment. */
if (maniptype == IP_NAT_MANIP_SRC)
conntrack->status |= IPS_SRC_NAT;
else
conntrack->status |= IPS_DST_NAT;
}
/* Place in source hash if this is the first time. */
if (have_to_hash) {
unsigned int srchash
= hash_by_src(&conntrack->tuplehash[IP_CT_DIR_ORIGINAL]
.tuple);
WRITE_LOCK(&ip_nat_lock);
list_add(&info->bysource, &bysource[srchash]);
WRITE_UNLOCK(&ip_nat_lock);
}
/* It's done. */
if (maniptype == IP_NAT_MANIP_DST)
set_bit(IPS_DST_NAT_DONE_BIT, &conntrack->status);
else
set_bit(IPS_SRC_NAT_DONE_BIT, &conntrack->status);
return NF_ACCEPT;
}
这个函数转换来转换去,让人头大,慢慢抽丝拨茧先。首先调用invert_tuplepr取得一个当前数据包对应的replay tuple,然后对其取反得到一个curr_tuple,接着调用get_unique_tuple函数:
static void
get_unique_tuple(struct ip_conntrack_tuple *tuple,
const struct ip_conntrack_tuple *orig_tuple,
const struct ip_nat_range *range,
struct ip_conntrack *conntrack,
enum ip_nat_manip_type maniptype)
{
struct ip_nat_protocol *proto
= ip_nat_find_proto(orig_tuple->dst.protonum);
/* 1) If this srcip/proto/src-proto-part is currently mapped,
and that same mapping gives a unique tuple within the given
range, use that.
This is only required for source (ie. NAT/masq) mappings.
So far, we don't do local source mappings, so multiple
manips not an issue. */
if (maniptype == IP_NAT_MANIP_SRC) {
if (find_appropriate_src(orig_tuple, tuple, range)) {
DEBUGP("get_unique_tuple: Found current src map\n");
if (!ip_nat_used_tuple(tuple, conntrack))
return;
}
}
/* 2) Select the least-used IP/proto combination in the given
range. */
*tuple = *orig_tuple;
find_best_ips_proto(tuple, range, conntrack, maniptype);
/* 3) The per-protocol part of the manip is made to map into
the range to make a unique tuple. */
/* Only bother mapping if it's not already in range and unique */
if ((!(range->flags & IP_NAT_RANGE_PROTO_SPECIFIED)
|| proto->in_range(tuple, maniptype, &range->min, &range->max))
&& !ip_nat_used_tuple(tuple, conntrack))
return;
/* Last change: get protocol to try to obtain unique tuple. */
proto->unique_tuple(tuple, range, maniptype, conntrack);
}
第一个值得关注的是find_appropriate_src函数,它主要是在一个以bysource为首的链表中进行遍历查找,这是做什么?我们初始化NAT的时候,对bysource进行过处始化,所以,到目前为止,这个链表还是空的,我们暂时跳过对这个函数的讨论,函数接下来执行:
/*将curr_tuple赋值给的new_tuple*/
*tuple = *orig_tuple;
find_best_ips_proto(tuple, range, conntrack, maniptype);
range中包含了规则中转换后地址的信息,find_best_ips_proto函数用转换后地址修改new_tuple:
static void
find_best_ips_proto(struct ip_conntrack_tuple *tuple,
const struct ip_nat_range *range,
const struct ip_conntrack *conntrack,
enum ip_nat_manip_type maniptype)
{
u_int32_t *var_ipp;
/* Host order */
u_int32_t minip, maxip, j;
if (!(range->flags & IP_NAT_RANGE_MAP_IPS))
return;
if (maniptype == IP_NAT_MANIP_SRC)
var_ipp = &tuple->src.ip;
else
var_ipp = &tuple->dst.ip;
/* Fast path: only one choice. */
if (range->min_ip == range->max_ip) {
*var_ipp = range->min_ip;
return;
}
minip = ntohl(range->min_ip);
maxip = ntohl(range->max_ip);
j = jhash_2words(tuple->src.ip, tuple->dst.ip, 0);
*var_ipp = htonl(minip + j % (maxip - minip + 1));
}
如果是源地址转换,var_ipp指针指向tuple中的源地址:
var_ipp = &tuple->src.ip;
然后,用range中的转换后地址替换它:
if (range->min_ip == range->max_ip) {
*var_ipp = range->min_ip;
return;
}
这样,再返回至ip_nat_setup_info函数时,我们已经得到了一个根据规则中转换后地址修改过的new_tuple,接着,一个小判断,以确定确实是被修改过,然后,用它替换连接表中数据包的replay tuple:
if (!ip_ct_tuple_equal(&new_tuple, &curr_tuple)) {
struct ip_conntrack_tuple reply;
/* 根据new tuple取反,得到转换后地址的reply tuple */
invert_tuplepr(&reply, &new_tuple);
/*修改连接跟踪表中的reply tuple*/
ip_conntrack_alter_reply(conntrack, &reply);
/* 设置状态标志位 */
if (maniptype == IP_NAT_MANIP_SRC)
conntrack->status |= IPS_SRC_NAT;
else
conntrack->status |= IPS_DST_NAT;
}
我们忽略了太多的细节,如指定协议后的端口转换、转换后地址为地址池的情况等等,但是,这并不影响我们了解整个NAT的全过程,还是继续我们的例子:一个192.186.0.1至100.100.100.100的WEB访问,被修改为100.100.100.1的连接跟随表被修改的流程如下图所示:
在修改了数据包对应的连接跟踪表后,函数将返回至ip_nat_fn中,在函数的最后一句,调用了nat_packet修改数据包的来源地址:
/* Do packet manipulations according to ip_nat_setup_info. */
unsigned int nat_packet(struct ip_conntrack *ct,
enum ip_conntrack_info ctinfo,
unsigned int hooknum,
struct sk_buff **pskb)
{
enum ip_conntrack_dir dir = CTINFO2DIR(ctinfo);
unsigned long statusbit;
enum ip_nat_manip_type mtype = HOOK2MANIP(hooknum);
/*根据转换类型,设置状态标位位*/
if (mtype == IP_NAT_MANIP_SRC)
statusbit = IPS_SRC_NAT;
else
statusbit = IPS_DST_NAT;
/* 应答的情况,暂时不考虑它 */
if (dir == IP_CT_DIR_REPLY)
statusbit ^= IPS_NAT_MASK;
/* Non-atomic: these bits don't change. */
if (ct->status & statusbit) {
struct ip_conntrack_tuple target;
/* 取得修改后replay tuple,并取反,对于源地址转换,就应该中target中的源地址替换IP包中的源地址 */
invert_tuplepr(&target, &ct->tuplehash[!dir].tuple);
/*根据replay tuple中的地址信息,修改数据包*/
if (!manip_pkt(target.dst.protonum, pskb, 0, &target, mtype))
return NF_DROP;
}
return NF_ACCEPT;
}
数据包的修改工作,是在mainip_pkt中完成的:
static int
manip_pkt(u_int16_t proto,
struct sk_buff **pskb,
unsigned int iphdroff,
const struct ip_conntrack_tuple *target,
enum ip_nat_manip_type maniptype)
{
struct iphdr *iph;
/*修改数据包,置相应标志位*/
(*pskb)->nfcache |= NFC_ALTERED;
if (!skb_ip_make_writable(pskb, iphdroff + sizeof(*iph)))
return 0;
/*取得IP首部*/
iph = (void *)(*pskb)->data + iphdroff;
/* 高层协议部份暂时不考虑 */
if (!ip_nat_find_proto(proto)->manip_pkt(pskb, iphdroff,
target, maniptype))
return 0;
iph = (void *)(*pskb)->data + iphdroff;
/*源地址转换,修改IP包中的来源地址*/
if (maniptype == IP_NAT_MANIP_SRC) {
iph->check = ip_nat_cheat_check(~iph->saddr, target->src.ip,
iph->check);
iph->saddr = target->src.ip;
} else {
iph->check = ip_nat_cheat_check(~iph->daddr, target->dst.ip,
iph->check);
iph->daddr = target->dst.ip;
}
return 1;
}
这样,转换后的数据就被发送出去了。但是,这只是出去的数据包,绝大多数情况下,回来的数据包将再次进入地址转换模块。
应答的包对于应答的数据包,同样会进入ip_nat_fn函数,当判断了该数据包的连接状态,除了ICMP协议的应答需要特需处理外,数据包同样会进入nat_packet函数:
switch (ctinfo) {
……
}
……
return nat_packet(ct, ctinfo, hooknum, pskb);
函数取得对应连接跟踪表的tuple,因为连接表中有转换前的地址信息,所以这里取反,用取反的tuple中的目的地址(即原来的来源地址)修改数据包。这样,整个NAT就建立起来了:
/* Do packet manipulations according to ip_nat_setup_info. */
unsigned int nat_packet(struct ip_conntrack *ct,
enum ip_conntrack_info ctinfo,
unsigned int hooknum,
struct sk_buff **pskb)
{
enum ip_conntrack_dir dir = CTINFO2DIR(ctinfo);
unsigned long statusbit;
enum ip_nat_manip_type mtype = HOOK2MANIP(hooknum);
if (mtype == IP_NAT_MANIP_SRC)
statusbit = IPS_SRC_NAT;
else
statusbit = IPS_DST_NAT;
/* Invert if this is reply dir. */
if (dir == IP_CT_DIR_REPLY)
statusbit ^= IPS_NAT_MASK;
/* Non-atomic: these bits don't change. */
if (ct->status & statusbit) {
struct ip_conntrack_tuple target;
/* We are aiming to look like inverse of other direction. */
invert_tuplepr(&target, &ct->tuplehash[!dir].tuple);
if (!manip_pkt(target.dst.protonum, pskb, 0, &target, mtype))
return NF_DROP;
}
return NF_ACCEPT;
}
目的地址转换的流程和原理与源地址转换是一样的。呵呵。
当然,这只是最基本,最简单的流程,做为一个完整的地址转换,存在一个地址对应多条连接(在查块初始化的时候,遇到过初始化hash表,呵呵,就是拿来做这个的),另外,如九贱在连接跟踪的实现中,叙述过动态协议的相关内容,NAT也要对此做相应的处理等等。