原文发表在 http://shumh.net/posts/net/2022-10-14-masquerade/
本文重点介绍 iptables MASQUERADE 动作实现原理,需要读者具备 iptables 一些基础知识。iptables 的基础知识请翻阅 Iptables 指南 1.1.19。
在讲 MASQUERADE 之前,先说 iptables 的另一个动作:SNAT,如果你对这个动作比较熟悉可以直接略过。
SNAT target
这个 target 是用来做源网络地址转换的,就是重写包的源 IP 地址。那么什么时候会用到这个 target 呢,一般在共享公网地址时使用,即主机访问公网时,通过 SNAT 规则将从本地出去的包的源地址改为 Internet 地址(主机在内网通信时的源地址通常是内网地址,不可在 Internet 上路由),这样从公网的回包才能送回主机。
SNAT 只能用在 nat 表的 POSTROUTING 链里。
Option | --to-source |
---|---|
Example | iptables -t nat -A POSTROUTING -p tcp -o eth0 -j SNAT --to-source 194.236.50.155-194.236.50.160:1024-32000 |
Explanation | 指定源地址和端口,有以下几种方式:<li>1、单独的地址。<li>2、一段连续的地址,用连字符分隔,如194.236.50.155-194.236.50.160,这样可以实现负载平衡。每个流会被随机分配一个 IP,但对于同一个流使用的是同一个 IP。<li>3、在指定 -p tcp 或 -p udp 的前提下,可以指定源端口的范围,如194.236.50.155:1024-32000,这样包的源端口就被限制在1024-32000了。<li>注意,如果可能,iptables 总是想避免任何的端口变更,换句话说,它总是尽力使用建立连接时所用的端口。但是如果两台机子使用相同的源端口,iptables 将会把他们的其中之一映射到另外的一个端口。如果没有指定端口范围, 所有的在512以内的源端口会被映射到512以内的另一个端口,512和1023之间的将会被映射到 1024内,其他的将会被映射到大于或对于1024的端口,也就是说是同范围映射。还要注意,这种映射和目的端口无关。 |
MASQUERADE target
MASQUERADE 的作用和 SNAT 一样,区别是它不需要指定 --to-source。MASQUERADE 专门用于动态获取IP地址的连接,比如,拨号上网、DHCP连接等。
MASQUERADE(伪装)会自动获取网卡上的 IP 地址,而不用像 SNAT 那样需要使用 --to-source 指定,当 IP 发生变化时不需要手动改动。当网卡 down 掉时,MASQUERADE 不会保留任何相关的 conntrack 记录,如果我们使用 SNAT target,conntrack 记录是被保留下来的,直到被超时 GC,这会占用很多连接跟踪的内存。
注意,MASQUERADE 和 SNAT一样,只能用于 nat 表的 POSTROUTING链。
Option | --to-ports |
---|---|
Example | iptables -t nat -A POSTROUTING -p TCP -j MASQUERADE --to-ports 1024-31000 |
Explanation | 在指定TCP或UDP的前提下,设置外出包能使用的端口,设置单个端口:--to-ports 1025,或者是端口范围:--to-ports 1024-3000 |
MASQUERADE 如何选择源 IP
前面聊了 MASQUERADE 的作用,现在终于到了本文的重点,当主机上配置多个 IP 地址时,MASQUERADE 如何从这些 IP 地址中选出一个合适的源 IP。
考虑如下环境:
机器 IP 配置如下:
代码语言:shell复制1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN group default qlen 1000
link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
inet 127.0.0.1/8 scope host lo
valid_lft forever preferred_lft forever
inet6 ::1/128 scope host
valid_lft forever preferred_lft forever
2: eth0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 xdp qdisc mq state UP group default qlen 1000
link/ether 52:54:00:91:a0:be brd ff:ff:ff:ff:ff:ff
inet 10.0.10.13/24 brd 10.0.10.255 scope global eth0
valid_lft forever preferred_lft forever
inet 10.0.10.14/24 scope global secondary eth0
valid_lft forever preferred_lft forever
inet6 fe80::5054:ff:fe91:a0be/64 scope link
valid_lft forever preferred_lft forever
3: docker0: <BROADCAST,MULTICAST,NOARP,UP,LOWER_UP> mtu 1500 qdisc noqueue state UP group default qlen 1000
link/ether 5e:88:9f:6d:a7:2b brd ff:ff:ff:ff:ff:ff
inet 192.168.1.101/32 scope link docker0
valid_lft forever preferred_lft forever
inet6 fe80::5c88:9fff:fe6d:a72b/64 scope link
valid_lft forever preferred_lft forever
路由如下:
代码语言:shell复制default via 10.0.10.1 dev eth0
10.0.10.0/24 dev eth0 proto kernel scope link src 10.0.10.13
192.168.0.0/24 via 192.168.1.101 dev docker0 src 192.168.1.101
192.168.1.101 dev docker0 scope link
配置了 MASQUERADE 规则:
代码语言:shell复制iptables -t nat -A POSTROUTING -j MASQUERADE
如上所示,机器 eth0 Interface 上配置了 10.0.10.13/24, 10.0.10.14/24 两个地址,docker0 上配置了 192.168.1.101/32 地址。
问题1: 在机器上访问 10.0.10.15 时,MASQUERADE 选择的源 IP 是哪个?(10.0.10.13? 10.0.10.14? 192.168.1.101?)
问题2: 在机器上访问 192.168.0.2 时,MASQUERADE 选择的源 IP 是哪个?(10.0.10.13? 10.0.10.14? 192.168.1.101?)
更改 docker0 IP 地址的 scope,从 link 改为 global:
代码语言:shell复制3: docker0: <BROADCAST,MULTICAST,NOARP,UP,LOWER_UP> mtu 1500 qdisc noqueue state UP group default qlen 1000
link/ether 5e:88:9f:6d:a7:2b brd ff:ff:ff:ff:ff:ff
inet 192.168.1.101/32 scope global docker0
valid_lft forever preferred_lft forever
inet6 fe80::5c88:9fff:fe6d:a72b/64 scope link
valid_lft forever preferred_lft forever
问题3: 更改 docker0 IP 地址的 scope 后,此时在机器上访问 192.168.0.2 时,MASQUERADE 选择的源 IP 是哪个?(10.0.10.13? 10.0.10.14? 192.168.1.101?)
代码分析
MASQUERADE 在 NAT 表中注册的钩子函数
代码语言:c复制# net/netfilter/xt_MASQUERADE.c
static struct xt_target masquerade_tg_reg[] __read_mostly = {
{
#if IS_ENABLED(CONFIG_IPV6)
.name = "MASQUERADE",
.family = NFPROTO_IPV6,
.target = masquerade_tg6,
.targetsize = sizeof(struct nf_nat_range),
.table = "nat",
.hooks = 1 << NF_INET_POST_ROUTING,
.checkentry = masquerade_tg6_checkentry,
.destroy = masquerade_tg_destroy,
.me = THIS_MODULE,
}, {
#endif
.name = "MASQUERADE",
.family = NFPROTO_IPV4,
.target = masquerade_tg,
.targetsize = sizeof(struct nf_nat_ipv4_multi_range_compat),
.table = "nat",
.hooks = 1 << NF_INET_POST_ROUTING,
.checkentry = masquerade_tg_check,
.destroy = masquerade_tg_destroy,
.me = THIS_MODULE,
}
};
masquerade_tg 函数
代码语言:c复制static unsigned int
masquerade_tg(struct sk_buff *skb, const struct xt_action_param *par)
{
struct nf_nat_range2 range;
const struct nf_nat_ipv4_multi_range_compat *mr;
mr = par->targinfo;
range.flags = mr->range[0].flags;
range.min_proto = mr->range[0].min;
range.max_proto = mr->range[0].max;
return nf_nat_masquerade_ipv4(skb, xt_hooknum(par), &range,
xt_out(par));
}
nf_nat_masquerade_ipv4 是核心函数,其中 MASQUERADE 选择源 IP 的逻辑就在其中:
代码语言:c复制unsigned int
nf_nat_masquerade_ipv4(struct sk_buff *skb, unsigned int hooknum,
const struct nf_nat_range2 *range,
const struct net_device *out)
{
struct nf_conn *ct;
struct nf_conn_nat *nat;
enum ip_conntrack_info ctinfo;
struct nf_nat_range2 newrange;
const struct rtable *rt;
__be32 newsrc, nh;
WARN_ON(hooknum != NF_INET_POST_ROUTING);
ct = nf_ct_get(skb, &ctinfo);
WARN_ON(!(ct && (ctinfo == IP_CT_NEW || ctinfo == IP_CT_RELATED ||
ctinfo == IP_CT_RELATED_REPLY)));
/* Source address is 0.0.0.0 - locally generated packet that is
* probably not supposed to be masqueraded.
*/
if (ct->tuplehash[IP_CT_DIR_ORIGINAL].tuple.src.u3.ip == 0)
return NF_ACCEPT;
/* 获取路由 */
rt = skb_rtable(skb);
/* 获取下一跳地址 */
nh = rt_nexthop(rt, ip_hdr(skb)->daddr);
/* 选择源 IP */
newsrc = inet_select_addr(out, nh, RT_SCOPE_UNIVERSE);
if (!newsrc) {
pr_info("%s ate my IP addressn", out->name);
return NF_DROP;
}
nat = nf_ct_nat_ext_add(ct);
if (nat)
nat->masq_index = out->ifindex;
/* Transfer from original range. */
memset(&newrange.min_addr, 0, sizeof(newrange.min_addr));
memset(&newrange.max_addr, 0, sizeof(newrange.max_addr));
newrange.flags = range->flags | NF_NAT_RANGE_MAP_IPS;
newrange.min_addr.ip = newsrc;
newrange.max_addr.ip = newsrc;
newrange.min_proto = range->min_proto;
newrange.max_proto = range->max_proto;
/* Hand modified range to generic setup. */
return nf_nat_setup_info(ct, &newrange, NF_NAT_MANIP_SRC);
}
链路如下:
代码语言:shell复制masquerade_tg
-> nf_nat_masquerade_ipv4(skb, xt_hooknum(par), &range,
xt_out(par))
-> inet_select_addr(out, nh, RT_SCOPE_UNIVERSE)
重点来看 inet_select_addr 如何选择源 IP:
代码语言:c复制/* 入参:
net_device: 出口设备
dst: 下一跳地址
scope: 0 (RT_SCOPE_UNIVERSE)
enum rt_scope_t {
RT_SCOPE_UNIVERSE=0,
/* User defined values */
RT_SCOPE_SITE=200,
RT_SCOPE_LINK=253,
RT_SCOPE_HOST=254,
RT_SCOPE_NOWHERE=255
};
scope 详细见: http://www.embeddedlinux.org.cn/linux_net/0596002556/understandlni-CHP-30-SECT-2.html#understandlni-CHP-30-SECT-2.1
*/
__be32 inet_select_addr(const struct net_device *dev, __be32 dst, int scope)
{
const struct in_ifaddr *ifa;
__be32 addr = 0;
unsigned char localnet_scope = RT_SCOPE_HOST;
struct in_device *in_dev;
struct net *net = dev_net(dev);
int master_idx;
rcu_read_lock();
/* 获取出口设备上的所有地址 */
in_dev = __in_dev_get_rcu(dev);
if (!in_dev)
goto no_in_dev;
if (unlikely(IN_DEV_ROUTE_LOCALNET(in_dev)))
localnet_scope = RT_SCOPE_LINK;
in_dev_for_each_ifa_rcu(ifa, in_dev) {
/* 只关心设备上的主地址 */
if (ifa->ifa_flags & IFA_F_SECONDARY)
continue;
/* 对比 scope,注意入参 scope 为 0 */
if (min(ifa->ifa_scope, localnet_scope) > scope)
continue;
/* 如果 dst 不为空,判断 dst 和 设备上的地址是否在一个网段 */
if (!dst || inet_ifa_match(dst, ifa)) {
addr = ifa->ifa_local;
break;
}
/* 找到地址 */
if (!addr)
addr = ifa->ifa_local;
}
if (addr)
goto out_unlock;
/* 上面的逻辑如果没有找到合适的地址,这里会先尝试找出口设备的 master device,再遍历每一个设备*/
no_in_dev:
master_idx = l3mdev_master_ifindex_rcu(dev);
/* For VRFs, the VRF device takes the place of the loopback device,
* with addresses on it being preferred. Note in such cases the
* loopback device will be among the devices that fail the master_idx
* equality check in the loop below.
*/
if (master_idx &&
(dev = dev_get_by_index_rcu(net, master_idx)) &&
(in_dev = __in_dev_get_rcu(dev))) {
addr = in_dev_select_addr(in_dev, scope);
if (addr)
goto out_unlock;
}
/* Not loopback addresses on loopback should be preferred
in this case. It is important that lo is the first interface
in dev_base list.
*/
/* 遍历所有设备 */
for_each_netdev_rcu(net, dev) {
if (l3mdev_master_ifindex_rcu(dev) != master_idx)
continue;
in_dev = __in_dev_get_rcu(dev);
if (!in_dev)
continue;
addr = in_dev_select_addr(in_dev, scope);
if (addr)
goto out_unlock;
}
out_unlock:
rcu_read_unlock();
return addr;
}
static __be32 in_dev_select_addr(const struct in_device *in_dev,
int scope)
{
const struct in_ifaddr *ifa;
in_dev_for_each_ifa_rcu(ifa, in_dev) {
/* 只关系主设备 */
if (ifa->ifa_flags & IFA_F_SECONDARY)
continue;
if (ifa->ifa_scope != RT_SCOPE_LINK &&
ifa->ifa_scope <= scope)
return ifa->ifa_local;
}
return 0;
}
MASQUERADE 选择源 IP 的逻辑我们已经看完了,现在来回答上面提的三个问题:
问题1: 在机器上访问 10.0.10.15 时,MASQUERADE 选择的源 IP 是哪个?(10.0.10.13? 10.0.10.14? 192.168.1.101?)
答: 10.0.10.13,访问 10.0.10.15 会匹配第二条路由,出口设备为 eth0,选择 eth0 上主地址 10.0.10.13
问题2: 在机器上访问 192.168.0.2 时,MASQUERADE 选择的源 IP 是哪个?(10.0.10.13? 10.0.10.14? 192.168.1.101?)
答: 10.0.10.13,访问 192.168.0.2 时会匹配 192.168.0.0/24 via 192.168.1.101 dev docker0 src 192.168.1.101
路由,出口设备为 docker0,但是因为 docker0 IP address scope 为 link,不满足 if (min(ifa->ifa_scope, localnet_scope) > scope)
条件,被过滤掉,从而会触发后面选择主设备的逻辑,选到 eth0 的主地址
问题3: 更改 docker0 IP 地址的 scope 后,此时在机器上访问 192.168.0.2 时,MASQUERADE 选择的源 IP 是哪个?(10.0.10.13? 10.0.10.14? 192.168.1.101?)
答: 192.168.1.101,访问 192.168.0.2 时会匹配 192.168.0.0/24 via 192.168.1.101 dev docker0 src 192.168.1.101
路由,出口设备为 docker0,但是因为 docker0 IP address scope 为 global,满足 if (min(ifa->ifa_scope, localnet_scope) > scope)
条件