如下函数在接收到TCP报文之后,首先进行错误检查tcp_error。
int nf_conntrack_tcp_packet(struct nf_conn *ct,
struct sk_buff *skb,
unsigned int dataoff,
enum ip_conntrack_info ctinfo,
const struct nf_hook_state *state)
{
struct net *net = nf_ct_net(ct);
struct nf_tcp_net *tn = nf_tcp_pernet(net);
const struct tcphdr *th;
th = skb_header_pointer(skb, dataoff, sizeof(_tcph), &_tcph);
if (th == NULL)
return -NF_ACCEPT;
if (tcp_error(th, skb, dataoff, state))
return -NF_ACCEPT;
如果TCP头部长度字段的值,小于标准的头部长度(20字节),表明是构造错误的包。或者TCP头部和数据的总长度小于标准TCP头部长度,表明为被截断的TCP报文。
static bool tcp_error(const struct tcphdr *th,
struct sk_buff *skb,
unsigned int dataoff,
const struct nf_hook_state *state)
{
unsigned int tcplen = skb->len - dataoff;
u8 tcpflags;
/* Not whole TCP header or malformed packet */
if (th->doff*4 < sizeof(struct tcphdr) || tcplen < th->doff*4) {
tcp_error_log(skb, state, "truncated packet");
return true;
}
接下来报文校验和的检测受控于nf_conntrack_checksum的值,默认为1。可通过一下PROC文件进行修改。
# modprobe nf_conntrack
# cat /proc/sys/net/netfilter/nf_conntrack_checksum
1
这里仅对ingress进入系统的报文进行checksum检查,hook点不等于NF_INET_PRE_ROUTING的话,不进行检查。checksum正确时nf_checksum返回零。
/* Checksum invalid? Ignore.
* We skip checking packets on the outgoing path
* because the checksum is assumed to be correct.
*/
/* FIXME: Source route IP option packets --RR */
if (state->net->ct.sysctl_checksum &&
state->hook == NF_INET_PRE_ROUTING &&
nf_checksum(skb, state->hook, dataoff, IPPROTO_TCP, state->pf)) {
tcp_error_log(skb, state, "bad checksum");
return true;
}
在去掉ECN相关的两个标志ECE和CWR,以及PUSH标志之后,对剩余的标志位组合进行检查。
/* Check TCP flags. */
tcpflags = (tcp_flag_byte(th) & ~(TCPHDR_ECE|TCPHDR_CWR|TCPHDR_PSH));
if (!tcp_valid_flags[tcpflags]) {
tcp_error_log(skb, state, "invalid tcp flag combination");
return true;
}
return false;
合法的TCP标志位
tcp_valid_flags数组的大小为:0x37+1,以防某个报文同时设置了所有这些标志位。以下未列出的标志位组合都是非法的。
/* table of valid flag combinations - PUSH, ECE and CWR are always valid */
static const u8 tcp_valid_flags[(TCPHDR_FIN|TCPHDR_SYN|TCPHDR_RST|TCPHDR_ACK|
TCPHDR_URG) + 1] =
{
[TCPHDR_SYN] = 1,
[TCPHDR_SYN|TCPHDR_URG] = 1,
[TCPHDR_SYN|TCPHDR_ACK] = 1,
[TCPHDR_RST] = 1,
[TCPHDR_RST|TCPHDR_ACK] = 1,
[TCPHDR_FIN|TCPHDR_ACK] = 1,
[TCPHDR_FIN|TCPHDR_ACK|TCPHDR_URG] = 1,
[TCPHDR_ACK] = 1,
[TCPHDR_ACK|TCPHDR_URG] = 1,
};
#define tcp_flag_byte(th) (((u_int8_t *)th)[13])
#define TCPHDR_FIN 0x01
#define TCPHDR_SYN 0x02
#define TCPHDR_RST 0x04
#define TCPHDR_PSH 0x08
#define TCPHDR_ACK 0x10
#define TCPHDR_URG 0x20
#define TCPHDR_ECE 0x40
#define TCPHDR_CWR 0x80
#define TCPHDR_SYN_ECN (TCPHDR_SYN | TCPHDR_ECE | TCPHDR_CWR)
TCP校验和
对于IPv4协议,由函数nf_ip_checksum检查TCP校验和。如果ip_summed等于CHECKSUM_COMPLETE,表明skb结构中的csum已经计算过得部分校验和(不包括伪头部),如果其折叠为16比特之后,取反等于零,表明验证通过。反之,对于TCP或者UDP协议,需要将伪头部计算进csum,最终结果为零,验证通过。
__sum16 nf_ip_checksum(struct sk_buff *skb, unsigned int hook,
unsigned int dataoff, u8 protocol)
{
const struct iphdr *iph = ip_hdr(skb);
__sum16 csum = 0;
switch (skb->ip_summed) {
case CHECKSUM_COMPLETE:
if (hook != NF_INET_PRE_ROUTING && hook != NF_INET_LOCAL_IN)
break;
if ((protocol != IPPROTO_TCP && protocol != IPPROTO_UDP &&
!csum_fold(skb->csum)) ||
!csum_tcpudp_magic(iph->saddr, iph->daddr,
skb->len - dataoff, protocol,
skb->csum)) {
skb->ip_summed = CHECKSUM_UNNECESSARY;
break;
}
fallthrough;
对于ip_summed等于CHECKSUM_NONE的情况,校验和还未计算。对于TCP或者UDP,先行计算伪头部的校验和,再行计算协议数据的校验和。
case CHECKSUM_NONE:
if (protocol != IPPROTO_TCP && protocol != IPPROTO_UDP)
skb->csum = 0;
else
skb->csum = csum_tcpudp_nofold(iph->saddr, iph->daddr,
skb->len - dataoff,
protocol, 0);
csum = __skb_checksum_complete(skb);
}
return csum;
第一步先将csum由32bit变为17bit;第二步将其由17bit变为16bit;最后进行取反操作。
/*
* Fold a partial checksum without adding pseudo headers
*/
static inline __sum16 csum_fold(__wsum csum)
{
u32 sum = (__force u32)csum;
sum = (sum & 0xffff) + (sum >> 16);
sum = (sum & 0xffff) + (sum >> 16);
return (__force __sum16)~sum;
}
内核版本 5.10
|