nat&ftp

  之前ac上支持过ftp先关内容,dpdk上ftp的实现主要是拷贝内核ftp的实现。dpdk的代码涉及到公司业务,就不上传了。看内核协议栈代码

基本原理

  向连接跟踪子系统注册一个helper来跟踪FTP控制连接上传输的报文,通过搜索这些报文中的PORT(主动模式)和PASV(被动模式)命令,进而获取到即将要建立的期望连接信息(端口和IP地址)。

初始化

初始化时最关键的操作应该就是向连接跟踪子系统注册helper。初始化函数如下:

//该buffer用来拷贝skb中FTP控制连接报文内容,搜索关键字之前会先把报文拷贝到该
//buffer中,然后对buffer进行搜索,这样虽然会影响效率,但是实现简单
static char *ftp_buffer;
static DEFINE_SPINLOCK(nf_ftp_lock);

//虽然标准协议规定FTP的控制端口就是21,但是为了可扩展,内核在实现时,将可
//监听的控制端口做成了参数可配的,并且可以同时最多跟踪8个控制端口,下面分析
//代码时,为了理解方便,可以都视作只跟踪21号端口
#define MAX_PORTS 8
//要跟踪的控制端口号
static u_int16_t ports[MAX_PORTS];
//ports数组中当前指定了几个控制端口
static unsigned int ports_c;
module_param_array(ports, ushort, &ports_c, 0400);
//AF_INET和AF_INET6协议族各注册一个helper
static struct nf_conntrack_helper ftp[MAX_PORTS][2] __read_mostly;
//每个helper都有一个名字,这里保存名字,命名方法见下方init()
static char ftp_names[MAX_PORTS][2][sizeof("ftp-65535")] __read_mostly;

static int __init nf_conntrack_ftp_init(void)
{
    int i, j = -1, ret = 0;
    char *tmpname;
    //ftp_buffer用于保存FTP控制连接的应用层数据,最长为65535,其具体用法见下方help()
    ftp_buffer = kmalloc(65536, GFP_KERNEL);
    if (!ftp_buffer)
        return -ENOMEM;
    //如果模块加载时没有指定控制端口信息,那么默认只监听21号端口
    if (ports_c == 0)
        ports[ports_c++] = FTP_PORT;
    //初始化helper,然后将其向系统注册
    for (i = 0; i < ports_c; i++) {
        //初始化tuple(源和目的)
        ftp[i][0].tuple.src.l3num = PF_INET;
        ftp[i][1].tuple.src.l3num = PF_INET6;
        for (j = 0; j < 2; j++) {
            ftp[i][j].tuple.src.u.tcp.port = htons(ports[i]);
            ftp[i][j].tuple.dst.protonum = IPPROTO_TCP;
            //允许同时只存在1个未确认的期望连接
            ftp[i][j].max_expected = 1;
            //未确认的期望连接的超时时间为5分钟
            ftp[i][j].timeout = 5 * 60;    /* 5 Minutes */
            ftp[i][j].me = THIS_MODULE;
            //help()回调,非常重要,见下方
            ftp[i][j].help = help;
            //21号控制端口的helper名字为"ftp",其它端口的helper名字为"ftp-端口号"
            tmpname = &ftp_names[i][j][0];
            if (ports[i] == FTP_PORT)
                sprintf(tmpname, "ftp");
            else
                sprintf(tmpname, "ftp-%d", ports[i]);
            ftp[i][j].name = tmpname;
            //向连接跟踪子系统注册helper
            ret = nf_conntrack_helper_register(&ftp[i][j]);
            if (ret) {
                nf_conntrack_ftp_fini();
                return ret;
            }
        }
    }
    return 0;
}
View Code

help()实现关键点:

  1. 搜索关键字定义;
  2. 连接序号缓存;

搜索关键字

通过struct ftp_search定义了要搜索的一组关键字,以及一些搜索方法。

记住:为什么PORT就一定是ORGINAL方向,PASV在REPLY方向。这个分析 client--->server之间不同模式(port和pass模式)报文交互即可发现。

help()中,检查到一个命令行后,会调用find_pattern()进行关键字匹配,传入该函数的参数就是上面的struct ftp_search中的字段。

@data:要搜索的报文内容
@dlen:data的长度
@numoff:输出参数,如果搜索命中,记录地址信息在data中的偏移
@numlen: 输出参数,从numoff开始的numlen个字节为地址信息
@cmd:搜索命中后,调用getnum()填充该参数(将其作为第二个参数传入getnum()中)
@ret:搜索命中返回1,不命中返回0,如果只命中了一部分,那么返回-1表示失败
static int find_pattern(const char *data, size_t dlen,
    const char *pattern, size_t plen, char skip, char term, unsigned int *numoff,
    unsigned int *numlen, struct nf_conntrack_man *cmd,
    int (*getnum)(const char *, size_t, struct nf_conntrack_man *, char))
{
    size_t i;

    if (dlen == 0)
        return 0;
    if (dlen <= plen) {
        //报文长度比关键字还短,但是可以匹配一部分关键字,返回-1
        if (strnicmp(data, pattern, dlen) == 0)
            return -1;
        else return 0;
    }
    //不匹配返回0
    if (strnicmp(data, pattern, plen) != 0) 
        return 0;
    //命令已经匹配,下面尝试从命令中解析IP地址和端口号

    //先跳过skip字符
    for (i = plen; data[i] != skip; i++)
        if (i == dlen - 1) return -1;
    i++;
    //记录地址信息偏移,调用getnum()解析地址信息并记录numlen
    *numoff = i;
    *numlen = getnum(data + i, dlen - i, cmd, term);
    //命令匹配,但是没解析到任何地址参数,解析失败,返回-1
    if (!*numlen)
        return -1;
    //一切正常并且搜索命中,返回1
    return 1;
}

搜索过程非常直接,就是直接匹配FTP控制连接中的命令

连接序号缓存

FTP控制连接上的命令都是以行为单位进行收发的,即它们是有边界的,但是TCP的传输是字节流,所有在匹配时,就非常有必要先识别报文中数据边界,然后才能对其进行搜索等后续处理。

在实现时,定义了一个结构专门缓存当前收到的一些行的tcp序号,通过这些序号进行定界

#define NUM_SEQ_TO_REMEMBER 2
struct nf_ct_ftp_master {
    //如果一个报文以新行结尾,则其末尾序号+1就是一个新行的起始序号,
    //该序号就会被缓存到seq_aft_nl中(数组名是seq after newline的缩写)
    u_int32_t seq_aft_nl[IP_CT_DIR_MAX][NUM_SEQ_TO_REMEMBER];
    //记录了seq_aft_nl[]中当前缓存了几个序号
    int seq_aft_nl_num[IP_CT_DIR_MAX];
};

helper在注册时,会将该结构作为扩展信息注册到FTP控制连接的连接跟踪信息块中。help()函数中,在收到报文时,如果该报文以\n结尾则尝试更新缓存信息

static void update_nl_seq(u32 nl_seq, struct nf_ct_ftp_master *info, int dir,
              struct sk_buff *skb)
{
    unsigned int i, oldest = NUM_SEQ_TO_REMEMBER;
    //从当前已缓存项中找一个最老的,即序号最小的
    for (i = 0; i < info->seq_aft_nl_num[dir]; i++) {
        //如果要缓存的seq已经在缓存数组中了,当然无需更新了
        if (info->seq_aft_nl[dir][i] == nl_seq)
            return;
        //这个oldest的比较是个bug吧
        if (oldest == info->seq_aft_nl_num[dir] ||
            before(info->seq_aft_nl[dir][i], info->seq_aft_nl[dir][oldest]))
            oldest = i;
    }
    if (info->seq_aft_nl_num[dir] < NUM_SEQ_TO_REMEMBER) {
        //数组还没有满,放入下一个空闲位置即可,并且累加有效缓存项个数
        info->seq_aft_nl[dir][info->seq_aft_nl_num[dir]++] = nl_seq;
        nf_conntrack_event_cache(IPCT_HELPINFO_VOLATILE, skb);
    } else if (oldest != NUM_SEQ_TO_REMEMBER &&
           after(nl_seq, info->seq_aft_nl[dir][oldest])) {
        //数组已满,并且要缓存的序号确实大于该最老的缓存项时进行更新
        info->seq_aft_nl[dir][oldest] = nl_seq;
        nf_conntrack_event_cache(IPCT_HELPINFO_VOLATILE, skb);
    }
}

seq_aft_nl[]数组并没有按照有序数组来维护。但是它的更新原则还是非常简单的:就是数组没满时,直接往后累加,如果满了,那么将tcp序列号最小的一个替换

序列号缓存项查找

调用find_nl_seq()对缓存的序号进行查找。

static int find_nl_seq(u32 seq, const struct nf_ct_ftp_master *info, int dir)
{
    unsigned int i;

    for (i = 0; i < info->seq_aft_nl_num[dir]; i++)
        if (info->seq_aft_nl[dir][i] == seq)
            return 1;
    return 0;
}

  从help()实现中可以看到,更新缓存项时传入的seq时下一个命令的起始序号,查找缓存项时传入的seq是当前报文的起始序号。仔细想,这是合理的,因为这样可以保证如果查找时序号如果命中,就可以说明当前报文就是一个命令行的开始,这样就可以进行前面的关键字搜索等后续处理了。

help()回调

  实现FTP协议连接跟踪功能的核心是help(),从连接跟踪子系统之helper
中有看到,当非期望连接的skb在建立新的连接时,会根据Reply方向的tuple查找系统中已注册的helper,如果找到就会将该helper信息记录到连接跟踪信息块中,然后就会在help钩子处,调用helper中的help()回调

static int help(struct sk_buff *skb, unsigned int protoff, struct nf_conn *ct,
        enum ip_conntrack_info ctinfo)
{
    unsigned int dataoff, datalen;
    struct tcphdr _tcph, *th;
    char *fb_ptr;
    int ret;
    u32 seq;
    int dir = CTINFO2DIR(ctinfo);
    unsigned int matchlen, matchoff;
    //上面介绍的序号缓存信息
    struct nf_ct_ftp_master *ct_ftp_info = &nfct_help(ct)->help.ct_ftp_info;
    struct nf_conntrack_expect *exp;
    union nf_inet_addr *daddr;
    struct nf_conntrack_man cmd = {};
    unsigned int i;
    int found = 0, ends_in_nl;
    typeof(nf_nat_ftp_hook) nf_nat_ftp;

    //我们关注的控制命令只可能出现在ESTABLISHTED连接上
    if (ctinfo != IP_CT_ESTABLISHED && ctinfo != IP_CT_ESTABLISHED+IP_CT_IS_REPLY) 
        return NF_ACCEPT;
    //protoff是tcp协议距离skb->data的偏移,将TCP首部拷贝到_tcph中,
    //并且th指向_tcph,既数据包中tcp的首部
    th = skb_header_pointer(skb, protoff, sizeof(_tcph), &_tcph);
    if (th == NULL)
        return NF_ACCEPT;
    //dataoff为应用层数据距离skb->data的偏移量
    dataoff = protoff + th->doff * 4;
    if (dataoff >= skb->len)
        return NF_ACCEPT;
    //datalen就是skb中剥除所有协议首部后剩余应用层数据的长度
    datalen = skb->len - dataoff;

    spin_lock_bh(&nf_ftp_lock);
    //将FTP应用层数据拷贝到ftp_buffer中,ftp_ptr指向ftp_buffer
    fb_ptr = skb_header_pointer(skb, dataoff, datalen, ftp_buffer);
    BUG_ON(fb_ptr == NULL);
    //ends_in_nl标志当前报文的最后一个字节是否以'\n'结尾,一般情况下都应如此
    ends_in_nl = (fb_ptr[datalen - 1] == '\n');
    //seq是报文的最后一个字节的序号+1
    seq = ntohl(th->seq) + datalen;
    //查找序号缓存项,如果没有找到,则尝试更新缓存信息后结束,
    //因为这个数据包的第一个字节不是一个命令行的开头
    if (!find_nl_seq(ntohl(th->seq), ct_ftp_info, dir)) {
        ret = NF_ACCEPT;
        goto out_update_nl;
    }
    //从连接跟踪信息块中找到skb传输方向上的地址信息
    cmd.l3num = ct->tuplehash[dir].tuple.src.l3num;
    memcpy(cmd.u3.all, &ct->tuplehash[dir].tuple.src.u3.all, sizeof(cmd.u3.all));
    //搜索数据包的内容,从中寻找是否有特定的关键字(PORT和PASV信息)
    for (i = 0; i < ARRAY_SIZE(search[dir]); i++) {
        found = find_pattern(fb_ptr, datalen, search[dir][i].pattern,
                    search[dir][i].plen, search[dir][i].skip, search[dir][i].term,
                    &matchoff, &matchlen, &cmd, search[dir][i].getnum);
        if (found)
            break;
    }
    //虽然连接跟踪本意上不应该丢包,但是遇到这种部分匹配的异常情况,
    //说明传输确实是有问题的,直接丢弃也未尝不可
    if (found == -1) {
        ret = NF_DROP;
        goto out;
    } else if (found == 0) {
        //没有包含关注的关键字,尝试更新序列号缓存信息后结束        
        ret = NF_ACCEPT;
        goto out_update_nl;
    }
    //找到了我们关心的信息,说明即将要建立一个数据连接(即期望连接),
    //所以分配一个期望并对其进行初始化,然后将其加入期望连接表中,这样
    //等期望连接的数据包到达时就可以匹配到了
    exp = nf_ct_expect_alloc(ct);
    if (exp == NULL) {
        ret = NF_DROP;
        goto out;
    }
    //计算期望连接的tuple信息
    daddr = &ct->tuplehash[!dir].tuple.dst.u3;
    /* Update the ftp info */
    if ((cmd.l3num == ct->tuplehash[dir].tuple.src.l3num) &&
        memcmp(&cmd.u3.all, &ct->tuplehash[dir].tuple.src.u3.all, sizeof(cmd.u3.all))) {
        /* Thanks to Cristiano Lincoln Mattos
           <lincoln@cesar.org.br> for reporting this potential
           problem (DMZ machines opening holes to internal
           networks, or the packet filter itself). */
        if (!loose) {
            ret = NF_ACCEPT;
            goto out_put_expect;
        }
        daddr = &cmd.u3;
    }
    //初始化期望连接结构
    nf_ct_expect_init(exp, cmd.l3num, &ct->tuplehash[!dir].tuple.src.u3, daddr,
              IPPROTO_TCP, NULL, &cmd.u.tcp.port);

    /* Now, NAT might want to mangle the packet, and register the
     * (possibly changed) expectation itself. */
    //如果NAT会处理该数据包,则交给NAT处理,否则将其加入到期望连接跟踪表中
    nf_nat_ftp = rcu_dereference(nf_nat_ftp_hook);
    if (nf_nat_ftp && ct->status & IPS_NAT_MASK)
        ret = nf_nat_ftp(skb, ctinfo, search[dir][i].ftptype, matchoff, matchlen, exp);
    else {
        /* Can't expect this?  Best to drop packet now. */
        if (nf_ct_expect_related(exp) != 0)
            ret = NF_DROP;
        else
            ret = NF_ACCEPT;
    }
out_put_expect:
    nf_ct_expect_put(exp);
out_update_nl:
    //只有当前数据包是以'\n'结尾时才更新序号缓存数组
    if (ends_in_nl)
        update_nl_seq(seq, ct_ftp_info, dir, skb);
 out:
    spin_unlock_bh(&nf_ftp_lock);
    return ret;
}

Pasv模式:    

  当客户端C与服务端S建立控制连接后,若使用Pasv模式,那么客户端C会向服务端S发送一条Pasv命令,服务端S会对该命令发送回应信息,这个信息是:服务端S在本地打开了一个端口M,你现在去连接我吧。当客户端C收到这个信息后,就可以向服务端S的M端口进行连接,连接成功后,数据连接也就建立了。

 例:客户端->服务器端:PASV 
客户端<-服务器端:227 Entering Passive Mode (192,168,1,2,14,26). 

NAT+FTP

  如果是NAT模式,那数据包里服务器给的连接的动态端口和IP地址,需要做NAT转换,否则客户端使用此ip+port会不通,毕竟一个外网ip 对客户段不知。
/* FIXME: Time out? --RR */

static int nf_nat_ftp_fmt_cmd(struct nf_conn *ct, enum nf_ct_ftp_type type,
                  char *buffer, size_t buflen,
                  union nf_inet_addr *addr, u16 port)
{
    switch (type) {
    case NF_CT_FTP_PORT:
    case NF_CT_FTP_PASV:
        return snprintf(buffer, buflen, "%u,%u,%u,%u,%u,%u",
                ((unsigned char *)&addr->ip)[0],
                ((unsigned char *)&addr->ip)[1],
                ((unsigned char *)&addr->ip)[2],
                ((unsigned char *)&addr->ip)[3],
                port >> 8,
                port & 0xFF);
    case NF_CT_FTP_EPRT:
        if (nf_ct_l3num(ct) == NFPROTO_IPV4)
            return snprintf(buffer, buflen, "|1|%pI4|%u|",
                    &addr->ip, port);
        else
            return snprintf(buffer, buflen, "|2|%pI6|%u|",
                    &addr->ip6, port);
    case NF_CT_FTP_EPSV:
        return snprintf(buffer, buflen, "|||%u|", port);
    }

    return 0;
}

/* So, this packet has hit the connection tracking matching code.
   Mangle it, and change the expectation to match the new version. */
static unsigned int nf_nat_ftp(struct sk_buff *skb,
                   enum ip_conntrack_info ctinfo,
                   enum nf_ct_ftp_type type,
                   unsigned int protoff,
                   unsigned int matchoff,
                   unsigned int matchlen,
                   struct nf_conntrack_expect *exp)
{
    union nf_inet_addr newaddr;
    u_int16_t port;
    int dir = CTINFO2DIR(ctinfo);
    struct nf_conn *ct = exp->master;
    char buffer[sizeof("|1||65535|") + INET6_ADDRSTRLEN];
    unsigned int buflen;

    pr_debug("FTP_NAT: type %i, off %u len %u\n", type, matchoff, matchlen);

    /* Connection will come from wherever this packet goes, hence !dir */
    newaddr = ct->tuplehash[!dir].tuple.dst.u3;// reply 方向的目的ip,也就是对应内网ip
    exp->saved_proto.tcp.port = exp->tuple.dst.u.tcp.port;//保留以前使用的tcp port
    exp->dir = !dir;

    /* When you see the packet, we need to NAT it the same as the
     * this one. */  // 后面做nat使用
    exp->expectfn = nf_nat_follow_master;

    /* Try to get same port: if not, try to change it. */
    for (port = ntohs(exp->saved_proto.tcp.port); port != 0; port++) {
        int ret;

        exp->tuple.dst.u.tcp.port = htons(port);
        ret = nf_ct_expect_related(exp);
        if (ret == 0)
            break;
        else if (ret != -EBUSY) {
            port = 0;
            break;
        }
    }

    if (port == 0) {
        nf_ct_helper_log(skb, ct, "all ports in use");
        return NF_DROP;
    }

    buflen = nf_nat_ftp_fmt_cmd(ct, type, buffer, sizeof(buffer),
                    &newaddr, port);// 保存nat后的ip+port
    if (!buflen)
        goto out;

    pr_debug("calling nf_nat_mangle_tcp_packet\n");

    if (!nf_nat_mangle_tcp_packet(skb, ct, ctinfo, protoff, matchoff,
                      matchlen, buffer, buflen))//修改skb通知port+ip内容;以及修改tcp的seq,并记录修改前修改后 seq的差值。
                      //后面在 ipv4_confirm 调整序列号。
        goto out;

    return NF_ACCEPT;

out:
    nf_ct_helper_log(skb, ct, "cannot mangle packet");
    nf_ct_unexpect_related(exp);
    return NF_DROP;
}

调整seq

在重新写入经过nat后的ip+port后,如果ipload 长度有变化,比如port从77777 变为6666, 四位数变为三位数会记录当前相对seq的差值。并打上标签IPS_SEQ_ADJUST_BIT,后续调整tcp的seq。

int nf_ct_seqadj_set(struct nf_conn *ct, enum ip_conntrack_info ctinfo,
             __be32 seq, s32 off)
{
    struct nf_conn_seqadj *seqadj = nfct_seqadj(ct);
    enum ip_conntrack_dir dir = CTINFO2DIR(ctinfo);
    struct nf_ct_seqadj *this_way;

    if (off == 0)
        return 0;

    if (unlikely(!seqadj)) {
        WARN_ONCE(1, "Missing nfct_seqadj_ext_add() setup call\n");
        return 0;
    }

    set_bit(IPS_SEQ_ADJUST_BIT, &ct->status);

    spin_lock_bh(&ct->lock);
    this_way = &seqadj->seq[dir];
    if (this_way->offset_before == this_way->offset_after ||
        before(this_way->correction_pos, ntohl(seq))) {
        this_way->correction_pos = ntohl(seq);
        this_way->offset_before     = this_way->offset_after;
        this_way->offset_after    += off;
    }
    spin_unlock_bh(&ct->lock);
    return 0;
}

调整tcp的seq

static unsigned int ipv4_confirm(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);/* ct结构从下面获得: (struct nf_conn *)skb->nfct; */
     /* 未关联,或者是 已建立连接的关联连接的响应 不明白*/
    if (!ct || ctinfo == IP_CT_RELATED_REPLY)
        goto out;

    /* adjust seqs for loopback traffic only in outgoing direction 
         /* 有调整序号标记,且不是环回包,调整序号 */
    */
    if (test_bit(IPS_SEQ_ADJUST_BIT, &ct->status) &&
        !nf_is_loopback_packet(skb)) {
        if (!nf_ct_seq_adjust(skb, ct, ctinfo, ip_hdrlen(skb))) {
            NF_CT_STAT_INC_ATOMIC(nf_ct_net(ct), drop);
            return NF_DROP;
        }
    }
out:
    /* We've seen it coming out the other side: confirm it */
    return nf_conntrack_confirm(skb);
}

在netfiler最后关头,根据IPS_SEQ_ADJUST_BIT来判断是否需要调整tcp的seq(tcph->seq 以及 tcph->ack_seq)

    newseq = htonl(ntohl(tcph->seq) + seqoff);
    newack = htonl(ntohl(tcph->ack_seq) - ackoff);

    inet_proto_csum_replace4(&tcph->check, skb, tcph->seq, newseq, false);
    inet_proto_csum_replace4(&tcph->check, skb, tcph->ack_seq, newack,
                 false);

    pr_debug("Adjusting sequence number from %u->%u, ack from %u->%u\n",
         ntohl(tcph->seq), ntohl(newseq), ntohl(tcph->ack_seq),
         ntohl(newack));

    tcph->seq = newseq;
    tcph->ack_seq = newack;

 

主动模式举例:

  客户端发送PORT xxx,xxx,xxx,xxx,ppp,ppp。NAT设备通过help函数捕获了该消息。因为客户端是一个私网地址,其port命令中的地址也是一个私网地址,如果直接发送给服务器端,那么服务器端将不能连接该地址。所以,NAT设备需要为数据连接选择一个公网地址(可以是和主链接一样的地址,也可以是别的地址)和一个新的端口号,进行地址转换,将转换后的地址重新填充到PORT命令中。假设客户端发送的PORT命令为PORT 10.10.10.10 10000。经过NAT设备时,给其替换成1.1.1.1 1000。那么服务器将会连接客户端的(1.1.1.1 10000)。NAT设备必须为子连接的请求方向构建期望连接,即为

sip/mask      = rip/0xffffffff(由于源IP可以由服务器重新指定,所以这里直接设置母连接服务器端的IP地址也是不准确的,但是大多数正常情况是这样的)
sport/mask    = 0/0
dip/mask      = 1.1.1.1/0xffffffff
dport/mask    = 10000/0xffff
protocol = tcp

 

  • 1.首包在prerouting或者在output节点上,在函数resolve_normal_ct中会创建连接跟踪。如果存在期望连接的话,则关联其对应的master连接,并且设置新创建的连接跟踪的状态为IP_CT_RELATED,若果没有对应的期望连接,则设置其状态为IP_CT_NEW。正常情况下,首包只有这两种状态。
  • 2.对于第一个应答包和后续应答包也是在prerouting或者在output节点上,一般在函数resolve_normal_ct中可以找到对应的处于IP_CT_RELATED或者IP_CT_NEW状态的连接跟踪,找到后设置其状态为IP_CT_ESTABLISHED_REPLY
  • 3.对于非首个请求报文,在prerouting或者在output节点上,函数resolve_normal_ct中可以找到对应的连接跟踪,一般会将其设置为IP_CT_ESTABLISHED。
  • 4.对于ICMP错误报文(比如源抑制,ttl超时,不可达报文)到达netfilter后,会根据ICMP携带的原始报文查找其所属的CT,如果该ICMP差错报文是CT的请求方向报文产生的,那么设置其状态为IP_CT_RELATED,如果是应答方向的报文产生的则设置为IP_CT_RELATED_REPLY。
  • 5.很重要的一点,对于子连接的首包,会在函数init_conntrack中创建连接跟踪,并查找到其对应的子连接(先创建连接跟踪,然后查找期望连接进行关联),在离开init_conntrack函数之前执行对应的expectfn函数。

假设服务器端以2.2.2.2 20连接客户端1.1.1.1 10000。那么NAT将会为数据通道创建新的连接跟踪为:

org方向
dip      = 1.1.1.1
dport    = 10000
sip      = 2.2.2.2
sport    = 20
protocol = tcp

reply方向:
dip      = 2.2.2.2
dport    = 20
sip      = 1.1.1.1
sport    = 10000
protocol = tcp

这样的连接跟踪创建后,请求方向的报文都可以找到对应的连接跟踪,但是客户端收到的请求报文是经过dnat方向操作后的报文:

org:
dip = 10.10.10.10 dport = 10000 sip = 2.2.2.2 sport = 20 protocol = tcp
reply:
dip = 2.2.2.2 dport = 20 sip = 10.10.10.10 sport = 10000 protocol = tcp

  是无法命中连接跟踪的,毕竟之前连接是(2.2.2.2<------>1.1.1.1zz之间的)。

所以对于这种情况下的子连接需要做两件事:

  • 1.为子连接构建nat信息,在nat模块中将根据这些信息进行nat操作。
  • 2.修正连接跟踪的应答方向五元组,使客户端报文能命中连接跟踪。

这两件事由谁来做了?

答案就是expect函数

  对于ftp来说,该函数为nf_nat_follow_master。从上面可以知道,数据通道请求方向需要做DNAT。其中exp->dir的值为主连接设置的,该值为help函数收到PORT命令时的方向的反方向(主动模式为请求方向,那么反方向为应答方向),所以exp->dir的值为IP_CT_DIR_REPLY。

具体情况可以参考tftp逻辑来分析

 

posted @ 2023-03-13 16:35  codestacklinuxer  阅读(101)  评论(0编辑  收藏  举报