nat_tftp 期望连接

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

TFTP协议穿越SNAT

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

 

  防火墙设备上配置了私网地址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()。
  •   当tftp请求包走到POST_ROUTING------>NF_IP_PRI_NAT_SRC   先执行SNAT
  •        执行完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 发送出去


  当后续tftp传输数据时(server--->client),在PRE_ROUTING 的nf_conntrack_in里面,新建tuple后,在expect_hash表中查可以匹配到新建tuple的期望连接(因为只根据源端口来匹配),因此上面代码的if成立,

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

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

  在后续的POST_ROUTING 调用 ipv4_helper以及 ipv4_confirm中,由于ct没有期望连接,所以跳过helper有关的代码;最后直接调用nf_conntrack_confirm()。
根据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);
}

 

 

 拓扑如下:

 

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概述

  简单文件传输协议 (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。

 

posted @ 2023-03-09 18:08  codestacklinuxer  阅读(99)  评论(0编辑  收藏  举报