From 160e1df0cb27cac5882bb8728362e48a9050efba Mon Sep 17 00:00:00 2001 From: chn <897331845@qq.com> Date: Mon, 28 Oct 2019 23:29:28 +0800 Subject: [PATCH] --- README.md | 54 ++++++++++++++++++++++++++++++++---------------------- 1 file changed, 32 insertions(+), 22 deletions(-) diff --git a/README.md b/README.md index e80c0b7..c2acc60 100644 --- a/README.md +++ b/README.md @@ -1,28 +1,35 @@ ### 技术细节 +1 抓取 +2 请求 +4 作为流的第一个出现 +8 保留 Windows NT + #### 抓取数据包 -假定模块加载时,取定参数:`CAPTURE_MARK=0x10 RESERVE_WIN_MARK=0x20`。这也是默认的参数。 +模块根据数据包的 mark 来决定如何处理一个数据包,而不是按照端口来自行判断。这样可以有充分的灵活性。 -模块根据握手时的第一个包(SYN,以后简称首包)来确定是否追踪这个 TCP 流以及修改的策略(保留包含 `"Windows NT"` 的 UA,还是替换掉所有的 UA)。当首包 mark 的 `0x10` 位被置为 1 时,追踪这条流。当首包 mark 的 `0x20` 位被置为 1 时,将保留包含 `"Windows NT"` 的 UA,否则将替换所有的 UA。 +模块根据 0x100 位来确定是否抓取一个数据包。请将需要追踪的 TCP 流的所有数据包的这一位置 1。 -在确定追踪这条流的前提下,模块根据数据包的 mark 来确定是否抓取这个数据包。仅当 `0x10` 位被置为 1 时,模块会抓取这个数据包,否则不会抓取之。除了首包以外,`0x20` 位不起作用。如果一个数据包 mark 的 `0x10` 位被置为 1,但是所属的流没有被跟踪,或者这个包是服务端发来的(并且不是三次握手时的 SYN ACK 包),则会发出警告并丢弃(返回 `NF_DROP`)这个数据包。应该避免这样的情况出现。 +模块根据 0x200 位来确定这个包是从客户端流向服务端还是从服务端流向客户端。请将需要追踪的 TCP 流中,客户到服务端的包的这一位置为 1。 -总之,模块通过 `0x10` 确定一个数据包是否被捕获,通过首包(SYN)的 `0x20` 确定修改的策略,其它数据包的 `0x20` 被忽略。如果要追踪一条流,应该保证所有客户端到服务端的数据包的 `0x10` 位被置为 1、首包的 `0x20` 被置为合适的值、其它 IP 协议数据包的 `0x10` 被置为 0。模块中不会再作除上述内容以外的过滤,甚至不会判断目标端口是否为 `80`。在写防火墙规则时需要特殊考虑本地地址作为服务端的情况,以及(通常情况下)仅仅标记目标 `80` 端口的数据。 +模块根据 0x400 位来确定应该将这个包视为创建一条新的流(或覆盖已有的流)还是作为已经有的一条流的数据包处理。请将需要追踪的 TCP 流中,握手时由客户端发往服务端的 SYN 包的这一位置 1。 + +模块根据创建这条流时的数据包的 0x800 位来确定是否保留包含 Windows NT 的 UA。如果需要,请在将 400 置为 1 的同时将这一位置为 1。 + +实际上,上面 4 个位的位置都是可以自行指定的,只需要修改 /etc/modules.d/99-rkp-ua 中的参数即可修改,参数的意义顾名思义即可。 #### 流追踪的过程 -我们首先假定不会发生乱序(TCP disorder)和丢包的情况,并假定追踪的流是合法的 HTTP 1.x 请求。捕获到首包之后,会开始追踪这条流并将状态置为 `rkpstm_established_sniffing`,然后返回 `NF_ACCEPT`。 +我们首先假定不会发生乱序(TCP disorder)和丢包的情况,并假定追踪的流是合法的 HTTP 1.x 请求。捕获到首包之后,会开始追踪这条流并将状态置为 `rkpStream_sniffing`,然后返回 `NF_ACCEPT`。 -主循环:当流的状态被置为 `rkpstm_established_sniffing` 时,每收到一个数据包(应用层长度不为零),都会截留(返回 `NF_STOLEN`,包括包含 HTTP 头的最后一个数据包)直到捕获到整个 HTTP 头部(在应用层中读到 `\r\n\r\n`)。当确认捕获到整个 HTTP 头部后,会根据情况检查并修改 UA,然后将截获的数据包发出,将状态置为 `rkpstm_established_waiting`(除非最后一个包就带有 PUSH)后返回 `NF_STOLEN`。之后不含 PUSH 的数据包都将直接返回 `NF_ACCEPT`。当捕获到包含 PUSH 的数据包时,会将流的状态置为 `rkpstm_established_sniffing`,返回 `NF_ACCEPT`。对于不含应用层数据的包(单纯的 ACK 包),会直接放行并不修改状态。 +主循环:当流的状态被置为 `rkpStream_sniffing` 时,每收到从客户端发往服务端的一个数据包(应用层长度不为零),都会截留(返回 `NF_STOLEN`,包括包含 HTTP 头的最后一个数据包)直到捕获到整个 HTTP 头部(在应用层中读到 `\r\n\r\n`)。当确认捕获到整个 HTTP 头部后,会根据情况检查并修改 UA,然后将截获的数据包发出,将状态置为 `rkpStream_waiting`(除非最后一个包就带有 PUSH)后返回 `NF_STOLEN`。之后不含 PUSH 的数据包都将直接返回 `NF_ACCEPT`。当捕获到包含 PUSH 的数据包时,会将流的状态置为 `rkpstm_established_sniffing`,返回 `NF_ACCEPT`。对于不含应用层数据的包(假定为单纯的 ACK 包),会直接放行并不修改状态。 -为了处理乱序和丢包的情况,模块会记录建立连接时的序列号,并在每次返回 `NF_ACCEPT` 时更新这个序列号。对于每个收到的数据包,如果序列号不符合期待,则进行判断:若在期待的序列号之前 `0x80000000` 的范围内,视作重传,直接返回 `NF_ACCEPT`;否则,说明发生了乱序,将这个包放到缓存区延迟处理,返回 `NF_STOLEN`。每处理完成一个数据包后,确认缓存区是否有序列号符合期待的数据包并处理。因为实际情况下路由器上转发到外网的包经过的网络环境很简单,乱序出现的概率非常小(测试至少小于千分之一),所以不会造成性能的问题。 +为了处理乱序和丢包的情况,模块会记录建立连接时的序列号,并在每次缓存或发出一个包含应用层数据的数据包时更新这个序列号;同时,也会监测服务端返回的应答数据,记录服务端现在接收到了哪个包。对于每个捕获到的数据包,如果序列号不符合期待,则进行判断:若在期待的序列号与服务器已确认的序列号之间,视作重传,直接返回 `NF_ACCEPT`;否则,说明可能发生了乱序,将这个包放到缓存区延迟处理,返回 `NF_STOLEN`。每处理完成一个数据包后,确认缓存区是否有序列号符合期待的数据包并处理。因为实际情况下路由器上转发到外网的包经过的网络环境很简单,乱序出现的概率非常小(测试至少小于千分之一),所以不会造成性能的问题。 捕获过程中,如果发现 HTTP 头的长度超过 64 个数据包,或者在收集到完整的头部之前就收到 PSH,则认为不是有效的 HTTP 1.x 请求,会发出警告,将截获的数据包发出,返回 `NF_ACCEPT`。 -为了应对没有发出 FIN 和 ACK 就挂掉的连接(尤其是现在 keep-alive 非常流行),模块干脆不会特殊处理 FIN 和 RST,而是:在建立每条连接、以及每条连接的包经过的时候,同时记录时间(精确到秒就足够了)。每隔 1200 秒后,收到一个数据包的时候会触发清理的动作,超过 1200 秒没有活动的流就会被清理。因为使用源端口号来查找对应的流,因此不会造成查找太慢,只是确实稍稍浪费内存(应该可以忽略不计)。 - -另外,当一个新的连接的两个地址和两个端口与一个旧的连接都相同的时候,模块会将旧的连接覆盖掉。 +为了应对没有发出 FIN 和 ACK 就挂掉的连接(尤其是现在 keep-alive 非常流行),模块干脆不会特殊处理 FIN 和 RST,而是:在建立每条连接、以及每条连接的包经过的时候,同时记录时间(精确到秒就足够了)。每隔 1200 秒后,收到一个数据包的时候会触发清理的动作,超过 1200 秒没有活动的流就会被清理。因为使用源端口号与目的端口号来查找对应的流,因此不会造成查找太慢,只是确实稍稍浪费内存(应该可以忽略不计)。 #### 假装面向对象 @@ -39,6 +46,7 @@ * `struct sk_buff* buff`:截取的数据包。保证已经 `skb_ensure_writable`。 * `struct sk_buff* buff_prev`:由于乱序而需要暂缓处理的数据包。保证已经 `skb_ensure_writable`。 * `u_int32_t seq`:存储下一个期待收到的包的序列号。 +* `u_int32_t seq_ack`:存储服务端已经确认收到的最新的序列号。 * `time_t last_active`:记录最后一次活动的时间(单位为秒)。超过 1200 秒没有活动的连接会被放弃。 * `u_int8_t scan_matched`:在 `rpstm_established_sniffing` 状态下,指示直到 `buff` 中最后一个包的应用层末尾,匹配 `"\r\n\r\n"` 的字节数。一旦匹配到,则会短暂地被用来记录匹配 `"User-Agent: "` 等其它字符串的进度。当它没有意义时,总是被置为 0。 * `u_int8_t windows_preserve`:当 UA 中包含 `"Windows NT"` 时,是否保留不变。实际上,如果不需要保留,根本就不需要去确认是否包含 `"Windows NT"`。 @@ -54,13 +62,14 @@ 返回值的低八位用于通知上层函数如何处理,不同位表示不同的意义: - * `0x01` 位:表示这个包是否属于这个流。置为 1 时表示属于。 - * `0x02` 位:表示这个包是否可能需要修改。置为 1 时表示可能需要。 - * `0x04` 位:表示这个包是否需要被截留。置为 1 时表示需要。 - * `0x08` 位:发生了某些错误(比如,没有找全 HTTP 头却要 PSH,或者读取已经截取的应用层数据的前几个字节知道不是支持的 HTTP 方法,等)。上层函数应该向用户发出警告,将所有流中的数据放出,并停止模块的运行。 + * `0x1` 位:表示这个包是否属于这个流。置为 1 时表示属于。 + * `0x2` 位:表示这个包是否可能需要修改。置为 1 时表示可能需要。 + * `0x4` 位:表示这个包是否需要被截留。置为 1 时表示需要。 + * `0x8` 位:发生了某些严重的错误(我也想不到可能是什么错误,留作备用吧)。上层函数应该向用户发出警告,将所有流中的数据放出,并停止模块的运行。 事实上,可能的返回结果和处理方法: + * `rtn & 0x8 == 0x8`:向用户发出警告,将所有流中的数据放出,并停止模块的运行,返回 `NF_ACCEPT`。 * `rtn & 0x01 == 0x00`:不属于这个流。当然是去尝试下一个或者宣布这个包被错误地抓到啦。 * `else`:稍后需要调用 `rkpStream_execute`,但在这之前和之后需要还需要根据不同情况做一点事情。 * `rtn & 0x02 == 0x02`:确保这个包可以修改(调用 `skb_ensure_aritable`)。否则不用调用。 @@ -69,18 +78,19 @@ 高 24 位用于通知 `rkpStream_execute` 如何处理。其中,低四位共同表示包的处理方法,其它位在个别情况下才有意义。具体: * `0x0`:包不属于这个流。除了这种情况以外,都需要更新时间,以后不再重复。 - * `0x1`:包属于这个流,但直接放行就可以(不携带应用层数据,或者是重传)。 - * `0x2`:包是属于未来的、包含有应用层数据的数据包,需要置入 `buff_prev`。除了这三个以外,都需要更新序列号,不再重复。 - * `0x3`:包是 HTTP 头部的一部分,并且即使算上这个数据包,也没有收集到完整的头部。仅仅在这时,之后的四位才有用,它用来标记 `"\r\n\r\n"` 已经匹配了几个字符。 - * `0x4`:包是 HTTP 头部的一部分,算上这个数据包,已经收集到了完整的头部,并且头部是在这个数据包中结束的,并且没有 PSH 标志。这时,高 16 位有意义,它用来标记 `"\r\n\r\nx"` 中,`'x'` 的位置相对于这个包中应用层第一个字节的偏移。 - * `0x5`:与 `0x4` 类似,区别在于有 PSH,需要设置状态为 `rkpstm_established_sniffing`。 - * `0x6`:包不是 HTTP 头部的一部分,并且没有设置 PSH;总之,放行即可。它与 `0x1` 的区别在于,是否需要更新序列号。 - * `0x7`:包不是 HTTP 头部的一部分,并且设置了 PSH。放行并设置状态为 `rkpstm_established_sniffing`。 + * `0x1`:包是从服务端发到客户端的。更新一下时间和序列号就可以放行了。除此了这两个以外,包都是客户端发给服务端的,不再重复。 + * `0x2`:包属于这个流,但直接放行就可以(不携带应用层数据,或者是重传)。 + * `0x3`:包是属于未来的、包含有应用层数据的数据包,需要置入 `buff_prev`。除了这三个以外,都需要更新序列号,不再重复。 + * `0x4`:包是 HTTP 头部的一部分,并且即使算上这个数据包,也没有收集到完整的头部。仅仅在这时,之后的四位才有用,它用来标记 `"\r\n\r\n"` 已经匹配了几个字符。 + * `0x5`:包是 HTTP 头部的一部分,算上这个数据包,已经收集到了完整的头部,并且头部是在这个数据包中结束的,并且没有 PSH 标志。这时,高 16 位有意义,它用来标记 `"\r\n\r\nx"` 中,`'x'` 的位置相对于这个包中应用层第一个字节的偏移。 + * `0x6`:与 `0x4` 类似,区别在于有 PSH,需要设置状态为 `rkpStream_sniffing`。 + * `0x7`:包不是 HTTP 头部的一部分,并且没有设置 PSH;总之,放行即可。它与 `0x1` 的区别在于,是否需要更新序列号。 + * `0x8`:包不是 HTTP 头部的一部分,并且设置了 PSH。放行并设置状态为 `rkpStream_Ssniffing`。 * `void rkpStream_execute(struct rkpStream*, struct sk_buff*, u_int32_t)`:执行需要执行的动作。第三个参数传入 `rkpStream_judge` 对于这个包的返回值,它的低八位会被忽略。它要做的事情完全由 `rkpStream_judge` 的返回值来通知。以下数值是指 `rtn & 0x0f00 >> 8` 的值。 * `0x0`:返回。 - * `0x1`:更新时间,然后返回。 + * `0x1`:更新时间和 `seq_ack`,然后返回。 * `0x2`:更新时间,置入 `buff_prev`。 * `0x3`:更新时间、序列号,设置 `scan_matched`。调用 `__rkpStream_reexecute` 来尝试处理 `buff_prev` 中的包。 * `0x`