在之前文章ftp&nat写到调整seq问题,现在专门来看下:
tcp负载长度发生变化
在ftp传输PORT命令或者PASV的应答时会进行alg处理,如果使能了nat则会修改PORT命令或者PASV的应答的内容,导致tcp负载发生变化。
/* Generic function for mangling variable-length address changes inside
* NATed TCP connections (like the PORT XXX,XXX,XXX,XXX,XXX,XXX
* command in FTP).
*
* Takes care about all the nasty sequence number changes, checksumming,
* skb enlargement, ...
*
* */
int __nf_nat_mangle_tcp_packet(struct sk_buff *skb,struct nf_conn *ct,enum ip_conntrack_info ctinfo,
unsigned int protoff,unsigned int match_offset,
unsigned int match_len,const char *rep_buffer,unsigned int rep_len, bool adjust)
{
-----------------------------
/* 长度发生改变,需要调整序列号,match_len为原始的PORT命令内容长度,rep_len为修改后的长度 */
/* 记住这里还没有变更tcp头的序列号,所以tcph->seq还是原始的值 */
if (adjust && rep_len != match_len)
nf_ct_seqadj_set(ct, ctinfo, tcph->seq,
(int)rep_len - (int)match_len);//变化长度,变长为正,变短为负
return 1;
}
其中:rep_len 表示替换后长度, match_len表示替换前的长度。
/**
* struct nf_ct_seqadj - sequence number adjustment information
*
* @correction_pos: position of the last TCP sequence number modification上次序列号修改的位置
* @offset_before: sequence number offset before last modification序列号偏移,在上一次修改之后
* @offset_after: sequence number offset after last modification序列号偏移,在最后一次修改之后
*/
struct nf_ct_seqadj {
u32 correction_pos;/* 最后一次修改tcp负载长度的报文原始的序列号 */
s32 offset_before;/* 最后一次的上一次修改报文的tcp负载累积长度 */
s32 offset_after;/* 最后一次修改tcp报文后的累积长度 */
};
/*
调整参数设置
偏移量off为零时不需要进行处理。首先,将此连接的状态设置IPS_SEQ_ADJUST_BIT标志,表明此连接需要进行序号调整。
对于此连接的第一次序号调整,三个相关参数的值都为零。记录下连接当前的序号correction_pos,调整之后的序号偏移量offset_after,
对于首次调整offset_before为零。
对于非首次的序号调整,如果现在的报文序号在上一次调整序号(correction_pos)位置之后,更新调整序号correction_pos的值,
并且offset_beforce记录的为上一次序号调整的偏移量,而offset_after记录的为最后一次调整的总的偏移量
*/
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];/* 获取本方向的序列号信息,刚开始的时候是三者都为0 */
if (this_way->offset_before == this_way->offset_after ||//只有初始化的时候会相等,说明是tcp负载长度第一次发生变化
before(this_way->correction_pos, ntohl(seq))) {/* 新的序列号大于上一次的调整,后续调整 */
this_way->correction_pos = ntohl(seq); //记录下连接当前的序号correction_pos
this_way->offset_before = this_way->offset_after;//记录的为上一次序号调整的偏移量
this_way->offset_after += off;//录的为最后一次调整的总的偏移量
}
spin_unlock_bh(&ct->lock);
return 0;
}
TCP序号调整
当前处理的报文的序号,在调整序号之后,新的序号seqoff使用offset_after,否则,使用offset_before的值,后者可能是重传/乱序报文。报文的新序号等于其自带序号与偏移量seqoff的和。
skb1-seq correction_pos
| |
------------------|-----------|--------------------|-----
| |
| skb2-seq
|
offset_before <--|-->offset_after
对于seq序号的调整,使用方向dir在结构seqadj->seq所取得的相关数据。
对于ACK序号。其方向与以上的序号处理相反,使用other_way结构。
previous
correction_pos
| current
| skb3-ack_seq correction_pos
| | |
----------|--------|--------------|--------------------|-----
| | | |
| | | skb4-ack_seq
| | | |
offset_before |--> offset_after <--|
/* TCP sequence number adjustment. Returns 1 on success, 0 on failure */
int nf_ct_seq_adjust(struct sk_buff *skb,
struct nf_conn *ct, enum ip_conntrack_info ctinfo,
unsigned int protoff)
{
enum ip_conntrack_dir dir = CTINFO2DIR(ctinfo);
struct tcphdr *tcph;
__be32 newseq, newack;
s32 seqoff, ackoff;
struct nf_conn_seqadj *seqadj = nfct_seqadj(ct);
struct nf_ct_seqadj *this_way, *other_way;
int res;
this_way = &seqadj->seq[dir];
other_way = &seqadj->seq[!dir];
if (!skb_make_writable(skb, protoff + sizeof(*tcph)))
return 0;
tcph = (void *)skb->data + protoff;
spin_lock_bh(&ct->lock);
//这个判断非常关键,一共有7中情况,见下面详细分析
//这里采用的是after,没有等于号,也就是说tcph->seq==this_way->correction_pos
//使用seqoff = this_way->offset_before,这符合实际。
/* 新报文序列号是在最新修改之后,所以使用after,否则使用before */
if (after(ntohl(tcph->seq), this_way->correction_pos))
seqoff = this_way->offset_after;//新的序号seqoff使用offset_after
else
seqoff = this_way->offset_before;
//对于ACK序号。其方向与以上的序号处理相反,使用other_way结构
/* 调整确认序列号,确认序列号应该与反方向的发送序列号的偏移进行匹配,
* 判断该报文是在上次修改之前的报文,还是之后的报文。
* other_way->correction_pos记录的是发送方向发生改变的报文的原始序列号。而tcph->ack_seq是接收方对修改序列号
* 后的报文的应答,这里减去other_way->offset_before后即为对原始发送序列号报文的应答序列号。
*/
if (after(ntohl(tcph->ack_seq) - other_way->offset_before,
other_way->correction_pos))
ackoff = other_way->offset_after;
else
ackoff = other_way->offset_before;
/* 新的序列号等于原始序列号加上偏移 */
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;
res = nf_ct_sack_adjust(skb, protoff, tcph, ct, ctinfo);
spin_unlock_bh(&ct->lock);
return res;
}
当前还没有发生过tcp负载长度变化,after(ntohl(tcph->seq), this_way->correction_pos)为真,seqoff = this_way->offset_after;也就是为0,不会实际修改tcph->seq。
当前已经发生过一次tcp负载长度变化,after(ntohl(tcph->seq), this_way->correction_pos)为真,seqoff = this_way->offset_after;也就是为10,修改tcph->seq=tcph->seq+10。
当前已经发生过一次tcp负载长度变化,但是当前的报文的序列号为6000,小于this_way->correction_pos。这种情况就是报文迷路了导致的,
after(ntohl(tcph->seq), this_way->correction_pos)为假,seqoff = this_way->offset_before;也就是为0,不会实际修改tcph->seq。
当前已经发生过两次tcp负载长度变化,after(ntohl(tcph->seq), this_way->correction_pos)为真,seqoff = this_way->offset_after;也就是为5,修改tcph->seq=tcph->seq+5。
前已经发生过两次tcp负载长度变化,但是当前的报文的序列号为16000,小于this_way->correction_pos。这种情况就是报文迷路了导致的,
after(ntohl(tcph->seq), this_way->correction_pos)为假,seqoff = this_way->offset_before;也就是为10,会实际修改tcph->seq=tcph->seq+10。
当前已经发生过两次tcp负载长度变化,但是当前的报文的序列号为6000,小于this_way->correction_pos。这种情况就是报文严重迷路了导致的,after(ntohl(tcph->seq), this_way->correction_pos)为假,
seqoff = this_way->offset_before;也就是为10,会实际修改tcph->seq=tcph->seq+10。
这种修改是错误的,因为序列号6000的报文不需要修改序列号。也就是说netfilter不能正确处理两次tcp负载长度变化之前的迷路报文。
应答序列号的调整
应答序列号表示接受方期望收到的下一个字节序列号。它等于接收方收到的报文的发送序列号加上报文的长度。
接收方还可以合并应答,也就是说应答报文不一定和接收的报文对应起来。 而且发送方在接收应答序列号时,只关注比当前已经应答过的序列号大的序列号即可。
正常情况下一对一应答报文
情况1:
当前发送方向还没有发生过tcp负载长度变化,接收方收到的tcph->seq=6000,假设报文长度为100,那么应答序列号为tcph->ack_seq=6100。after(ntohl(tcph->ack_seq) - other_way->offset_before,other_way->correction_pos) 为真,seqoff = this_way->offset_after为0,不会修改tcph->ack_seq。
情况2:
当前发送方向已经发生过一次tcp负载长度变化,接收方收到的tcph->seq=10000,假设报文长度为100。假设本次应答报文就是应答了序列号为10000的报文,由于长度边长了10,所以应答序列号为tcph->ack_seq=10110。tcph->ack_seq-other_way->offset_before=10110-0=10110大于other_way->correction_pos。所以after(ntohl(tcph->ack_seq) - other_way->offset_before,other_way->correction_pos) 为真,seqoff = this_way->offset_after为10,修改tcph->ack_seq=tcph->ack_seq-10=10100。这个值与发送方的正常的期望应答序列号一致,没有问题。
情况3:
当前发送方向已经发生过一次tcp负载长度变化,接收方收到的tcph->seq=16010(netfilter给增加了10个字节),假设报文长度为100。假设本次应答报文就是应答了序列号为16000的报文,应答序列号为tcph->ack_seq=16110。tcph->ack_seq-other_way->offset_before=16110-0=16110大于other_way->correction_pos。所以after(ntohl(tcph->ack_seq) - other_way->offset_before,other_way->correction_pos) 为真,seqoff = this_way->offset_after为10,修改tcph->ack_seq=tcph->ack_seq-10=16100。这个值与发送方的正常的期望应答序列号一致,没有问题。
情况4:
当前发送方向已经发生过两次tcp负载长度变化,假设本次应答序列号为20000的报文,假设报文长度为100。那么接收方收到的报文的序列号为20010(netfilter给增加一个10个字节),由于本次报文的长度在netfilter中减少了5个字节,所以实际接收方收到的报文长度为95个字节,应答序列号为tcph->ack_seq=20105。tcph->ack_seq-other_way->offset_before=20105 - 5=20100大于other_way->correction_pos。所以after(ntohl(tcph->ack_seq) - other_way->offset_before,other_way->correction_pos) 为真,seqoff = this_way->offset_after为5,修改tcph->ack_seq=tcph->ack_seq-5=26105-5=26100。这个值与发送方的正常的期望应答序列号一致,没有问题。
情况5:
当前发送方向已经发生过两次tcp负载长度变化,接收方收到的tcph->seq=26005(netfilter给增加了5个字节序列号),假设报文长度为100。假设本次应答报文就是应答了序列号为26000的报文,应答序列号为tcph->ack_seq=26105。tcph->ack_seq-other_way->offset_before=26105 - 10=26095大于other_way->correction_pos。所以after(ntohl(tcph->ack_seq) - other_way->offset_before,other_way->correction_pos) 为真,seqoff = this_way->offset_after为5,修改tcph->ack_seq=tcph->ack_seq-5=26105-5=26100。这个值与发送方的正常的期望应答序列号一致,没有问题。
从前面五种情况来说,netfilter不会导致应答序列号超过发送方发送的最后一个字节+1。这是符合我们的预期的。前面没有说明合并应答的情况,实际也是满足要求的。
正常情况下非一对一应答,应答相比于发送有延迟
情况1:
当前发送方向已经发生过一次tcp负载长度变化,发送方情况如上图所示,此时应答方最大应答到序列号为6000。假设本次应答接下来1000个字节。那么应答序列号tcph->ack_seq=7000。tcph->ack_seq-other_way->offset_before=7000 - 0=7000小于other_way->correction_pos。所以after(ntohl(tcph->ack_seq) - other_way->offset_before,other_way->correction_pos) 为假,seqoff = this_way->offset_before为0,修改tcph->ack_seq不会发生变化,没有问题。
情况2:
当前发送方向已经发生过一次tcp负载长度变化,发送方情况如上图所示,此时应答方最大应答到序列号为6000。假设发送方发送的序列号已经到了15000了。本次应答接下来5000个字节。那么应答序列号tcph->ack_seq=11000。该应答序列号已经包含了对发生长度变化的10000序列号报文的应答。因为该报文在netfilter中增加了10个字节,所以实际应答应该要比序列号小10个字节,即真实到发送端的应答序列号为11000-10=10090。tcph->ack_seq-other_way->offset_before=11000 - 0=11000大于other_way->correction_pos。所以after(ntohl(tcph->ack_seq) - other_way->offset_before,other_way->correction_pos) 为真,seqoff = this_way->offset_after为10,修改tcph->ack_seq=tcph->ack_seq-10=10090,与预期相符,没有问题。
情况3:
当前发送方向已经发生过两次tcp负载长度变化,发送方情况如上图所示,此时应答方最大应答到序列号为6000。假设发送方发送的序列号已经到了25000了。本次应答接下来2000个字节。那么应答序列号tcph->ack_seq=8000。该应答序列号没有包含了对发生长度变化的10000序列号报文的应答,所以实际应答应该要比序列号不变,即真实到发送端的应答序列号为8000。tcph->ack_seq-other_way->offset_before=8000- 5=7095小于other_way->correction_pos。所以after(ntohl(tcph->ack_seq) - other_way->offset_before,other_way->correction_pos) 为假,seqoff = this_way->offset_before为10,修改tcph->ack_seq=tcph->ack_seq-10=7095,与预期不相符,有点问题,发生了报文的部分应答问题。
情况4:
当前发送方向已经发生过两次tcp负载长度变化,发送方情况如上图所示,此时应答方最大应答到序列号为6000。假设发送方发送的序列号已经到了25000了。本次应答接下来5000个字节。那么应答序列号tcph->ack_seq=11000。该应答序列号已经包含了对发生长度变化的10000序列号报文的应答。因为该报文在netfilter中增加了10个字节,所以实际应答应该要比序列号小10个字节,即真实到发送到的应答序列号为11000-10=10090。tcph->ack_seq-other_way->offset_before=11000 - 0=11000小于other_way->correction_pos。所以after(ntohl(tcph->ack_seq) - other_way->offset_before,other_way->correction_pos) 为假,seqoff = this_way->offset_before为10,修改tcph->ack_seq=tcph->ack_seq-10=10090,与预期相符,没有问题。
迷路的应答报文
迷路应答报文与延迟应答类似,只不过发送方收到应答报文时,对应的字节已经被应答过了,属于重复应答。
sack选项的序列号调整
kind=4是选择性确认(Selective Acknowledgment,SACK)选项。
TCP通信时,如果某个TCP报文段丢失,则TCP模块会重传最后被确认的TCP报文段后续的所有报文段,这样原先已经正确传输的TCP报文段也可能重复发送,从而降低了TCP性能。SACK技术正是为改善这种情况而产生的,
它使TCP模块只重新发送丢失的TCP报文段,不用发送所有未被确认的TCP报文段。选择性确认选项用在连接初始化时,表示是否支持SACK技术。我们可以通过修改
/proc/sys/net/ipv4/tcp_sack内核变量来启用或关闭选择性确认选项。
kind=5是SACK实际工作的选项。该选项的参数告诉发送方本端已经收到并缓存的不连续的数据块,从而让发送端可以据此检查并重发丢失的数据块。每个块边沿(edge of block)参数包含一个4字节的序号。
其中块左边沿表示不连续块的第一个数据的序号,而块右边沿则表示不连续块的最后一个数据的序号的下一个序号。这样一对参数(块左边沿和块右边沿)之间的数据是没有收到的。
因为一个块信息占用8字节,所以TCP头部选项中实际上最多可以包含4个这样的不连续数据块(考虑选项类型和长度占用的2字节)。
sack序列号调整与应答序列号调整类似:
/* Adjust one found SACK option including checksum correction */
static void nf_ct_sack_block_adjust(struct sk_buff *skb,
struct tcphdr *tcph,
unsigned int sackoff,
unsigned int sackend,
struct nf_ct_seqadj *seq)
{
while (sackoff < sackend) {
struct tcp_sack_block_wire *sack;
__be32 new_start_seq, new_end_seq;
sack = (void *)skb->data + sackoff;//获取sack块
/* 起始序列号 */
if (after(ntohl(sack->start_seq) - seq->offset_before,
seq->correction_pos))
new_start_seq = htonl(ntohl(sack->start_seq) -
seq->offset_after);
else
new_start_seq = htonl(ntohl(sack->start_seq) -
seq->offset_before);
//结束序列号
if (after(ntohl(sack->end_seq) - seq->offset_before,
seq->correction_pos))
new_end_seq = htonl(ntohl(sack->end_seq) -
seq->offset_after);
else
new_end_seq = htonl(ntohl(sack->end_seq) -
seq->offset_before);
pr_debug("sack_adjust: start_seq: %u->%u, end_seq: %u->%u\n",
ntohl(sack->start_seq), ntohl(new_start_seq),
ntohl(sack->end_seq), ntohl(new_end_seq));
inet_proto_csum_replace4(&tcph->check, skb,
sack->start_seq, new_start_seq, false);
inet_proto_csum_replace4(&tcph->check, skb,
sack->end_seq, new_end_seq, false);
/* 修改 */
sack->start_seq = new_start_seq;
sack->end_seq = new_end_seq;
/* 调到下一个sack选项 */
sackoff += sizeof(*sack);
}
}
http代理服务器(3-4-7层代理)-网络事件库公共组件、内核kernel驱动 摄像头驱动 tcpip网络协议栈、netfilter、bridge 好像看过!!!! 但行好事 莫问前程 --身高体重180的胖子