使用iptables来进行端口映射我们似乎比较熟悉,指定一个范围的端口映射好像也略懂一二,但实际测试结果却不尽人意。
一般而言要在路由器里实现一条端口映射规则,需要两个iptables规则,一条是目的地址转换一条是源地址转换。 如下(192.168.40.200是wan口地址,10.0.0.150是LAN侧主机地址):
iptables -t nat -I PREROUTING -p tcp -d 192.168.40.200 --dport 23 -j DNAT --to 10.0.0.150:23
iptables -t nat -I POSTROUTING -p tcp -s 10.0.0.150 --sport 23 -j SNAT --to 192.168.40.200:23
查看man iptables可知,范围端口映射规则也挺简单,匹配源时写为--sport port[:port]
,target时写为[ipaddr][-ipaddr][:port[-port]]
。即上面单个的端口映射规则改为这种形式:
iptables -t nat -I PREROUTING -p tcp -d 192.168.40.200 --dport 23:100 -j DNAT --to 10.0.0.150:23-100
重点不在于规则怎么写,而是范围端口映射时是怎么映射?因为它可以少对多也可以多对少。例如配置[10-12]映射到[100-102],我怎么知道端口11会映射到具体那个端口,101吗? [100-200]映射到[10-20]时又会怎么样呢?
如果测试一下第一个问题会发现总是映射到100端口,不是想象的101端口。
内核代码一向难度较高,这里也只是浅浅的忽悠一下。
跟踪一下DNAT target的动作,DNAT动作的代码在net/ipv4/netfilter/nf_nat_rule.c
里的ipt_dnat_target()
函数,然后进入nf_nat_setup_info()
==》 get_unique_tuple()
。
get_unique_tuple函数
1.static void
2.get_unique_tuple(struct nf_conntrack_tuple *tuple,
3. const struct nf_conntrack_tuple *orig_tuple,
4. const struct nf_nat_range *range,
5. struct nf_conn *ct,
6. enum nf_nat_manip_type maniptype)
7.{
8. struct net *net = nf_ct_net(ct);
9. const struct nf_nat_protocol *proto;
10. u16 zone = nf_ct_zone(ct);
11.
12. /* 1) If this srcip/proto/src-proto-part is currently mapped,
13. and that same mapping gives a unique tuple within the given
14. range, use that.
15.
16. This is only required for source (ie. NAT/masq) mappings.
17. So far, we don't do local source mappings, so multiple
18. manips not an issue. */
19. if (maniptype == IP_NAT_MANIP_SRC &&
20. !(range->flags & IP_NAT_RANGE_PROTO_RANDOM)) {
21. if (find_appropriate_src(net, zone, orig_tuple, tuple, range)) {
22. pr_debug("get_unique_tuple: Found current src map\n");
23. if (!nf_nat_used_tuple(tuple, ct))
24. return;
25. }
26. }
27. printk("%s(%d)\n", __FUNCTION__, __LINE__);
28. /* 2) Select the least-used IP/proto combination in the given
29. range. */
30. *tuple = *orig_tuple;
31. find_best_ips_proto(zone, tuple, range, ct, maniptype);
32.
33. /* 3) The per-protocol part of the manip is made to map into
34. the range to make a unique tuple. */
35.
36. rcu_read_lock();
37. proto = __nf_nat_proto_find(orig_tuple->dst.protonum);
38.
39. /* Only bother mapping if it's not already in range and unique */
40. if (!(range->flags & IP_NAT_RANGE_PROTO_RANDOM) &&
41. (!(range->flags & IP_NAT_RANGE_PROTO_SPECIFIED) ||
42. proto->in_range(tuple, maniptype, &range->min, &range->max)) &&
43. !nf_nat_used_tuple(tuple, ct))
44. goto out;
45.
46. /* Last change: get protocol to try to obtain unique tuple. */
47. proto->unique_tuple(tuple, range, maniptype, ct);
48.out:
49. printk("%s(%d)\n", __FUNCTION__, __LINE__);
50. rcu_read_unlock();
51.}
52.
重点在第40行的判断,里面的proto->in_range(tuple, maniptype, &range->min, &range->max)
会判断原tuple的端口是否在映射的端口范围内,如果在那就基本要直接goto out了。否则会进入proto->unique_tuple
==> tcp_unique_tuple
==>nf_nat_proto_unique_tuple
。
nf_nat_proto_unique_tuple函数,看这一句就好了
1.for (i = 0; ; ++off) {
2. *portptr = htons(min + off % range_size);
3. if (++i != range_size && nf_nat_used_tuple(tuple, ct))
4. continue;
5. if (!(range->flags & IP_NAT_RANGE_PROTO_RANDOM))
6. *rover = off;
7. return;
8. }
portptr为传出的参数,就是映射到这个端口啦。min为映射端口范围的最小值,off为偏移,大多数时候为0,range_size为端口范围大小。所以进到这里一般情况下,就会映射范围端口里面最小的端口。
当匹配的端口在映射端口的区间内时,那么端口号不会被修改。如果匹配端口不在映射端口的区间内,则大多数情况下映射端口号为最小的端口号,即映射端口号是不可预测的。
举例: [1000-2000] 映射到[1000-2000]时会一一映射
[1000-2000] 映射到[3000-4000]结果不可预知,一般会映射到3000端口。