简介

本文分三部分:

  • 第一部分:搭建一个SNAT环境,通过ping和tcpdump验证实验。
  • 第二部分:以五链为基础,从业务上分析SNAT的处理流程。
  • 第三部分:分析内核源码(Linux 6.16-rc5),深入理解conntrack基础上的SNAT实现逻辑。

本文详细分析一对ICMP数据包ping requestping reply,基于conntrack(在IP_CT_DIR_ORIGINALIP_CT_DIR_REPLY)两个方向的SNAT业务处理流程。其中,SNAT规则是通过nft下发到内核中的。

一. SNAT实验

1.2 网络拓扑

我们要用PC110.0.3.1 通过 SNAT 路由器 RT1,ping 通RT2 192.168.206.1

网络拓扑

1.3 RT1 SNAT 配置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
~ # nft add table ip mytable
~ # nft add chain ip mytable postrouting { type nat hook postrouting priority 100 \;}
~ # nft add rule ip mytable postrouting ip saddr 10.0.3.0/24 oif "eth0" masquerade
~ # nft add rule ip mytable postrouting meta nftrace set 1 ip saddr 10.0.3.0/24 oif "eth0" masquerade
~ #
~ #
~ # nft -a list table ip mytable
table ip mytable { # handle 1
chain postrouting { # handle 1
type nat hook postrouting priority srcnat; policy accept;
ip saddr 10.0.3.0/24 oif "eth0" masquerade # handle 2
meta nftrace set 1 ip saddr 10.0.3.0/24 oif "eth0" masquerade # handle 3
}
}

1.4 实验效果

1
2
3
4
5
6
7
8
9
10
~ # ip netns exec left ping 192.168.206.1
PING 192.168.206.1 (192.168.206.1): 56 data bytes
64 bytes from 192.168.206.1: seq=0 ttl=63 time=41.186 ms
64 bytes from 192.168.206.1: seq=1 ttl=63 time=1.008 ms
64 bytes from 192.168.206.1: seq=2 ttl=63 time=0.588 ms
64 bytes from 192.168.206.1: seq=3 ttl=63 time=0.640 ms

--- 192.168.206.1 ping statistics ---
^C4 packets transmitted, 4 packets received, 0% packet loss
round-trip min/avg/max = 0.588/10.855/41.186 ms

1.5 抓包分析

1.5.1 RT1 eth1 抓包

发出 10.0.3.1 –> 192.168.206.1ICMP request
收到 192.168.206.1 –> 10.0.3.1ICMP reply

1
2
3
02:14:28.306188 fa:64:6e:a0:54:c8 > 66:37:84:be:0e:9f, ethertype IPv4 (0x0800), length 98: 10.0.3.1 > 192.168.206.1: ICMP echo request, id 122, seq 0, length 64
02:14:28.307291 66:37:84:be:0e:9f > fa:64:6e:a0:54:c8, ethertype IPv4 (0x0800), length 98: 192.168.206.1 > 10.0.3.1: ICMP echo reply, id 122, seq 0, length 64

1.5.2 RT1 eth0 抓包

10.0.3.1 –> 192.168.206.1ICMP request的数据包被SNAT规则修改了源IP,变为 192.168.206.250 –> 192.168.206.1

收到RT2返回的 192.168.206.1 –> 192.168.206.250ICMP reply的数据包被SNAT规则修改了目的IP,变为192.168.206.1 –> 10.0.3.1转发给PC1。

1
2
02:15:40.972860 52:54:00:12:34:56 > 7a:d7:5d:43:7d:3e, ethertype IPv4 (0x0800), length 98: 192.168.206.250 > 192.168.206.1: ICMP echo request, id 126, seq 0, length 64
02:15:40.973268 7a:d7:5d:43:7d:3e > 52:54:00:12:34:56, ethertype IPv4 (0x0800), length 98: 192.168.206.1 > 192.168.206.250: ICMP echo reply, id 126, seq 0, length 64

二. 业务流程分析

本节聚焦RT1路由器,观察ICMP,在五链上,基于会话的业务处理。

2.1 基础知识复习

2.1.1 五链

比较基础的网络协议栈处理流程,所谓五链就是内核在处理数据包时埋下的五个HOOK点:

定义在<linux/netfilter.h>

  • NF_INET_PRE_ROUTING:查路由前经过的埋点
  • NF_INET_LOCAL_IN:目的ip是本地地址要经过的埋点
  • NF_INET_FORWARD:目的ip不是本地地址,开启转发后要经过的埋点
  • NF_INET_LOCAL_OUT:由本地产生的数据包要经过的埋点
  • NF_INET_POST_ROUTING:路由后要经过的埋点

网络拓扑

2.1.2 会话

linux内核为了追踪数据流,业务处理,有一个称为会话(conntrack)的功能模块,一个会话描述了符合特定规则的一系列数据包的特征和状态。

  • 特征:一对五元组
  • 状态:例如TCP在握手、建立成功、分手期间的不同状态。

五元组就是一个数据包的第四层协议,源、目IP地址,源、目端口号。例如tcp src=192.168.206.100 dst=4.145.79.82 sport=49380 dport=443

而一个会话包括ORIGINAL方向和REPLY方向,每个方向一个五元组,一个会话有两个五元组。

可以通过sudo conntrack -L查询会话

1
2
zrf@debian:$ sudo conntrack -L
tcp 6 431824 ESTABLISHED src=192.168.206.100 dst=4.145.79.82 sport=49380 dport=443 src=4.145.79.82 dst=10.10.100.174 sport=443 dport=49380 [ASSURED] mark=0 use=1

其中,第一对五元组是ORIGINAL方向,第二对五元组是REPLY方向。

2.2 ICMP request

本节我们分析RT1路由器的eth1接口收到来自PC1的10.0.3.1 -> 192.168.206.1的ICMP请求后,把源IP10.0.3.1通过SNAT规则转为192.168.206.250的业务处理逻辑。

从五链的转发顺序中,可以知道,我们会先进入PRE_ROUTING埋点,发现目的IP 192.168.206.1 不是本地地址后,查询路由,进入FORWARD埋点,最后通过POST_ROUTING埋点从eth0接口把数据包转发出去。

本小节也将追踪这三个埋点,看看netfiler框架是如何记录会话,SNAT转换的。

2.2.1 PRE_ROUTING

PRE_ROUTINGHOOK点上,根据hook优先级,会先后进入以下模块:

  • NF_IP_PRI_CONNTRACK_DEFRAG = -400,
  • NF_IP_PRI_CONNTRACK = -200,
  • NF_IP_PRI_NAT_DST = -100,

我们重点关注NF_IP_PRI_CONNTRACK会话模块就可以。

skb进入会话模块后

首先创建一对五元组(ICMP没有端口)icmp ORIGINAL:src=10.0.3.1,dst=192.168.206.1 REPLY:src=192.168.206.1,dst=10.0.3.1

会话状态为ctinfo:NEW

下面的log是在NF_INET_PRE_ROUTING.(NF_IP_PRI_CONNTRACK+1)注册了一个hook函数,用以验证以上的分析。

1
2
3
4
[ 3946.922715] ctinfo:NEW L3:ipv4[2],L4:icmp:[1] dir:0,src=10.0.3.1,dst=192.168.206.1 dir:1,src=192.168.206.1,dst=10.0.3.1
[ 3946.922803] NF-CORE: pf = AF_INET, hook = NF_INET_PRE_ROUTING, elem->pri = -199 verdict = NF_ACCEPT skb ffff888004d74100
[ 3946.924097] indev = left-veth, outdev : NULL
[ 3946.924458] 10.0.3.1 -> 192.168.206.1 protocol 1 id 16310

2.2.2 FORWARD

查询路由,后发现需要进行转发,则进入NF_INET_FORWARD埋点。

通过log可以看到,查询路由期间,可以找到出接口outdev : eth0

1
2
3
[ 3946.927980] NF-CORE: pf = AF_INET, hook = NF_INET_FORWARD, elem->pri = -225 verdict = NF_ACCEPT skb ffff888004d74100
[ 3946.928599] indev = left-veth, outdev : eth0
[ 3946.928829] 10.0.3.1 -> 192.168.206.1 protocol 1 id 16310

2.2.3 POST_ROUTING

POST_ROUTINGHOOK点上,根据hook优先级,会先后进入以下模块:

  • NF_IP_PRI_NAT_SRC
  • NF_IP_PRI_CONNTRACK_CONFIRM

可以看到,NF_IP_PRI_NAT_SRC就是SNAT,也就是我们在ORIGINAL方向最该关心的业务。

进入SNAT模块后,会根据我们通过nft add rule ip mytable postrouting meta nftrace set 1 ip saddr 10.0.3.0/24 oif "eth0" masquerade添加的规则,修改REPLY方向的五元组:

此前REPLY方向的五元组为:dir:1,src=192.168.206.1,dst=10.0.3.1

而通过SNAT模块后,REPLY方向的五元组为:dir:1,src=192.168.206.1,dst=192.168.206.250

也就是说,SNAT模块把REPLY方向五元组的dst修改为192.168.206.250

最后,修改skb的源IP地址:

此前skb为:[ 3946.924458] 10.0.3.1 -> 192.168.206.1 protocol 1 id 16310

而通过SNAT模块后,skb为:[ 3946.935845] 192.168.206.250 -> 192.168.206.1 protocol 1 id 16310

1
2
3
4
[ 3946.934240] ctinfo:NEW L3:ipv4[2],L4:icmp:[1] dir:0,src=10.0.3.1,dst=192.168.206.1 dir:1,src=192.168.206.1,dst=192.168.206.250
[ 3946.934282] NF-CORE: pf = AF_INET, hook = NF_INET_POST_ROUTING, elem->pri = 226 verdict = NF_ACCEPT skb ffff888004d74100
[ 3946.935528] indev = left-veth, outdev : eth0
[ 3946.935845] 192.168.206.250 -> 192.168.206.1 protocol 1 id 16310

2.3 ICMP reply

RT2收到ICMP request后,会给RT1一个192.168.206.1 > 192.168.206.250: ICMP echo reply

eth0收到数据包后,依旧从PRE_ROUTING进入第一个埋点,

2.3.1 PRE_ROUTING

PRE_ROUTINGHOOK点上,根据hook优先级,会先后进入以下模块:

  • NF_IP_PRI_CONNTRACK_DEFRAG = -400,
  • NF_IP_PRI_CONNTRACK = -200,
  • NF_IP_PRI_NAT_DST = -100,

重点关注NF_IP_PRI_CONNTRACKNF_IP_PRI_NAT_DST

skb进入会话模块后

尝试在数据库中查找是否已经有这条会话实例,本次查询应该是可以查到的,而且还可以知道,这次的数据包是REPLY方向,以下是在NF_INET_PRE_ROUTING.(NF_IP_PRI_CONNTRACK+1)上注册埋点,,可以看到,数据包上已经绑定了ct实例,且方向为REPLY:

1
2
3
4
[ 3946.946929] ctinfo:REPLY L3:ipv4[2],L4:icmp:[1] dir:0,src=10.0.3.1,dst=192.168.206.1 dir:1,src=192.168.206.1,dst=192.168.206.250
[ 3946.947113] NF-CORE: pf = AF_INET, hook = NF_INET_PRE_ROUTING, elem->pri = -199 verdict = NF_ACCEPT skb ffff888004d74100
[ 3946.950289] indev = eth0, outdev : NULL
[ 3946.950536] 192.168.206.1 -> 192.168.206.250 protocol 1 id 43037

此时,数据包还是[ 3946.950536] 192.168.206.1 -> 192.168.206.250 protocol 1 id 43037

接下来进入NF_IP_PRI_NAT_DST的埋点:

此时会话存在,将会依据REPLY,修改目的地址,所以从这个埋点出来后,数据包已经变为了[ 3946.952435] 192.168.206.1 -> 10.0.3.1 protocol 1 id 43037

1
2
3
[ 3946.950884] NF-CORE: pf = AF_INET, hook = NF_INET_PRE_ROUTING, elem->pri = -100 verdict = NF_ACCEPT skb ffff888004d74100
[ 3946.952045] indev = eth0, outdev : NULL
[ 3946.952435] 192.168.206.1 -> 10.0.3.1 protocol 1 id 43037

2.3.2 FORWARD

查询路由,后发现需要进行转发,则进入NF_INET_FORWARD埋点。

通过log可以看到,查询路由期间,可以找到出接口outdev : left-veth

1
2
3
[ 3946.954591] NF-CORE: pf = AF_INET, hook = NF_INET_FORWARD, elem->pri = -225 verdict = NF_ACCEPT skb ffff888004d74100
[ 3946.956420] indev = eth0, outdev : left-veth
[ 3946.956644] 192.168.206.1 -> 10.0.3.1 protocol 1 id 43037

2.3.3 POST_ROUTING

最终,数据包从left-veth口发出去。

三. 源码分析

本节开始,从代码实现的层面,来了解一下conntrack和SNAT。

分为两部分:

  • 运行时会话上下文:conntrack的创建和确认。

  • 应用规则:应用NAT规则,修改数据包(以nft举例)

3.1 会话的建立

NF_INET_PRE_ROUTING.NF_IP_PRI_CONNTRACK埋点上,如果这个数据包没有绑定会话,将会创建一个新的会话实例,我们主要关心会话的方向和两个五元组:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
static int
resolve_normal_ct()
{
struct nf_conntrack_tuple tuple;
enum ip_conntrack_info ctinfo;
struct nf_conn *ct;

/* tuple : src=10.0.3.1,dst=192.168.206.1 */
nf_ct_get_tuple(skb, skb_network_offset(skb),
dataoff, state->pf, protonum, state->net,
&tuple);

init_conntrack(state->net, tmpl, &tuple,
skb, dataoff, hash);

ctinfo = IP_CT_NEW;
return 0;
}

此时会话的信息如下:

1
[ 3946.922715] ctinfo:NEW L3:ipv4[2],L4:icmp:[1] dir:0,src=10.0.3.1,dst=192.168.206.1 dir:1,src=192.168.206.1,dst=10.0.3.1

两个五元组不必多说,我们再关注一下init_conntrack()函数创建ct时,与方向有关的信息与存放位置:

tunple是此时数据包src=10.0.3.1,dst=192.168.206.1的元组,通过nf_ct_invert_tuple()反转元组函数得到一个期望的响应元组repl_tuple:src=192.168.206.1,dst=10.0.3.1

__nf_conntrack_alloc()分配内存,并为两个五元组分别赋值tunplerepl_tuple

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
static noinline struct nf_conntrack_tuple_hash *
init_conntrack(struct net *net, struct nf_conn *tmpl,
const struct nf_conntrack_tuple *tuple,
struct sk_buff *skb,
unsigned int dataoff, u32 hash)
{
struct nf_conn *ct;
struct nf_conntrack_tuple repl_tuple;

if (!nf_ct_invert_tuple(&repl_tuple, tuple))
return NULL;

ct = __nf_conntrack_alloc(net, zone, tuple, &repl_tuple, GFP_ATOMIC, hash);

return &ct->tuplehash[IP_CT_DIR_ORIGINAL];
}

3.2 会话的匹配

继续分析init_conntrack(),请关注返回值&ct->tuplehash[IP_CT_DIR_ORIGINAL],这是一个基于IP_CT_DIR_ORIGINAL方向的哈希值,如果下次在此收到这样的数据包,将通过这个哈希值在IP_CT_DIR_ORIGINAL方向查找。

让我们回头再关注以下resolve_normal_ct()更多细节,在创建会话实例时,会尝试在IP_CT_DIR_ORIGINALIP_CT_DIR_REPLY两个方向查看是否存在会话,都没有的话,才会创建新的实例。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
static int
resolve_normal_ct(struct nf_conn *tmpl,
struct sk_buff *skb,
unsigned int dataoff,
u_int8_t protonum,
const struct nf_hook_state *state)
{
struct nf_conntrack_tuple tuple;
struct nf_conntrack_tuple_hash *h;
enum ip_conntrack_info ctinfo;
struct nf_conn *ct;

nf_ct_get_tuple(skb, skb_network_offset(skb),
dataoff, state->pf, protonum, state->net,
&tuple);

/* 尝试在IP_CT_DIR_ORIGINAL方向通过哈希值查找ct */
hash = hash_conntrack_raw(&tuple, nf_ct_zone_id(zone, IP_CT_DIR_ORIGINAL), state->net);
h = __nf_conntrack_find_get(state->net, zone, &tuple, hash);

/* 未找到 */
if (!h) {
/* /* 尝试在IP_CT_DIR_REPLY方向通过哈希值查找ct */ */
u32 tmp = hash_conntrack_raw(&tuple, nf_ct_zone_id(zone, IP_CT_DIR_REPLY), state->net);
h = __nf_conntrack_find_get(state->net, zone, &tuple, tmp);
}

/* 两个方向都没有找到才会创建一个新的ct实例 */
if (!h)
h = init_conntrack(state->net, tmpl, &tuple,
skb, dataoff, hash);

nf_ct_set(skb, ct, ctinfo);
return 0;
}

3.3 会话的修改

我这里指的会话的修改,主要是针对会话中的两个五元组的修改,并不涉及到skb数据包的修改,此刻系统可能基于SNAT规则,修改会话中五元组的规则,在内核中,SNAT主要通过nf_nat_setup_info()修改会话。

我们以ctinfo:NEW L3:ipv4[2],L4:icmp:[1] dir:0,src=10.0.3.1,dst=192.168.206.1 dir:1,src=192.168.206.1,dst=10.0.3.1为例,看下nf_nat_setup_info()是怎么修改规则的。

修改过程详见代码注释:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
unsigned int
nf_nat_setup_info(struct nf_conn *ct,
const struct nf_nat_range2 *range,
enum nf_nat_manip_type maniptype)
{
/*
* ct->tuplehash[IP_CT_DIR_ORIGINAL].tuple:
* src=10.0.3.1,dst=192.168.206.1
* ct->tuplehash[IP_CT_DIR_REPLY].tuple:
* src=192.168.206.1,dst=10.0.3.1
*/

nf_ct_invert_tuple(&curr_tuple,
&ct->tuplehash[IP_CT_DIR_REPLY].tuple);

/* curr_tuple: src=10.0.3.1,dst=192.168.206.1 */

get_unique_tuple(&new_tuple, &curr_tuple, range, ct, maniptype);
/* new_tuple: src=192.168.206.250,dst=192.168.206.1 */

nf_ct_invert_tuple(&reply, &new_tuple);
/* reply: src=192.168.206.1,dst=192.168.206.250 */

nf_conntrack_alter_reply(ct, &reply);
/*
* ct->tuplehash[IP_CT_DIR_ORIGINAL].tuple:
* src=10.0.3.1,dst=192.168.206.1
* ct->tuplehash[IP_CT_DIR_REPLY].tuple:
* src=192.168.206.1,dst=192.168.206.250
*/

/* 以IP_CT_DIR_ORIGINAL为键 */
srchash = hash_by_src(net, nf_ct_zone(ct),
&ct->tuplehash[IP_CT_DIR_ORIGINAL].tuple);

/* 加入到全局&nf_nat_bysource[srchash]表中 */
hlist_add_head_rcu(&ct->nat_bysource,
&nf_nat_bysource[srchash]);

return NF_ACCEPT;
}
EXPORT_SYMBOL(nf_nat_setup_info);

3.4 修改数据包

请注意,截至目前为止,所有的修改只是针对会话实例的修改,并没有真正修改数据包。

当这些会话规则就绪以后,将通过nf_nat_packet()来修改源IP:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
unsigned int nf_nat_packet(struct nf_conn *ct,
enum ip_conntrack_info ctinfo,
unsigned int hooknum,
struct sk_buff *skb)
{
/* Non-atomic: these bits don't change. */
if (ct->status & statusbit)
verdict = nf_nat_manip_pkt(skb, ct, mtype, dir)
{
/* ct->tuplehash[!dir].tuple : src=192.168.206.1,dst=192.168.206.250*/
nf_ct_invert_tuple(&target, &ct->tuplehash[!dir].tuple);
{
/* target : src=192.168.206.250,dst=192.168.206.1 */
iph = (void *)skb->data + iphdroff;
if (maniptype == NF_NAT_MANIP_SRC)
/* iph->saddr 修改为 192.168.206.250 */
iph->saddr = target->src.u3.ip;
else
iph->daddr = target->dst.u3.ip;
return true;
}
}
return verdict;
}
EXPORT_SYMBOL_GPL(nf_nat_packet);

3.5 REPLY 方向

第二节中,我们已经分析过,REPLY方向将在NF_INET_PRE_ROUTING.NF_IP_PRI_CONNTRACK埋点上,通过基于方向的哈希(第三节会话的匹配),匹配到ctinfo:REPLY L3:ipv4[2],L4:icmp:[1] dir:0,src=10.0.3.1,dst=192.168.206.1 dir:1,src=192.168.206.1,dst=192.168.206.250会话。

接来在,在NF_INET_PRE_ROUTING.NF_IP_PRI_NAT_DST埋点上,把REPLY方向的数据包src:192.168.206.1 dst:192.168.206.250修改dst,变为src:192.168.206.1 dst:10.0.3.1

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
nf_nat_ipv4_pre_routing() {
nf_nat_ipv4_fn() {
nf_nat_inet_fn() {
nf_nat_packet() {
nf_nat_manip_pkt() {
/* ct->tuplehash[!dir].tuple : src=10.0.3.1,dst=192.168.206.1 */
nf_ct_invert_tuple(&target, &ct->tuplehash[!dir].tuple);
/* target : src=192.168.206.1,dst=10.0.3.1 */

/* iph->daddr : 192.168.206.250 */
iph->daddr = target->dst.u3.ip;
/* iph->daddr : 10.0.3.1 */
}
}
}
}
}

3.6 SNAT规则的添加

有很多方法可以给内核添加NAT规则,例如iptablesnft编写内核模块等。

本次实验SNAT规则是通过nft来下发的:

由于不同的前端有各自的配置方法,这里并不会详细分析nft的源码,只会介绍举例内核最近的一层配置业务逻辑。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
~ # nft add table ip mytable
~ # nft add chain ip mytable postrouting { type nat hook postrouting priority 100 \;}
~ # nft add rule ip mytable postrouting ip saddr 10.0.3.0/24 oif "eth0" masquerade
~ # nft add rule ip mytable postrouting meta nftrace set 1 ip saddr 10.0.3.0/24 oif "eth0" masquerade
~ #
~ #
~ # nft -a list table ip mytable
table ip mytable { # handle 1
chain postrouting { # handle 1
type nat hook postrouting priority srcnat; policy accept;
ip saddr 10.0.3.0/24 oif "eth0" masquerade # handle 2
meta nftrace set 1 ip saddr 10.0.3.0/24 oif "eth0" masquerade # handle 3
}
}

回忆ORIGINAL方向时,ctinfo:NEW状态的ct实例,在经过NF_INET_POST_ROUTING.NF_IP_PRI_NAT_SRC埋点时,会依据nft对内核的配置,首先在nf_nat_setup_info()修改REPLY方向五元组中的dst,然后在nf_nat_packet()修改数据包的src

也就是在这个埋点,执行了nft注册的SNAT规则。

我们看这个埋点的回调函数nf_nat_inet_fn

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
unsigned int
nf_nat_inet_fn(void *priv, struct sk_buff *skb,
const struct nf_hook_state *state)
{
struct nf_conn *ct;
enum ip_conntrack_info ctinfo;
ct = nf_ct_get(skb, &ctinfo);

switch (ctinfo) {
case IP_CT_NEW:
for (i = 0; i < e->num_hook_entries; i++) {
/* 执行nft或者iptables注册SNAT规则 */
ret = e->hooks[i].hook(e->hooks[i].priv, skb,
state);
}
return nf_nat_packet(ct, ctinfo, state->hook, skb);
}
EXPORT_SYMBOL_GPL(nf_nat_inet_fn);

针对nft前端来说,调用栈如下,可见,从nf_nat模块,进入到nft模块,再进入到nft_masq模块,最终调用nf_nat提供的接口函数:

1
2
3
4
5
6
7
8
9
10
11
nf_nat_inet_fn() /* nf_nat_core.c:960 */ {
nft_nat_do_chain() /* nft_chain_nat.c:32 */ {
expr_call_ops_eval() /* nf_tables_core.c:237 */ {
nft_masq_eval() /* nft_masq.c:114 */ {
nf_nat_masquerade_ipv4() /* nf_nat_masquerade.c:74 */ {
nf_nat_setup_info() /* nf_nat_core.c:792 */
}
}
}
}
}