FTP协议有两种工作方式:PORT方式和PASV方式,中文意思为主动式和被动式。
Port模式:ftp server:tcp 21
server:tcp 20 ------>client:dynamic
Pasv模式:ftp server:tcp 21
server:tcp dynamic
PORT(主动)方式的连接过程是:客户端向服务器的FTP端口(默认是21)发送连接请求,服务器接受连接,建立一条命令链路。当需要传送数据时,客户端在命令链路上用PORT命令告诉服务器:“我打开了XXXX端口,你过来连接我”。于是服务器从20端口向客户端的XXXX端口发送连接请求,建立一条数据链路来传送数据。
PASV(被动)方式的连接过程是:客户端向服务器的FTP端口(默认是21)发送连接请求,服务器接受连接,建立一条命令链路。当需要传送数据时,服务器在命令链路上用PASV命令告诉客户端:“我打开了XXXX端口,你过来连接我”。于是客户端向服务器的XXXX端口发送连接请求,建立一条数据链路来传送数据。
iptables模块关系:
1、modprobe ip_tables
当 iptables 对 filter、nat、mangle 任意一个表进行操作的时候,会自动加载 ip_tables
模块
另外,iptable_filter、iptable_nat、iptable_mangle 模块也会自动加载。
2、modprobe ip_conntrack
ip_conntrack 是状态检测机制,state 模块要用到
当 iptables -A INPUT -m state --state RELATED,ESTABLISHED -j ACCEPT
时,ip_conntrack 自动加载。
3、modprobe ip_conntrack_ftp
ip_conntrack_ftp 是本机做 FTP 时用的。
ip_nat_ftp 是通过本机的 FTP 需要用到的(若你的系统不需要路由转发,没必要用这个)
当 modprobe ip_nat_ftp 时,系统自动会加载 ip_conntrack_ftp 模块。
具体设置
加载模块(ports=21可以不写,如果用其他端口时要加上):
很多协议的控制信息在应用层数据中被包含,这些信息直接影响到了链路的建立,比如ftp协议就是这样,ftp分为port模式和pass模式,port模式中,起初client连接server的21端口,然后当需要传输data的时候,client发送一个控制包给server,包中包含client端开启的端口和自己的ip地址,server收到之后用自己的20端口去连接client控制包中建议的ip和端口,在这种情况下,如果client在nat后面使用私网地址,那么server将无法连接client,因此nat网关必须要处理这种情况,处理方式就是修改client发给server的控制包(如果加密将不可能修改,还好ftp是不加密的);在pass模式下,client连接server的21端口后,如果要传输data,client还要连接server的另一个随机端口,该端口是由server发送的控制包传给client的,如果client或者server端所在的防火墙禁止了任意非熟知端口,那么数据将被防火墙拦截;不管是port模式还是pass模式,防火墙都要处理“第二个”数据连接通路的放行问题,在linux中是通过RELATED状态来放行的,正如前文所述,只需配置一条--state
RELATED -j
ACCEPT规则即可,但是具体这个规则如何实现,linux的连接追踪模块又是怎样处理ftp的nat问题的,本文详述之。
首先从ip_conntrack的HOOK函数说起:
unsigned int ip_conntrack_in(...)
{
...
proto =
ip_ct_find_proto((*pskb)->nh.iph->protocol);
//从数据包中取出协议号
...//resolve_normal_ct会试图在已建立的连接中寻找刚进入的包属于的连接,如果找不到则新建立一个状态为NEW的连接,同时还要初始化该连接相关的数据,比如helper
ct =
resolve_normal_ct(*pskb,
proto,&set_reply,hooknum,&ctinfo);//此中调用的init_conntrack函数是一个做了很多事的函数
...
if (ret !=
NF_DROP &&
ct->helper) { //如果有helper则调用其help函数
ret = ct->helper->help(*pskb, ct,
ctinfo);
...
}
...
}
init_conntrack中有如下逻辑:
...//从链表中查找该连接,如果找到说明这是一个“预测”的连接
expected = LIST_FIND(&ip_conntrack_expect_list,
expect_cmp,
struct ip_conntrack_expect *, tuple);
...
if (expected) {
__set_bit(IPS_EXPECTED_BIT,
&conntrack->status);
//预测的连接到了,设置一个标志,在resolve_normal_ct得到已有连接的情况下会判断如果有了这个标志,则设置IP_CT_RELATED状态,该状态可用于filter的判断
expected->sibling = conntrack;
//预测的连接已经到来并且初始化了。expected->sibling在预测的时候是NULL,因为那时仅仅是预测,连接还没有真的到来,后面可以看到,ip_conntrak预测之后,ip_nat会使用预测结果,然后调用helper的help修改应用层的和连接相关的控制数据,比如ip地址和端口信息,在遍历一个已有连接的所有预测到的连接从而决定是否调用ip_nat的helper时,如果一个预测即一个ip_conntrack_expect的sibling字段非NULL,ip_nat将跳过此预测结果,因为它已经是真实的连接了,说明已经在它还是预测的连接的时候就已经被help过了。
...
}
...
每一个ip_conntrack都可以拥有多个helper,用于帮助处理连接相关的信息,比如ftp协议穿越防火墙就需要处理nat和副连接(data连接)问题,因此就有必要用一个helper模块来处理这一类情况,处理ftp
nat的helper和处理副连接的helper其实不是一类helper,前者是ip_nat_ftp结构体,后者是ip_conntrack_ftp结构体,虽然不同,但是它们的处理逻辑和注册逻辑都是一样的,因此到后面说ftp
nat的时候再统一说明。下面是ip_conntrack_ftp注册的help函数的实现逻辑
static int help(...)
{
...//操作skb,取出我们需要的一切信息
skb_copy_bits(skb, dataoff, ftp_buffer, skb->len -
dataoff);
...
array[0] =
(ntohl(ct->tuplehash[dir].tuple.src.ip)
>> 24) & 0xFF;
array[1] =
(ntohl(ct->tuplehash[dir].tuple.src.ip)
>> 16) & 0xFF;
array[2] =
(ntohl(ct->tuplehash[dir].tuple.src.ip)
>> 8) & 0xFF;
array[3] =
ntohl(ct->tuplehash[dir].tuple.src.ip)
& 0xFF;
//以上的这个array就是server需要连接的ip地址
for (i = 0;
i < ARRAY_SIZE(search); i++) {
if (search[i].dir != dir) continue;
found =
find_pattern(...);//在ftp_buffer中寻找search字符,如果找到了,则说明本次数据包需要help,其中有个参数是个数组,数组的每一个元素都是一个匹配键,此谓search,是一个ftp_search结构体类型的数组
if (found) break;
}
...//如果找不到则返回,说明本次到来的数据不需要help
exp =
ip_conntrack_expect_alloc();
...
//初始化一个ip_conntrack_expect,可以用于描述一个将要建立的连接
exp->expectfn = NULL;
ip_conntrack_expect_related(exp, ct);
//准备添加一个RELATED的连接,如果用户在iptables规则中配置RELATED连接可以通过,那么ftp的port模式数据连接就可以畅行无阻了。iptables的RELATED连接就是在这里被“预料”到的,然后加入进已有的连接。
ret =
NF_ACCEPT;
out:
UNLOCK_BH(&ip_ftp_lock);
return
ret;
}
最终会在ip_conntrack_expect_insert函数中将“预料”到的连接加入与此“预料”的连接相关联的已有连接的链表中,同时还将这个预料到的连接加入一个系统全局的链表中,并且如果已有的连接需要限制“预料”连接的建立连接时间,则需要启动一个定时器,定时器超时连接还不到的话,就会删除该预料的连接。这个related连接会被netfilter的state模块使用,比如你使用--state
NEW/ESTABLISHED/...的话,在state模块中的match回调函数中,系统会取出该数据包属于的连接,然后取出该连接的state,将之与参数的state比较,然后返回进入target抉择。
以上是数据在ip_conntrack模块中的流程,出了ip_conntrack就该进入ip_nat了,还是从其HOOK说起:
static unsigned int ip_nat_fn(...)
{
...
ct =
ip_conntrack_get(*pskb, &ctinfo);
//得到连接,如果没有得到则返回NULL
... //如果没有得到既有连接则返回ACCEPT(注意有ICMP重定向的特殊情况),由后续的链来抉择,不管怎样nat总在conntrack之后起作用,因此只要有连接,conntrack就会将之加入hash
switch
(ctinfo) {
...
case
IP_CT_NEW: //如果是一个连接的第一个包,那么就要初始化一系列结构体,包括两个方向的nat转换表,ftp等等需要help的协议的相关结构体等等
info = &ct->nat.info;
WRITE_LOCK(&ip_nat_lock);
if (!(info->initialized & (1
<< maniptype))) {
...
ret = ip_nat_rule_find(pskb, hooknum, in, out, ct, info);
...
return
do_bindings(ct, ctinfo, info, hooknum, pskb);
}
unsigned int do_bindings(...)
{
...
int proto =
(*pskb)->nh.iph->protocol;
...//实施地址/端口转换,省略。就是在两个方向的转换表中根据方向和地址/端口信息来修改数据包的协议头
helper =
info->helper; //info在ip_nat_setup_info也就是初始化连接的时候就会被建立,这里只是取出来
if (helper)
{
...//一个主连接可以有多个与之RELATED的副连接,因此下面就遍历这些副连接
list_for_each_prev(cur_item,
&ct->sibling_list) {
...//如果已经是established的连接了,则说明下面将要做的工作已经作过了,就不再做了。
if (exp_for_packet(exp, *pskb)) { //包合理则调用help函数,在help函数中处理特殊的nat转换,比如ftp的port模式相关的nat转换
ret = helper->help(ct, exp, info, ctinfo, hooknum,
pskb);
...
}
对于ip_nat_ftp帮助模块而言,其help函数的执行逻辑如下:
static unsigned int help(...)
{
...
ct_ftp_info
= &exp->help.exp_ftp_info;
...
ftp_data_fixup(ct_ftp_info, ct, pskb, ctinfo, exp);
...
}
最终ftp_data_fixup调用了mangle[ct_ftp_info->ftptype](...)函数,显然最后的函数完成了对数据包的修改,对数据包进行修改就是为了ftp服务器可以成功连接到客户端。由于客户端很多时候在具有nat功能的防火墙后,并且都是用私网地址,而在ftp的port模式下,如果客户端将一个私有地址建议给了ftp服务器用于连接,服务器是连接不到的,这个建议的ip地址在ftp的数据包中,因此必须修改数据包,将建议的地址和端口修改为nat后的地址和端口,同时再铺设一条nat,用于服务器连接客户端时将请求真正转到内网的客户端。ip_nat_mangle_tcp_packet是ip_nat_helper.c中的一个很重要的函数,就是它完成了对应用层数据包的修改。
ip_conntrack和ip_nat处理ftp总的过程就是,ip_conntrack模块得到了连接的信息,然后根据连接信息可以得到一个helper,需要说明,一个连接完全可以没有helper,而且大多数的都没有helper,是否需要helper是根据连接的类型决定的,一般触及应用层控制数据的修改时才会使用helper,比如ftp的控制命令是在应用层数据中被传输的,连接的类型是可以从数据包以及协议头中得到的,因此ip_conntrack模块需要数据包不能分段,也就是说需要完整的ip数据包。得到helper之后开始调用其help函数,然后判断当前数据包是否需要help,比如判断是否是ftp的特殊命令,该命令可以建立一条新的连接,如果是这样的话,那么help函数则“预测”到一条即将建立的连接并将之和当前连接关联,然后ip_conntrack基本就没有什么做的了,数据包继续在netfilter中流动,进入nat,同样的,nat也如ip_conntrack判断是否需要help,如果是则调用helper的help函数,判断是否需要help的依据一般就是是否在ip_conntrack模块中“预测”到了即将建立的连接,如果预测到了,那么就调用nat的helper的help函数,并且将预测到的连接参数传入,在ip_nat_ftp的help函数中根据预测连接的信息对应用层控制数据进行修改。
两类helper的注册都是在模块初始化的时候进行的,而helper与连接或者nat的绑定则是在连接初始化的时候进行的。ip_nat_fn是nat的HOOK,其中对于IP_CT_NEW包来讲需要调用call_expect,而后者最终调用下面的函数实现ftp相关的ip_nat_helper结构体的指定,该结构体在模块初始化时被注册:
unsigned int ip_nat_setup_info(...)
{
...
info->helper = LIST_FIND(&helpers,
helper_cmp, struct ip_nat_helper *, &reply);
//寻找ip_nat_ftp的helper,在ip_nat_ftp的init中,会调用ip_conntrack_helper_register将ftp相关的信息注册进内核,这些信息包含在ip_nat_helper结构体中,其中有很多静态数据是用于匹配helper的,比如新建一个连接,当数据越过conntrack而进入nat时会调用ip_nat_setup_info,在该函数中,如上述调用LIST_FIND,其实就是使用当前的addr,port等信息和注册的helper逐个进行比较,一旦有命中的则将此helper取出留作后用,ip_nat_helper中最重要的就是help函数了。
...
}
ip_nat_ftp模块的初始化函数如下:
static int __init init(void)
{
...
for (i = 0;
(i < MAX_PORTS) &&
ports[i]; i++) {
ftp[i].tuple.src.u.tcp.port = htons(ports[i]);
ftp[i].tuple.dst.protonum = IPPROTO_TCP;
ftp[i].mask.src.u.tcp.port = 0xFFFF;
ftp[i].mask.dst.protonum = 0xFFFF;
ftp[i].max_expected = 1;
ftp[i].timeout = 0;
ftp[i].flags = IP_CT_HELPER_F_REUSE_EXPECT;
ftp[i].me = ip_conntrack_ftp;
ftp[i].help = help;
...
ret = ip_conntrack_helper_register(&ftp[i]);
...
}
return
0;
}
类似的ip_conntrack的helper也是在模块初始化时注册,在连接初始化时被指定特定的连接的,道理和nat是一样的。
总之,helper模块一般是对需要在应用层数据中传输控制数据的协议进行帮助的,因为OS实现的协议栈并不包含应用层,但是有的时候必须对应用层控制数据进行修改,这时就不得不需要一个额外的帮助模块了,注意,一般helper修改的都是控制数据,而不是业务数据,所谓控制数据就是和业务无关的,仅仅影响到连接本身的数据。