在 Linux 中,Netfilter 自带一个 conntrack 模块,于是就先以 ftp 协议为例研究了一下。其主要原理是,监控相应端口的流量,尝试获取到含有动态端口的数据包,然后,将相应端口的数据包标记成RELATED的数据(猜测是这样),因而,使其实现通信。
实现 ftp 动态端口的模块 文件是 net/netfilter/nf_conntrack_ftp.c,include/linux/netfilter/nf_conntrack_ftp.h,include/uapi/linux/netfilter/nf_conntrack_ftp.h,net/netfilter/nf_nat_ftp.c,还有一个 net/netfilter/ip_vs_ftp.c 文件暂时没看懂干啥的。这些文件均来自于 linux 内核文件。在 Ubuntu 18.04 系统中,以 apt-get install linux-source-5.0.0 的方式获取。
测试系统 Ubuntu 18.04,内核版本:5.0.0-31-generic,iptables 版本:1.6.1
首先,了解如何开启linux系统自带的 ftp 协议的动态端口模块。
vim /etc/sysctl.conf
net.netfilter.nf_conntrack_helper=1
sysctl -p
使其生效。modprobe nf_conntrack
modprobe nf_conntrack_ftp
iptables -F # 清空规则
iptables -P INPUT ACCEPT
iptables -P OUTPUT ACCEPT
iptables -P FORWARD DROP # FORWARD链规则默认DROP
iptables -A FORWARD -p tcp -m state --state NEW --dport 21 -j ACCEPT
iptables -A FORWARD -p tcp -m conntrack --ctstate ESTABLISHED,RELATED -j ACCEPT
# 按照对上面那篇文章的理解,应该是用下面这个规则的,
# 但是,测试发现,只用下面这个反而通信不了,用上面那句规则,模块是会工作的。有点迷,可能没理解好。
# iptables -A FORWARD -m conntrack --ctstate ESTABLISHED,RELATED -m helper --helper opc -p tcp -j ACCEPT
此外,还可能需要要一条这样的规则iptables -A PREROUTING -t raw -p tcp --dport 21 -j CT --helper ftp
注意:
(1)必须要用-D选项删除上面这条规则后,nf_conntrack_ftp模块才能被卸载,否则卸载时会提示被使用。
(2)应使用 iptables -t raw-nL 查看这条规则的配置。
此处在 FORWARD 链上测试,也可自行改成INPUT和OUTPUT链,Ubuntu 18.04 使用 bridge_utils 配置网桥后,需要额外加载模块,FOWWARD 链规则才生效,相关操作可见这篇帖子。
5. 开启 ftp 服务器后,使用 winscp 客户端对其进行访问测试。
默认情况下,winscp 是用 被动模式通信的,被动模式的确需要动态端口开放这这一策略。
不过首先要明确一点,防火墙上做动态端口开放,显然要求协议是明文的,加密是不行的,加密就没法从数据包里获得地址和动态端口信息了(除非知道秘钥,并且重写模块)。所以,连接设置如下:
如果选用主动模式通信,可点击上面界面中的“高级”,然后进入“连接”,取消“被动模式”的选中即可。主动模式,似乎只需要再开放20端口即可,不用conntrack,当然这和防火墙策略有关,也许不想实时开着20端口呢。
然后,连接就能读取成功,去除模块,或者减少iptables的放行规则,将无法通信。
今天太晚了,先不写了。
这个文件主要是定义了FTP几种模式:
/* This enum is exposed to userspace */
enum nf_ct_ftp_type {
/* PORT command from client */
NF_CT_FTP_PORT,
/* PASV response from server */
NF_CT_FTP_PASV,
/* EPRT command from client */
NF_CT_FTP_EPRT,
/* EPSV response from server */
NF_CT_FTP_EPSV,
};
上面两个是ipv4下面的主动和被动模式,下面两个则是ipv6下面的主动和被动模式
#define FTP_PORT 21
#define NF_CT_FTP_SEQ_PICKUP (1 << 0)
#define NUM_SEQ_TO_REMEMBER 2
/* This structure exists only once per master */
struct nf_ct_ftp_master {
/* Valid seq positions for cmd matching after newline */
u_int32_t seq_aft_nl[IP_CT_DIR_MAX][NUM_SEQ_TO_REMEMBER];
/* 0 means seq_match_aft_nl not set */
u_int16_t seq_aft_nl_num[IP_CT_DIR_MAX];
/* pickup sequence tracking, useful for conntrackd */
u_int16_t flags[IP_CT_DIR_MAX];
};
没太懂这里的,但是能看出来时用来存储序列号的,就是conntrack时,要校验并更新数据流的序列号。如果序列号对不上,并不会放行这个包。
#define MAX_PORTS 8
static u_int16_t ports[MAX_PORTS];
static unsigned int ports_c;
module_param_array(ports, ushort, &ports_c, 0400);
static bool loose;
module_param(loose, bool, 0600);
首先限制了最大监听的端口数量,不超过8个ftp服务器的端口。
module_param 这个貌似是可以在加载模块是传递参数,0400这个表明权限,但是不太懂这个loose变量到底是什么啥意思,修复的那个bug代表啥含义。
static int try_rfc959(const char *, size_t, struct nf_conntrack_man *,
char, unsigned int *);
static int try_rfc1123(const char *, size_t, struct nf_conntrack_man *,
char, unsigned int *);
static int try_eprt(const char *, size_t, struct nf_conntrack_man *,
char, unsigned int *);
static int try_epsv_response(const char *, size_t, struct nf_conntrack_man *,
char, unsigned int *);
static struct ftp_search {
const char *pattern;
size_t plen;
char skip;
char term;
enum nf_ct_ftp_type ftptype;
int (*getnum)(const char *, size_t, struct nf_conntrack_man *, char, unsigned int *);
} search[IP_CT_DIR_MAX][2];
这四个函数,就是尝试解析 ftp 数据包的,分别以那四种情况,去尝试解析出 动态端口。
然后含有个search结构体数据,里面包含了四种情况对应的解析方法,getnum指针就指向这几个函数。
/* Return 1 for match, 0 for accept, -1 for partial. */
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,
unsigned int *))
这个函数也很好懂,传递参数数就是search结构体中的数据,如果满足某一个种情况,就由参数指针返回解析到的动态端口(可能还有ip地址)。
/* Look up to see if we're just after a \n. */
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;
}
这个应该是验证数据包的序列号和保存下来的是否一致。
/* We don't update if it's older than what we have. */
static void update_nl_seq(struct nf_conn *ct, u32 nl_seq,
struct nf_ct_ftp_master *info, int dir,
struct sk_buff *skb)
{
unsigned int i, oldest;
/* Look for oldest: if we find exact match, we're done. */
for (i = 0; i < info->seq_aft_nl_num[dir]; i++) {
if (info->seq_aft_nl[dir][i] == nl_seq)
return;
}
if (info->seq_aft_nl_num[dir] < NUM_SEQ_TO_REMEMBER) {
info->seq_aft_nl[dir][info->seq_aft_nl_num[dir]++] = nl_seq;
} else {
if (before(info->seq_aft_nl[dir][0], info->seq_aft_nl[dir][1]))
oldest = 0;
else
oldest = 1;
if (after(nl_seq, info->seq_aft_nl[dir][oldest]))
info->seq_aft_nl[dir][oldest] = nl_seq;
}
}
这个则是更新存储的序列号。
然后,接下来,就是最关键的help函数了
static int help(struct sk_buff *skb,
unsigned int protoff,
struct nf_conn *ct,
enum ip_conntrack_info ctinfo)
help函数中的部分内容如下
/* Until there's been traffic both ways, don't look in packets. */
if (ctinfo != IP_CT_ESTABLISHED &&
ctinfo != IP_CT_ESTABLISHED_REPLY) {
pr_debug("ftp: Conntrackinfo = %u\n", ctinfo);
return NF_ACCEPT;
}
对于SYN包,这里就直接返回了。所以,iptables里面,需要一条21端口的NEW包规则。
所以,这里是要求数据包一定是在有双方数据流的情况才执行后面的代码。
从这个角度看,这个返回值NF_ACCEPT似乎并不会放行数据包。
th = skb_header_pointer(skb, protoff, sizeof(_tcph), &_tcph); // tcp 头部
if (th == NULL)
return NF_ACCEPT;
dataoff = protoff + th->doff * 4;
/* No data? */
if (dataoff >= skb->len) {
pr_debug("ftp: dataoff(%u) >= skblen(%u)\n", dataoff,
skb->len);
return NF_ACCEPT;
}
datalen = skb->len - dataoff;
这个dataoff是偏移量,指向了tcp的payload开始的地方,这个datalen则是这个payload的长度。
spin_lock_bh(&nf_ftp_lock); // 似乎是上锁
fb_ptr = skb_header_pointer(skb, dataoff, datalen, ftp_buffer);
BUG_ON(fb_ptr == NULL);
ends_in_nl = (fb_ptr[datalen - 1] == '\n');
seq = ntohl(th->seq) + datalen;
fb_ptr 则是拿到了 payload的起始地址。
ends_in_nl 是ftp协议会以换行符结尾。
seq则是计算下一条数据包的序列号。
/* Look up to see if we're just after a \n. */
if (!find_nl_seq(ntohl(th->seq), ct_ftp_info, dir)) {
/* We're picking up this, clear flags and let it continue */
if (unlikely(ct_ftp_info->flags[dir] & NF_CT_FTP_SEQ_PICKUP)) {
ct_ftp_info->flags[dir] ^= NF_CT_FTP_SEQ_PICKUP;
goto skip_nl_seq;
}
/* Now if this ends in \n, update ftp info. */
pr_debug("nf_conntrack_ftp: wrong seq pos %s(%u) or %s(%u)\n",
ct_ftp_info->seq_aft_nl_num[dir] > 0 ? "" : "(UNSET)",
ct_ftp_info->seq_aft_nl[dir][0],
ct_ftp_info->seq_aft_nl_num[dir] > 1 ? "" : "(UNSET)",
ct_ftp_info->seq_aft_nl[dir][1]);
ret = NF_ACCEPT;
goto out_update_nl;
}
这一步似乎在验证序列号。
cmd.l3num = nf_ct_l3num(ct);
memcpy(cmd.u3.all, &ct->tuplehash[dir].tuple.src.u3.all,
sizeof(cmd.u3.all));
这个l3num其实是指ipv4还是ipv6,如果是ipv4,其值是2,如果是ipv6,其值是10,详见 include/linux/socket.h,里面 160 行的 supportd address families 部分。
u3.all则是指ip地址,第二步是在拷贝地址,因为ipv4和ipv6并不一样,用联合体将两种类型封装到一块去了。
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) {
/* We don't usually drop packets. After all, this is
connection tracking, not packet filtering.
However, it is necessary for accurate tracking in
this case. */
nf_ct_helper_log(skb, ct, "partial matching of `%s'",
search[dir][i].pattern);
ret = NF_DROP;
goto out;
} else if (found == 0) { /* No match */
ret = NF_ACCEPT;
goto out_update_nl;
}
将find_pattern的四种情况都查询一遍,如果匹配到四种模式数据包的任何一种,就返回1,都没有匹配就返回0,如果只部分匹配(例如,匹配到字母,但没有找到端口),返回-1,似乎是将部分匹配的包当前异常包丢弃了。
exp = nf_ct_expect_alloc(ct);
if (exp == NULL) {
nf_ct_helper_log(skb, ct, "cannot alloc expectation");
ret = NF_DROP;
goto out;
}
如果匹配到了,就为该数据流,分配一个exp(不知道为什么取名为expect,期望,期待?)
daddr = &ct->tuplehash[!dir].tuple.dst.u3; // 指向该数据包的目的地址(可能是ipv4,也可能是ipv6)
/* Update the ftp info */
if ((cmd.l3num == nf_ct_l3num(ct)) &&
memcmp(&cmd.u3.all, &ct->tuplehash[dir].tuple.src.u3.all,
sizeof(cmd.u3.all))) {
/* Enrico Scholz's passive FTP to partially RNAT'd ftp
server: it really wants us to connect to a
different IP address. Simply don't record it for
NAT. */
if (cmd.l3num == PF_INET) {
pr_debug("NOT RECORDING: %pI4 != %pI4\n",
&cmd.u3.ip,
&ct->tuplehash[dir].tuple.src.u3.ip);
} else {
pr_debug("NOT RECORDING: %pI6 != %pI6\n",
cmd.u3.ip6,
ct->tuplehash[dir].tuple.src.u3.ip6);
}
/* Thanks to Cristiano Lincoln Mattos
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;
}
如果是NAT模式,那数据包里服务器给的连接的动态端口和IP地址和这个数据包的IP地址可能不一样,因此单独做了判断。
nf_ct_expect_init(exp, NF_CT_EXPECT_CLASS_DEFAULT, 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. */
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,
protoff, matchoff, matchlen, exp);
else {
/* Can't expect this? Best to drop packet now. */
if (nf_ct_expect_related(exp) != 0) {
nf_ct_helper_log(skb, ct, "cannot add expectation");
ret = NF_DROP;
} else
ret = NF_ACCEPT;
}
这个nf_ct_expect_init函数,则是给netfilter某种指示,在这两个IP地址上,要放行目的端口是cmd.u.tcp.port的包,这个端口就是解析到的动态端口了。
static int nf_ct_ftp_from_nlattr(struct nlattr *attr, struct nf_conn *ct)
{
struct nf_ct_ftp_master *ftp = nfct_help_data(ct);
/* This conntrack has been injected from user-space, always pick up
* sequence tracking. Otherwise, the first FTP command after the
* failover breaks.
*/
ftp->flags[IP_CT_DIR_ORIGINAL] |= NF_CT_FTP_SEQ_PICKUP;
ftp->flags[IP_CT_DIR_REPLY] |= NF_CT_FTP_SEQ_PICKUP;
return 0;
}
这个不太懂是干什么的
static struct nf_conntrack_helper ftp[MAX_PORTS * 2] __read_mostly;
似乎是限定了最多的帮助函数,每个端口会分配一个helper,最多有8个端口,每个端口有ipv4和ipv6两种情况,因此是16个。
static const struct nf_conntrack_expect_policy ftp_exp_policy = {
.max_expected = 1,
.timeout = 5 * 60,
};
这个似乎是expect的策略,涉及到到底什么时候关掉开启的动态端口等策略。
static int __init nf_conntrack_ftp_init(void)
{
int i, ret = 0;
NF_CT_HELPER_BUILD_BUG_ON(sizeof(struct nf_ct_ftp_master));
ftp_buffer = kmalloc(65536, GFP_KERNEL);
if (!ftp_buffer)
return -ENOMEM;
if (ports_c == 0)
ports[ports_c++] = FTP_PORT;
/* FIXME should be configurable whether IPv4 and IPv6 FTP connections
are tracked or not - YK */
for (i = 0; i < ports_c; i++) {
nf_ct_helper_init(&ftp[2 * i], AF_INET, IPPROTO_TCP, "ftp",
FTP_PORT, ports[i], ports[i], &ftp_exp_policy,
0, help, nf_ct_ftp_from_nlattr, THIS_MODULE);
nf_ct_helper_init(&ftp[2 * i + 1], AF_INET6, IPPROTO_TCP, "ftp",
FTP_PORT, ports[i], ports[i], &ftp_exp_policy,
0, help, nf_ct_ftp_from_nlattr, THIS_MODULE);
}
ret = nf_conntrack_helpers_register(ftp, ports_c * 2);
if (ret < 0) {
pr_err("failed to register helpers\n");
kfree(ftp_buffer);
return ret;
}
return 0;
}
这个则是分别在各个端口还有两种ip协议下,注册help函数。
从名字可以看出,这个文件应该是ftp的NAT有关系。在不考虑NAT的情况下,并不需要加载这个模块以实现动态端口开放。
/* 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("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;
exp->saved_proto.tcp.port = exp->tuple.dst.u.tcp.port;
exp->dir = !dir;
/* When you see the packet, we need to NAT it the same as the
* this one. */
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);
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))
goto out;
return NF_ACCEPT;
out:
nf_ct_helper_log(skb, ct, "cannot mangle packet");
nf_ct_unexpect_related(exp);
return NF_DROP;
}
这个文件虽然简单(相比什么sip和h323而言),但目前并没有看懂它怎么做的,为什么这么做。
经过测试,上述 buffer 中,存的就是之前匹配的东西,例如字串 (192,16,10,2,125,32)。
但是 将该Linux主机搭建为内外网并使用 NAT 代理 IP 后,不加载 nf_nat_ftp 模块其实也可以用 ftp 通信。难道这个是在那种 端口转发 情况下用的?
本文对 FTP 协议的动态端口开放的实现源码进行了初探,感觉Linux内核果然是博大精深啊,本人水平还差的远,非常惭愧。
参照 这个 FTP 模块,做了一个 OPC DA 协议的动态端口开放。代码在这里。