由於 防火牆的默認策略 應該是DROP,對於像 FTP 這種在被動模式下,需要新開端口建立數據通道的協議而言,防火牆不能夠主動的開闢相關的端口,也就在默認DROP情況下,FTP的數據端口不能通信,就會出現能登錄成功,但獲取不到目錄的情況。關於 FTP 的介紹,可參考這篇帖子。
在 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
加載 nf_conntrack_ftp 模塊
首先,瞭解如何開啓linux系統自帶的 ftp 協議的動態端口模塊。
- 準備ftp客戶端和服務器,linux下可使用vsftpd作爲ftp服務器,相關安裝方法,可參考這篇帖子。windows下的ftp客戶端,可使用WinScp這個軟件。
- 設置系統對conntrack模塊的支持
vim /etc/sysctl.conf
添加一行:
net.netfilter.nf_conntrack_helper=1
然後使用sysctl -p
使其生效。
主要是新版系統需要加這個一句,舊版系統不需要,舊版似乎是默認開啓的,此部分可參考這個文章。 - 加載 nf_conntrack_ftp 模塊
modprobe nf_conntrack modprobe nf_conntrack_ftp
- iptables 測試規則(此處僅是ipv4的測試規則,ipv6需要用到ip6tables)
此處在 FORWARD 鏈上測試,也可自行改成INPUT和OUTPUT鏈,Ubuntu 18.04 使用 bridge_utils 配置網橋後,需要額外加載模塊,FOWWARD 鏈規則才生效,相關操作可見這篇帖子。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
5. 開啓 ftp 服務器後,使用 winscp 客戶端對其進行訪問測試。
默認情況下,winscp 是用 被動模式通信的,被動模式的確需要動態端口開放這這一策略。
不過首先要明確一點,防火牆上做動態端口開放,顯然要求協議是明文的,加密是不行的,加密就沒法從數據包裏獲得地址和動態端口信息了(除非知道祕鑰,並且重寫模塊)。所以,連接設置如下:
如果選用主動模式通信,可點擊上面界面中的“高級”,然後進入“連接”,取消“被動模式”的選中即可。主動模式,似乎只需要再開放20端口即可,不用conntrack,當然這和防火牆策略有關,也許不想實時開着20端口呢。
然後,連接就能讀取成功,去除模塊,或者減少iptables的放行規則,將無法通信。
nf_conntrack_ftp 模塊粗讀
今天太晚了,先不寫了。
include/uapi/linux/netfilter/nf_conntrack_ftp.h
這個文件主要是定義了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下面的主動和被動模式
include/linux/netfilter/nf_conntrack_ftp.h
#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時,要校驗並更新數據流的序列號。如果序列號對不上,並不會放行這個包。
net/netfilter/nf_conntrack_ftp.c
#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
<[email protected]> 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函數。
net/nefilter/nf_nat_ftp.c
從名字可以看出,這個文件應該是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而言),但目前並沒有看懂它怎麼做的,爲什麼這麼做。
總結
本文對 FTP 協議的動態端口開放的實現源碼進行了初探,感覺Linux內核果然是博大精深啊,本人水平還差的遠,非常慚愧。
OPC DA 協議 動態端口開放
參照 這個 FTP 模塊,做了一個 OPC DA 協議的動態端口開放。代碼在這裏。