nat_tftp 期望连接

时间:2021-01-09 00:53:45

  上篇文章分析了 内核 tftp help 期望连接相关代码, 其中有一点是nat_tftp没有分析,对应业务逻辑就是:TFTP协议穿越SNAT

TFTP协议穿越SNAT

穿越SNAT主要用于TFTP服务器部署在公网场景,客户端需要通过SNAT转换后访问外部服务器。如图2-1所示,展示了TFTP穿越防火墙SNAT时的工作流程,此时需要开启TFTP ALG功能才可以完成穿墙。

nat_tftp 期望连接

 

  防火墙设备上配置了私网地址192.168.12.2到公网地址106.120.22.2/TFTP服务的映射,实现IP地址的转换,以支持私网客户端对公网服务器的访问。

组网中,若没有开启ALG功能,防火墙在将收到后续数据协商报文(如Acknowledgement包)时会将其识别为非法流量进行拦截、也就不会根据控制连接会话将数据连接的目的IP(106.120.22.2)进行SNAT还原为私网地址(192.168.12.2),

从而导致私网客户端访问公网TFTP服务器失败。整个通信过程包括以下几个阶段(包1-5):

  • 192.168.12.2:49679→106.120.12.2:69,私网客户端访问公网服务器的69端口请求下载文件;
  • 106.120.22.2:2049 → 106.120.12.2:69,经过防火墙SNAT转换后,公网服务器收到RRQ信息;
  • 106.120.12.2:58470 → 106.120.22.2:2049,公网服务器响应,另起端口返回数据;
  • 防火墙利用TFTP ACL ALG技术,安全策略检查放行公网服务器响应包;
  • 106.120.12.2:58470 → 192.168.12.2:49679,防火墙利用TFTP NAT ALG技术完成SNAT还原;
  • 数据到达私网客户端,返回ACK消息,数据传输成功。
  • 包6-9为私网客户端访问公网服务器的69端口请求上传文件(WRQ)的过程,与请求下载文件(RRQ)类似,不再赘述。
static int tftp_help(struct sk_buff *skb,
unsigned int protoff,
struct nf_conn *ct,
enum ip_conntrack_info ctinfo)
{
const struct tftphdr *tfh;
struct tftphdr _tftph;
struct nf_conntrack_expect *exp;
struct nf_conntrack_tuple *tuple;
unsigned int ret = NF_ACCEPT;
typeof(nf_nat_tftp_hook) nf_nat_tftp;
/* 获得tftp首部 */
tfh = skb_header_pointer(skb, protoff + sizeof(struct udphdr),
sizeof(_tftph), &_tftph);
if (tfh == NULL)
return NF_ACCEPT;

switch (ntohs(tfh->opcode)) {
case TFTP_OPCODE_READ:
case TFTP_OPCODE_WRITE:
/* 在nf_ct_expect_cachep上分配一个expect连接,同时赋两个值:
exp->master = ct,
exp->use = 1。 */
/* RRQ and WRQ works the same way */
nf_ct_dump_tuple(&ct->tuplehash[IP_CT_DIR_ORIGINAL].tuple);
nf_ct_dump_tuple(&ct->tuplehash[IP_CT_DIR_REPLY].tuple);

exp = nf_ct_expect_alloc(ct);//exp->master = ct;
if (exp == NULL) {
nf_ct_helper_log(skb, ct, "cannot alloc expectation");
return NF_DROP;
}
/* 根据ct初始化expect */
tuple = &ct->tuplehash[IP_CT_DIR_REPLY].tuple;
/*初始化expect exp->class=NF_CT_EXPECT_CLASS_DEFAULT*/
nf_ct_expect_init(exp, NF_CT_EXPECT_CLASS_DEFAULT,
nf_ct_l3num(ct),
&tuple->src.u3, &tuple->dst.u3,
IPPROTO_UDP, NULL, &tuple->dst.u.udp.port);

pr_debug("expect: ");
nf_ct_dump_tuple(&exp->tuple);

nf_nat_tftp = rcu_dereference(nf_nat_tftp_hook);
/* 数据包需要走NAT时,if成立,局域网传输则else成立。
如果ct做了NAT,就调用nf_nat_tftp指向的函数,这里它指向nf_nat_tftp.c中的help()函数。*/
if (nf_nat_tftp && ct->status & IPS_NAT_MASK)
ret = nf_nat_tftp(skb, ctinfo, exp);//如果ct做了NAT,就调用nf_nat_tftp指向的函数,这里它指向nf_nat_tftp.c中的help()函数
else if (nf_ct_expect_related(exp) != 0) {//调用nf_ct_expect_insert 插入 nf_ct_expect_hash 同时ct.expect_count++
nf_ct_helper_log(skb, ct, "cannot add expectation");
ret = NF_DROP;
}
nf_ct_expect_put(exp);
break;
case TFTP_OPCODE_DATA:/* 数据 */
case TFTP_OPCODE_ACK:/* 数据的ACK */
pr_debug("Data/ACK opcode\n");
break;
case TFTP_OPCODE_ERROR:
pr_debug("Error opcode\n");
break;
default:
pr_debug("Unknown opcode\n");
}
return ret;
}

根据上篇文章介绍:

tftp_help()的工作有两部分:
1.   根据数据包的ct初始化一个expect连接,由于help函数是在ipv4_confirm()时调用的,所以ct是存在的。另外需要说明一点,tftp请求(读或写)只能从client到server,所以,如果要走NAT,tftp请求包的方向一定是从内网到外网的。
按照上面的例子,当前ct为:
  ORGINAL tuple: 17 192.168.10.1:58747 -> 192.168.10.100:69
  REPLY tuple: 17 192.168.10.100:69 -> 192.168.10.1:58747
生成的expect为:
  ORGINAL tuple: 17 192.168.10.100:0 -> 192.168.10.1:58747
同时,exp->master = ct。注意,expect只有一个tuple,即只有一个方向,这里只看到ORIGNAL方向的tuple,只是因为tuple的dir没赋值,默认为0。

2、如果ct做了NAT,就调用nf_nat_tftp指向的函数,这里它指向nf_nat_tftp.c中的help()函数 ,当前文章主要讲述这个nat_tftp

 

static unsigned int help(struct sk_buff *skb,
enum ip_conntrack_info ctinfo,
struct nf_conntrack_expect *exp)
{
const struct nf_conn *ct = exp->master;

exp->saved_proto.udp.port
= ct->tuplehash[IP_CT_DIR_ORIGINAL].tuple.src.u.udp.port;
exp->dir = IP_CT_DIR_REPLY;
/*
还注意到在help()函数中将exp-> expectfn赋值为nf_nat_follow_master(),这个函数的作用在后面会提到。
上面的内容是在客户端发送tftp请求后触发的动作,主要的效果就是生成了一个期望连接并可以被使用了。
下面以请求读数据来看一下传输数据时的数据包变化。 同时调用expect_related 将expect插入链表
*/
exp->expectfn = nf_nat_follow_master;
if (nf_ct_expect_related(exp) != 0) {
nf_ct_helper_log(skb, exp->master, "cannot add expectation");
return NF_DROP;
}
return NF_ACCEPT;
}

 

注意到在help()函数中将exp-> expectfn赋值为nf_nat_follow_master(),这个函数的作用在后面会提到:

  当tftp请求包进入nf_conntrack_in的时候,由于没有ct条目,所以调用init_conntrack()尝试新建一个条目,在这个函数中,根据skb新建两个方向的tuple,之后有这样的代码

/* Allocate a new conntrack: we return -ENOMEM if classification
failed due to stress. Otherwise it really is unclassifiable. */
static struct nf_conntrack_tuple_hash *
init_conntrack(struct net *net, struct nf_conn *tmpl,
const struct nf_conntrack_tuple *tuple, struct nf_conntrack_l3proto *l3proto,
struct nf_conntrack_l4proto *l4proto, struct sk_buff *skb,
unsigned int dataoff, u32 hash)
{
struct nf_conn *ct;
struct nf_conn_help *help;
struct nf_conntrack_tuple repl_tuple;
struct nf_conntrack_ecache *ecache;
struct nf_conntrack_expect *exp = NULL;
-----------------------------------------------------
zone = nf_ct_zone_tmpl(tmpl, skb, &tmp);
ct = __nf_conntrack_alloc(net, zone, tuple, &repl_tuple, GFP_ATOMIC,hash);
----------------------------
nf_ct_labels_ext_add(ct);
ecache = tmpl ? nf_ct_ecache_find(tmpl) : NULL;
nf_ct_ecache_ext_add(ct, ecache ? ecache->ctmask : 0, ecache ? ecache->expmask : 0, GFP_ATOMIC);
local_bh_disable();
/* 在helper 函数中 回生成expect 并加入全局链表 同时 expect_count++*/
if (net->ct.expect_count) {
/* 如果在期望连接链表中 */
spin_lock(&nf_conntrack_expect_lock);
exp = nf_ct_find_expectation(net, zone, tuple);
/* 如果在期望连接链表中 */
if (exp) {
pr_debug("expectation arrives ct=%p exp=%p\n",ct, exp);
/* Welcome, Mr. Bond. We've been expecting you... */
__set_bit(IPS_EXPECTED_BIT, &ct->status);
ct->master = exp->master;
if (exp->helper) {/* helper的ext以及help链表分配空间 */
help = nf_ct_helper_ext_add(ct, exp->helper,GFP_ATOMIC);
if (help)
rcu_assign_pointer(help->helper, exp->helper);
}
NF_CT_STAT_INC(net, expect_new);
}
spin_unlock(&nf_conntrack_expect_lock);
}
-----------------------------------------------
local_bh_enable();
if (exp) {
if (exp->expectfn)// ---> nf_nat_follow_master
exp->expectfn(ct, exp);
nf_ct_expect_put(exp);
}
return &ct->tuplehash[IP_CT_DIR_ORIGINAL];
}

 

在第一个包(client---->server)经过 PRE_ROUTING和 POST_ROUTING时:有如下相关动作

  • 在全局的期望连接链表expect_hash中查找是否有匹配新建tuple的期望连接。第一次过来的数据包肯定是没有的,于是走else分支,__nf_ct_try_assign_helper()函数去nf_ct_helper_hash哈希表中匹配当前tuple,由于我们在本节开头提到nf_conntrack_tftp_init()已经把tftp的helper extension添加进去了,所以可以匹配成功,于是把找到的helper赋值给nfct_help(ct)->helper,而这个helper的help方法就是tftp_help()。
  • POST_ROUTING------>NF_IP_PRI_NAT_SRC  
  •        执行完SNAT后 执行 NF_IP_PRI_CONNTRACK_HELPER 级别的  ipv4_helper的时候,会去执行这个help方法,即tftp_help(),也就是建立一个期望连接(建立expect使用ct reply方向的ip port来建立,也就是snat后的数据)。 由于之前已经进行过SNAT,所以就调用nf_nat_tftp指向的函数
  • 最后执行NF_IP_PRI_CONNTRACK_CONFIRM 级别的 ipv4_confirm()回调;
  •      报文pkt 发送出去

PRE_ROUTING

所以ct->master被赋值为exp->master,并且,还会执行exp->expectfn()函数,这个函数上面提到是指向nf_nat_follow_master()的,该函数根据ct的master来给ct做NAT,

  但是PRE_ROUTING中也可以调用DNAT,但是好像没有那个链接,只能在expect中调用。

POST_ROUTING
根据ct的master来给ct做NAT也就是NAT

/* Setup NAT on this expected conntrack so it follows master. */
/* If we fail to get a free NAT slot, we'll get dropped on confirm */
void nf_nat_follow_master(struct nf_conn *ct,
struct nf_conntrack_expect *exp)
{
struct nf_nat_range range;

/* This must be a fresh one. */
BUG_ON(ct->status & IPS_NAT_DONE_MASK);

/* Change src to where master sends to */
range.flags = NF_NAT_RANGE_MAP_IPS;
range.min_addr = range.max_addr
= ct->master->tuplehash[!exp->dir].tuple.dst.u3;
nf_nat_setup_info(ct, &range, NF_NAT_MANIP_SRC);

/* For DST manip, map port here to where it's expected. */
range.flags = (NF_NAT_RANGE_MAP_IPS | NF_NAT_RANGE_PROTO_SPECIFIED);
range.min_proto = range.max_proto = exp->saved_proto;
range.min_addr = range.max_addr
= ct->master->tuplehash[!exp->dir].tuple.src.u3;
nf_nat_setup_info(ct, &range, NF_NAT_MANIP_DST);
}

 

nat_tftp 期望连接

 

 拓扑如下:

nat_tftp 期望连接

 

client--->server有:

org:192.168.5.1:50173 ---->192.168.10.100:69
reply: 192.168.10.100:69---->192.168.10.1:50173(SNAT)
expect:192.168.10.100:0---->192.168.10.1:50173

server--->client:

  • PRE_ROUTING处执行contrack_in(), 但是DNAT 不会执行,因为五元组不一样。做SNAT使用的port是69。
before expectfn:
org:192.168.10.100:3873---->192.168.10.1:50173
reply: 192.168.10.1:50173 ----> 192.168.10.100:3873

after expectfn:
org:192.168.10.100:3873---->192.168.10.1:50173
reply: 192.168.5.1:50173 ----> 192.168.10.100:3873
  •  POST_ROUTING时:由于ct->help 不存在不执行 ipv4_helper;最后直接执行ipv4_confirm();同时tuple 变成:
org:192.168.10.100:3873---->192.168.10.1:50173
reply: 192.168.5.1:50173 ----> 192.168.10.100:3873
那么对于后续的数据:则直接进行nat转换,不会去调用expect了,因为此时可以命中tuple了。hash 以及reply_hash都已经被confirm了


补充:

TFTP概述

(RFC 1350) 的简单协议。TFTP 在 UDP 顶部实施,其中目标端口 69 是众所周知的端口。

TFTP 应用层网关 (ALG) 处理 TFTP 数据包,以启动请求并创建针孔以允许数据包从反向返回。在流处理中,有两个会话用于一个 TFTP 对话,

一个是由读取请求 (RRQ) 或写入请求 (WRQ) 数据包创建的 TFTP 控制会话;另一个是由数据包(用于 RRQ)或确认 (ACK) 数据包(用于 WRQ)创建的 TFTP 数据会话。

  • 读请求包:Read Request,简写为RRQ,从TFTP服务器获取数据的请求,Opcode字段值为1
  • 写请求包:Write Request,简写为WRQ,向TFTP服务器写数据的请求,Opcode字段值为2;
  • 数据包:Data,简写为DATA,TFTP传输的文件数据,Opcode字段值为3;
  • 确认包:Acknowledgement,简写为ACK,对收到的传输文件数据的确认,Opcode字段值为4;
  • 差错包:Error,简写为ERROR,错误消息,Opcode字段值为5;
  • 选项确认包:Option Acknowledgement,简写为OACK,用于确认收到的TFTP选项(后来协议才加上的,当客户端的RRQ和WRQ包带option字段时,服务器响应OACK),Opcode字段值为6。

 

http代理服务器(3-4-7层代理)-网络事件库公共组件、内核kernel驱动 摄像头驱动 tcpip网络协议栈、netfilter、bridge 好像看过!!!! 但行好事 莫问前程 --身高体重180的胖子