iptables MASQUERADE 如何选择源IP

2022-10-30 20:25:37 浏览数 (1)

原文发表在 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) 条件

0 人点赞