iptables深入解析:nat篇

jopen 4年前

原文出处: linuxDOS 

关于nat,在实际应用中还是很广泛的,snat/dnat/dmz/等等.下面我们就结合代码深入分析下nat的运作.
参考:iptables.1.4.21 kernel 3.8.13
NAT英文全称是“Network Address Translation”

顾名思义,它是一种把内部私有网络地址(IP地址)翻译成合法网络IP地址的技术。因此我们可以认为,NAT在一定程度上,能够有效的解决公网地址不足的问题

分类:
NAT有三种类型:静态NAT(Static NAT)、动态地址NAT(Pooled NAT)、网络地址端口转换NAPT(Port-Level NAT)
其中,网络地址端口转换NAPT(Network Address Port Translation)则是把内部地址映射到外部网络的一个IP地址的不同端口上。它可以将中小型的网络隐藏在一个合法的IP地址后面。NAPT与 动态地址NAT不同,它将内部连接映射到外部网络中的一个单独的IP地址上,同时在该地址上加上一个由NAT设备选定的端口号.NAPT是使用最普遍的一种转换方式
,它又细分为snat和dnat.

(1)源NAT(Source NAT,SNAT):修改数据包的源地址。源NAT改变第一个数据包的来源地址,它永远会在数据包发送到网络之前完成,数据包伪装就是一具SNAT的例子。
(2)目的NAT(Destination NAT,DNAT):修改数据包的目的地址。Destination NAT刚好与SNAT相反,它是改变第一个数据懈的目的地地址,如平衡负载、端口转发和透明代理就是属于DNAT

应用:
NAT主要可以实现以下几个功能:数据包伪装、平衡负载、端口转发和透明代理。

数据伪装: 可以将内网数据包中的地址信息更改成统一的对外地址信息,不让内网主机直接暴露在因特网上,保证内网主机的安全。同时,该功能也常用来实现共享上网。

端口转发: 当内网主机对外提供服务时,由于使用的是内部私有IP地址,外网无法直接访问。因此,需要在网关上进行端口转发,将特定服务的数据包转发给内网主机。

负载平衡: 目的地址转换NAT可以重定向一些服务器的连接到其他随机选定的服务器。

失效终结: 目的地址转换NAT可以用来提供高可靠性的服务。如果一个系统有一台通过路由器访问的关键服务器,一旦路由器检测到该服务器当机,它可以使用目的地址转换NAT透明的把连接转移到一个备份服务器上。

透明代理: NAT可以把连接到因特网的HTTP连接重定向到一个指定的HTTP代理服务器以缓存数据和过滤请求。一些因特网服务提供商就使用这种技术来减少带宽的使用而不用让他们的客户配置他们的浏览器支持代理连接

原理

地址转换
NAT的基本工作原理是,当私有网主机和公共网主机通信的IP包经过NAT网关时,将IP包中的源IP或目的IP在私有IP和NAT的公共IP之间进行转换

要做SNAT的信息包被添加到POSTROUTING链中。要做DNAT的信息包被添加到PREROUTING链中。直接从本地出站的信息包的规则被添加到OUTPUT 链中。

DNAT:若包是被送往PREROUTING链的,并且匹配了规则,则执行DNAT或REDIRECT目标。为了使数据包得到正确路由,必须在路由之前进行DNAT。

路由:内核检查信息包的头信息,尤其是信息包的目的地。

处理本地进程产生的包:对nat表OUTPUT链中的规则实施规则检查,对匹配的包执行目标动作。

SNAT:若包是被送往POSTROUTING链的,并且匹配了规则,则执行SNAT或MASQUERADE目标。系统在决定了数据包的路由之后才执行该链中的规则

但是nat也不是万能的,它也是有缺陷的,解决办法就是nat穿透技术:

其实NAT穿越技术依赖于UPnP协议的支持,也就是说NAT设备必须支持UPnP,支持NAT穿越技术;而网络应用程序一样也需要支持UPnP,支持NAT穿越技术,只不过,这通常都是通过调用相关的NAT Traversal API实现的,window XP默认已经安装了NAT Traversal API,当然网络应用程序要调用它仍然需要进行一些修改,现在的MSN Messenger就支持调用NAT Traversal API. 这里不再详细说明,感兴趣的可以查找资料.

下面看看实际代码部分:
Nat的初始化工作和之前分析的filter几乎一样。Nat的ipv4部分在Iptables_nat.c 、Core部分在nf_nat_core.c,不同的就是表不一样.
这里我们拿snat一个实际例子分析,应用环境如图:

 iptables深入解析:nat篇

很明显,直接lan内pc无法与外网通信,因为保留的ip地址即使外网能收到,但是回复的时候路由也会丢弃.所以需要snat:
#iptables -t nat -A POSTROUTING -s 192.168.1.0/24 -j SNAT –to-source 202.20.65.5
或#iptables -t nat -A POSTROUTING -s 192.168.1.0/24 -j MASQUERADE // 默认会获取wan口地址进行映射.
钩子点POSTROUTING 对应SNAT PREROUTING 对应DNAt(因为会影响以后的路由);还需要说明的是不论prerouting的dnat or postrouting snat都在基本ct的后边,helper和confirm的前面.(DNAT的优先级高于SNAT)
参见:

enum nf_ip_hook_priorities {      NF_IP_PRI_FIRST = INT_MIN,      NF_IP_PRI_CONNTRACK_DEFRAG = -400,      NF_IP_PRI_RAW = -300,      NF_IP_PRI_SELINUX_FIRST = -225,      NF_IP_PRI_CONNTRACK = -200,      NF_IP_PRI_MANGLE = -150,      NF_IP_PRI_NAT_DST = -100,      NF_IP_PRI_FILTER = 0,      NF_IP_PRI_SECURITY = 50,      NF_IP_PRI_NAT_SRC = 100,      NF_IP_PRI_SELINUX_LAST = 225,      NF_IP_PRI_CONNTRACK_HELPER = 300,      NF_IP_PRI_CONNTRACK_CONFIRM = INT_MAX,      NF_IP_PRI_LAST = INT_MAX,  };

对于图中lan–>wan(外网通信)我们梳理下报文的处理流程:
第一个报文:192.168.1.3—>202.20.65.4(协议端口先忽略)
–>nf_conntrack_in(查询ct,没有则建立ct,prerouting)—>先snat处理,然后ipv4_helper处理,最后ipv4_confirm处理.(postrouing)
回复的报文:202.20.65.4—>202.20.65.5
—>nf_conntrack_in,然后de-snat处理(查询到之前建立的ct, prerouting)—->snat处理(postrouting)
下面代码分析,先看snat的处理:
hook函数为nf_nat_ipv4_out它调用了核心处理函数nf_nat_ipv4_fn:

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 */      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 extension\n");              return NF_ACCEPT;          }      }        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 %p\n",                   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;      }        return nf_nat_packet(ct, ctinfo, hooknum, skb);    oif_changed:      nf_ct_kill_acct(ct, ctinfo, skb);      return NF_DROP;  }

它首先根据hooknum获取nat类型:enum nf_nat_manip_type maniptype // postrouting 为snat
获取ct信息.,并创建nat ext信息,由于之前已经建立了ct,所以这里状态为IP_CT_NEW. ct->status未设置.
接着调用nf_nat_rule_find–>ipt_do_tables处理snat rules. 对于rules的处理机制流程我们已经很熟悉了.和filter不一样的就是target的处理,
简单看看iptables对命令的解析的ipt_ip信息:
Ipt_do_table
先是查找五元组的匹配ip_packet_match ipt_entry->ip(struct ipt_ip)
那么上面的命令规则下发的ip信息是什么呢
In_dev :不限
Out_dev:不限
Src_ip:192.168.1.0/24
Dst_ip:不限
Protonum:不限
Sport:不限
Dport:不限
五元组匹配后,找到nat的target,解析 –to-source 202.20.65.5.

static struct xtables_target snat_tg_reg = {      .name        = "SNAT",      .version    = XTABLES_VERSION,      .family        = NFPROTO_IPV4,      .size        = XT_ALIGN(sizeof(struct nf_nat_ipv4_multi_range_compat)),      .userspacesize    = XT_ALIGN(sizeof(struct nf_nat_ipv4_multi_range_compat)),      .help        = SNAT_help,      .x6_parse    = SNAT_parse,      .x6_fcheck    = SNAT_fcheck,      .print        = SNAT_print,      .save        = SNAT_save,      .x6_options    = SNAT_opts,  };

先把ip地址信息放在struct nf_nat_ipv4_range range中,然后和struct ipt_natinfo *info = (void *)(*cb->target)关联其实就是填充.

/* Dest NAT data consists of a multi-range, indicating where to map     to. */  struct ipt_natinfo  {      struct xt_entry_target t;      struct nf_nat_ipv4_multi_range_compat mr;  };

我们看看内核的nat target是如何处理的:由于是单ip参数,所以是v1

{          .name        = "SNAT",          .revision    = 1,          .target        = xt_snat_target_v1,          .targetsize    = sizeof(struct nf_nat_range),          .table        = "nat",          .hooks        = (1 << NF_INET_POST_ROUTING) |                   (1 << NF_INET_LOCAL_IN),          .me        = THIS_MODULE,      },
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);  }

其实nat的处理就是nf_nat_setup_info包括dnat也是.
先是获取curr_tuple,调用get_unique_tuple根据curr_tuple和range信息建立新的tuple即new_tuple

get_unique_tuple(&new_tuple, &curr_tuple, range, ct, maniptype);

 

/* Manipulate the tuple into the range given. For NF_INET_POST_ROUTING,   * we change the source to map into the range. For NF_INET_PRE_ROUTING   * and NF_INET_LOCAL_OUT, we change the destination to map into the   * range. It might not be possible to get a unique tuple, but we try.   * At worst (or if we race), we will end up with a final duplicate in   * __ip_conntrack_confirm and drop the packet. */  static void  get_unique_tuple(struct nf_conntrack_tuple *tuple,           const struct nf_conntrack_tuple *orig_tuple,           const struct nf_nat_range *range,           struct nf_conn *ct,           enum nf_nat_manip_type maniptype)  {      const struct nf_nat_l3proto *l3proto;      const struct nf_nat_l4proto *l4proto;      struct net *net = nf_ct_net(ct);      u16 zone = nf_ct_zone(ct);        rcu_read_lock();      l3proto = __nf_nat_l3proto_find(orig_tuple->src.l3num);      l4proto = __nf_nat_l4proto_find(orig_tuple->src.l3num,                      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 == NF_NAT_MANIP_SRC &&       !(range->flags & NF_NAT_RANGE_PROTO_RANDOM)) {          /* try the original tuple first */          if (in_range(l3proto, l4proto, orig_tuple, range)) {              if (!nf_nat_used_tuple(orig_tuple, ct)) {                  *tuple = *orig_tuple;                  goto out;              }          } else if (find_appropriate_src(net, zone, l3proto, l4proto,                          orig_tuple, tuple, range)) {              pr_debug("get_unique_tuple: Found current src map\n");              if (!nf_nat_used_tuple(tuple, ct))                  goto out;          }      }        /* 2) Select the least-used IP/proto combination in the given range */      *tuple = *orig_tuple;      find_best_ips_proto(zone, tuple, range, ct, 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 & NF_NAT_RANGE_PROTO_RANDOM)) {          if (range->flags & NF_NAT_RANGE_PROTO_SPECIFIED) {              if (l4proto->in_range(tuple, maniptype,                       &range->min_proto,                       &range->max_proto) &&               (range->min_proto.all == range->max_proto.all ||               !nf_nat_used_tuple(tuple, ct)))                  goto out;          } else if (!nf_nat_used_tuple(tuple, ct)) {              goto out;          }      }        /* Last change: get protocol to try to obtain unique tuple. */      l4proto->unique_tuple(l3proto, tuple, range, maniptype, ct);  out:      rcu_read_unlock();  }

在这个函数里我们发现了类似ct的l3/l4协议注册处理时的结构体,

const struct nf_nat_l3proto *l3proto;      const struct nf_nat_l4proto *l4proto;

关于它们的注册:

static int __init nf_nat_l3proto_ipv4_init(void)  {      int err;        err = nf_nat_l4proto_register(NFPROTO_IPV4, &nf_nat_l4proto_icmp);      if (err < 0)          goto err1;      err = nf_nat_l3proto_register(&nf_nat_l3proto_ipv4);      if (err < 0)          goto err2;      return err;    err2:      nf_nat_l4proto_unregister(NFPROTO_IPV4, &nf_nat_l4proto_icmp);  err1:      return err;  }
static const struct nf_nat_l3proto nf_nat_l3proto_ipv4 = {      .l3proto        = NFPROTO_IPV4,      .in_range        = nf_nat_ipv4_in_range,      .secure_port        = nf_nat_ipv4_secure_port,      .manip_pkt        = nf_nat_ipv4_manip_pkt,      .csum_update        = nf_nat_ipv4_csum_update,      .csum_recalc        = nf_nat_ipv4_csum_recalc,      .nlattr_to_range    = nf_nat_ipv4_nlattr_to_range,  #ifdef CONFIG_XFRM      .decode_session        = nf_nat_ipv4_decode_session,  #endif  };

l4例如udp的:

const struct nf_nat_l4proto nf_nat_l4proto_udp = {      .l4proto        = IPPROTO_UDP,      .manip_pkt        = udp_manip_pkt,      .in_range        = nf_nat_l4proto_in_range,      .unique_tuple        = udp_unique_tuple,  #if defined(CONFIG_NF_CT_NETLINK) || defined(CONFIG_NF_CT_NETLINK_MODULE)      .nlattr_to_range    = nf_nat_l4proto_nlattr_to_range,  #endif  };

继续后面的处理,判断mainiptype 和range->flags. miainptype很明显是src,而range->flags的值来自哪里呢?
首先我们需要明白的是range来自用户空间的传递.

void xtables_option_tfcall(struct xtables_target *t)  {      if (t->x6_fcheck != NULL) {          struct xt_fcheck_call cb;            cb.ext_name = t->name;          cb.data = t->t->data;          cb.xflags = t->tflags;          cb.udata = t->udata;          t->x6_fcheck(&cb);      } else if (t->final_check != NULL) {          t->final_check(t->tflags);      }      if (t->x6_options != NULL)          xtables_options_fcheck(t->name, t->tflags, t->x6_options);  }

而x6_fcheck:

static void SNAT_fcheck(struct xt_fcheck_call *cb)  {      static const unsigned int f = F_TO_SRC | F_RANDOM;      struct nf_nat_ipv4_multi_range_compat *mr = cb->data;        if ((cb->xflags & f) == f)          mr->range[0].flags |= NF_NAT_RANGE_PROTO_RANDOM;  }

没有指定端口信息则为range.flags |= NF_NAT_RANGE_MAP_IPS;指定端口信息则:range.flags |= NF_NAT_RANGE_PROTO_SPECIFIED;
同理dnat。在iptables命令解析的时候是先处理target然后才去check的.而range的值:
Parse_to里先解析端口信息,如果有的话会赋值给:
只说下单端口的情况:

range.min.tcp.port                  = range.max.tcp.port                  = htons(port);

ip字符串 ip->s_addr
range.min_ip = ip->s_addr;
range.max_ip = range.min_ip;
然后填充到entry_target的data里
继续回到get_unique_tuple,接着in_range返回值为0(因为ip地址唯一),我们看看find_appropriate_src做了什么
很明显一开始net->ct.nat_bysource为null,所以这个函数也返回0.
关键在find_best_ips_proto
它根据源tuple信息和range参数,生产新的tuple(映射后的),然后nf_nat_used_tuple查询是否已有回应报文在hash链表上. 最后用l4proto->unique_tuple保证tuple 的唯一性. 总之get_unique_tuple函数主要工作就是生产新的映射后的tuple。
由于ip信息已经改变,所以new_tuple和curr_tuple肯定不一样. 之后新映射的tuple再逆转为repl_tuple(映射后)
调用nf_conntrack_alter_reply改变ct信息:

/* Alter reply tuple (maybe alter helper). This is for NAT, and is     implicitly racy: see __nf_conntrack_confirm */  void nf_conntrack_alter_reply(struct nf_conn *ct,               const struct nf_conntrack_tuple *newreply)  {      struct nf_conn_help *help = nfct_help(ct);        /* Should be unconfirmed, so not in hash table yet */      NF_CT_ASSERT(!nf_ct_is_confirmed(ct));        pr_debug("Altering reply tuple of %p to ", ct);      nf_ct_dump_tuple(newreply);        ct->tuplehash[IP_CT_DIR_REPLY].tuple = *newreply;      if (ct->master || (help && !hlist_empty(&help->expectations)))          return;        rcu_read_lock();      __nf_ct_try_assign_helper(ct, NULL, GFP_ATOMIC);      rcu_read_unlock();  }

即它改变了 ct->tuplehash[IP_CT_DIR_REPLY].tuple,其他不变.
最后处理nat ext,把nat信息和ct关联起来通过hlist:net->ct.nat_bysource
然后设置ct->status:

ct->status |= IPS_SRC_NAT_DONE

既然nat规则处理完毕,剩下的工作就是处理skb里ip报文信息了。
根据ct里的信息通过nf_nat_packet修改skb指向的ip头.
后续的钩子函数,如果有helper处理helper;然后就是ipv4_confirmed

/* reuse the hash saved before */      hash = *(unsigned long *)&ct->tuplehash[IP_CT_DIR_REPLY].hnnode.pprev;      hash = hash_bucket(hash, net);      repl_hash = hash_conntrack(net, zone,                   &ct->tuplehash[IP_CT_DIR_REPLY].tuple);

首先看第一个hash值,它在__nf_conntrack_alloc的时候被赋值,
Hash计算来自源五元组通过hash_conntrack_raw计算得来.那么snat的时候,基本ct处理后就是nat了,把ct里改变的是ct->tuplehash[IP_CT_DIR_REPLY].tuple信息,其他不变
这样的话 ipv4_confirm的时候,源hash不变,repl_hash重新计算(因为tuple的ip已经重新映射),然后加入inert ct的全局链表net->ct.hash[hash]
既然第一报文已经顺利发送出去,那么响应报文又是如何发给lan侧的呢?
首先202.20.65.5收到报文到prerouting钩子,nf_conntrack_in它会查询到ct信息
设置ctinfo 为IP_CT_ESTABLISHED_REPLY
然后看看ct timeout是否过期,然后调用l4proto->packet更新状态
Ct->status : confirmed + ips_src_nat
我们知道prerouting只能进行dnat。然后会进入dnat的钩子函数nf_nat_ipv4_in
根据ctinfo的信息,直接进入nf_nat_packet处理.
它里面有个关键部分:

/* Invert if this is reply dir. */      if (dir == IP_CT_DIR_REPLY)          statusbit ^= IPS_NAT_MASK;

然后进行dnat处理根据ct信息修改skb指向的ip头信息.即所谓的de-snat.同理dnat
简单看下五元组信息的变化:
我们可以看看数据伍元整的流程:
Lan-wan:
Ct(snat)即ct->tuplehash信息
orig:192.168.1.x—-202.20.65.4
reply:202.20.65.4—-202.20.65.5
回复的报文:
查询到ct(根据202.20.65.4—-202.20.65.5)
Prerouting上nat处理. 由于这个时候只能处理dnat,
找到ct里源五元组即192.168.1.x—-202.20.65.4反转为202.20.65.4—192.168.1.x
Skb根据这个信息进行dnat映射。即完成了正常的通信.

当然nat还有其他很多复杂的应用,这里仅仅分析一个实例应用的流程,作为深入理解nat的开始.