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; }
help()实现关键点:
- 搜索关键字定义;
- 连接序号缓存;
搜索关键字
通过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逻辑来分析。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 阿里最新开源QwQ-32B,效果媲美deepseek-r1满血版,部署成本又又又降低了!
· SQL Server 2025 AI相关能力初探
· AI编程工具终极对决:字节Trae VS Cursor,谁才是开发者新宠?
· 开源Multi-agent AI智能体框架aevatar.ai,欢迎大家贡献代码
· Manus重磅发布:全球首款通用AI代理技术深度解析与实战指南