(1)首先,了解kernel中iphdr/updhdr/tcphdr结构
struct iphdr:
// include/uapi/linux/ip.h
struct iphdr {
#if defined(__LITTLE_ENDAIN_BITFIELD)
__u8 ihl:4,
version:4;
#elif defined(__BIG_ENDAIN_BITFIELD)
__u8 version:4,
ihl:4;
#else
#error "Please fix <asm/byteorder.h>"
#endif
__u8 tos;
__be16 tot_len;
__be16 id;
__be16 frag_off;
__u8 ttl;
__u8 protocol;
__sum16 check;
__be32 saddr;
__be32 daddr;
};
struct udphdr:
// include/uapi/linux/udp.h
struct udphdr {
__be16 source;
__be16 dest;
__be16 len;
__sum16 check;
};
struct tcphdr:
// include/uapi/linux/tcp.h
struct tcphdr {
__be16 source;
__be16 dest;
__be32 seq;
__be32 ack_seq;
#if defined(__LITTLE_ENDIAN_BITFIELD)
__u16 resl:4,
doff:4,
fin:1,
syn:1,
rst:1,
psh:1,
ack:1,
urg:1,
ece:1,
cwr:1;
#elif defined(__BIG_ENDIAN_BITFIELD)
__u16 doff:4,
resl:4,
cwr:1,
ece:1,
urg:1,
ack:1,
psh:1,
rst:1,
syn:1,
fin:1;
#else
#error "Adjust your <asm/byteorder.h> defines"
#endif
__be16 window;
__sum16 check;
__be16 urg_ptr;
};
其次,看一下iphtr的结构图
|----|----|------|----------------|---------
|ver |ihl | tos | tot_len |
|----------------|----------------|
| id | frag_off |
|---------|------|----------------|
| ttl| |protoc| check | 20 Byte
|---------|------|----------------|
| saddr |
|---------------------------------|
| daddr |
|----------------|----------------|---------
| options | 40 Bytes
|---------------------------------| Max
iphdr->version
版本(4位),目前的协议版本号是4,因此IP有时也称作IPv4。
iphdr->ihl
首部长度(4位):首部长度指的是IP层头部占32 bit字的数目(也就是IP层头部包含多少个4字节 -- 32位),包括任何选项。由于它是一个4比特字段,因此首部最长为60个字节。普通IP数据报(没有任何选择项)字段的值是5 <==> 5 * 32 / 8 = 5 * 4 = 20 Bytes
iphdr->tos
服务类型字段(8位): 服务类型(TOS)字段包括一个3 bit的优先权子字段(现在已被忽略),4 bit的TOS子字段和1 bit未用位但必须置0。4 bit的TOS子字段分别代表:最小时延、最大吞吐量、最高可靠性和最
小费用。4 bit中只能设置其中1 bit。如果所有4 bit均为0,那么就意味着是一般服务。
iphdr->tot_len
总长度字段(16位)是指整个IP数据报的长度,以字节为单位。利用首部长度字段和总长度字段,就可以知道 IP数据报中数据内容的起始位置和长度。由于该字段长16比特,所以IP数据报最长可达65535字节
总长度字段是IP首部中必要的内容,因为一些数据链路(如以太网)需要填充一些数据以达到最小长度。尽管以太网的最小帧长为46字节,但是IP数据可能会更短。如果没有总长度字段,那么IP层就不知道46字节中有多少是IP数据报的内容。
iphdr->id
标识字段(16位)唯一地标识主机发送的每一份数据报。通常每发送一份报文它的值就会加1。
iphdr->frag_off (16位)
frag_off域的低13位 -- 分段偏移(Fragment offset)域指明了该分段在当前数据报中的什么位置上。除了一个数据报的最后一个分段以外,其他所有的分段(分片)必须是8字节的倍数。这是8字节是基本分段单位。由于该域有13个位,所以,每个数据报最多有8192个分段。因此,最大的数据报长度为65,536字节,比iphdr->tot_len域还要大1。
iphdr->frag_off的高3位
(1) 比特0是保留的,必须为0;
(2) 比特1是“更多分片”(MF -- More Fragment)标志。除了最后一片外,其他每个组成数据报的片都要把该比特置1。
(3) 比特2是“不分片”(DF -- Don't Fragment)标志,如果将这一比特置1,IP将不对数据报进行分片,这时如果有需要进行分片的数据报到来,会丢弃此数据报并发送一个ICMP差错报文给起始端。
|---|-------------|
|DM0| offset |
|---|-------------|
15 1312 0
iphdr->ttl
TTL(time-to-live) -- 8位,生存时间字段设置了数据报可以经过的最多路由器数。它指定了数据报的生存时间。TTL的初始值由源主机设置(通常为32或64),一旦经过一个处理它的路由器,它的值就减去1。当该字段的值为0时,数据报就被丢弃,并发送ICMP报文通知源主机。
TTL(Time to live)域是一个用于限制分组生存期的计数器。这里的计数时间单位为秒,因此最大的生存期为255秒。在每一跳上该计数器必须被递减,而且,当数据报在一台路由器上排队时间较长时,该计数器必须被多倍递减。在实践中,它只是跳计数器,当它递减到0的时候,分组被丢弃,路由器给源主机发送一个警告分组。此项特性可以避免数据报长时间地逗留在网络中,有时候当路由表被破坏之后,这种事情是有可能发生的。
iphdr->protocol
协议字段(8位): 根据它可以识别是哪个协议向IP传送数据。
当网络层组装完成一个完整的数据报之后,它需要知道该如何对它进行处理。协议(Protocol)域指明了该将它交给哪个传输进程。TCP是一种可能,但是UDP或者其他的协议也是可能的。
iphdr->check
首部检验和字段(16位)是根据IP首部计算的检验和码。它不对首部后面的数据进行计算。 ICMP、IGMP、UDP和TCP在它们各自的首部中均含有同时覆盖首部和数据检验和码。
为了计算一份数据报的IP检验和,首先把检验和字段置为0。然后,对首部中每个16 bit进行二进制反码求和(整个首部看成是由一串16 bit的字组成),结果存在检验和字段中。当收到一份IP数据报后,同样对首部中每个16 bit进行二进制反码的求和。由于接收方在计算过程中包含了发送方存在首部中的检验和,因此,如果首部在传输过程中没有发生任何差错,那么接收方计算的结果应该为全1。如果结果不是全1(即检验和错误),那么IP就丢弃收到的数据报。但是不生成差错报文,由上层去发现丢失的数据报并进行重传。
iphdr->saddr
32位源IP地址
iphdr->daddr
32位目的IP地址
再次,看一下tcphdr的结构图
|----------------|----------------|---------
| source | dest |
|----------------|----------------|
| seq |
|----------------|----------------|
| ack_seq | 20 Bytes
|----|----|------|----------------|
|doff|res1| | window |
|----|----|------|----------------|
| check | urg_ptr |
|----------------|----------------|---------
| options | 40 Bytes
|---------------------------------| max
tcphdr->source: 16位源端口
tcphdr->dest: 16位目的端口
tcphdr->seq: 表示此次发送的数据在整个报文段中的起始字节数。此序号用来标识从tcp发送端向tcp接收端发送的数据字节流,seq表示在这个报文段中的第一个数据字节。序号是32 bit的无符号数。为了安全起见,它的初始值是一个随机生成的数,它到达32位最大值后,又从零开始。
tcphdr->ack_seq: 指定的时下一个期望接收的字节,而不是已经正确接收到的最后一个字节。
tcphdr->doff: Tcp头长度,指明了在tcp头部包含多少个32位的字。此信息时必须的,因为options域的长度是可变的,所以整个Tcp头部的长度也是变化的。这个域实际上指明了数据部分在段内部的其起始地址(以32位字作为单位进行计量),因为这个数值正好是按字为单位的TCP头部的长度,所以,二者的效果是等同的
再次,由于要对tcp opt的结构进行strip,了解一下tcp opt个结构
Tcp options字段
由于doff占有4bit位,因此最大值为2^4-1 = 15,所有Head最大长度为15*4 = 60字节;而头部除了固定长度的20 bytes,剩下最大可以表示40 Bytes. Tcp options字段的一般数据结构如下:
Kind/Type(1字节) | Length(1字节) | Info(n字节) |
选项第一个字段kind说明选项的类型。有的Tcp选项没有后面两个字段,仅包含1字节的kind字段。第二字段Length(如果有的话)指定该选项的总长度,该长度包含kind和length字段占据的两个字节。第三个字段info(如果有的话)是选项的具体信息。
Kind/Type | Length | Name | Referenc | 描述&用途 |
---|---|---|---|---|
0(TCPOPT_EOL) | 1 | EOL | RFC793 | End of Option List 选项列表结束 |
1(TCPOPT_NOP) | 1 | NOP | RFC793 | No Operation 空操作(填充标志,为了字节对齐) |
2(TCPOPT_MSS) | 4(TCPOLEN_MSS) | MSS | RFC793 | Maximum Segment Size 最大segment长度 |
3(TCPOPT_WINDOW) | 3(TCPOLEN_WINDOW) | WSOPT | RFC793 | 窗口扩大系数(Windows Scaling Factor) |
4(TCPOPT_SACK_PERM) | 2(TCPOLEN_SACK_PERM) | SACK-Permitted | RFC2018 | 表明支持SACK |
5(TCPOPT_SACK) | 可变 | SACK | RFC2018 | SACK Block(收到乱序数据) |
8(TCPOPT_TIMESTAMP) | 10(TCPOLEN_TIMESTAMP) | TSPOT | RFC1323 | Timestamps |
19(TCPOPT_MD5SIG) | 18(TCPOLEN_MD5SIG) | TCP-MD5 | RFC2385 | MD5认证 |
28 | 4 | UTO | RFC5482 | User Timeout(超过一定闲置时间拆除连接) |
29 | 可变 | TCP-AO | RFC5925 | 认证(可选用各种算法) |
253/254(TCPOPT_EXP) | 可变 | Experimental | RFC4727 | 保留,用于科研实验 |
以下为mangle表中进行tcpopt相关的函数
static unsigned int
tcpoptstrip_tg4(struct sk_buff *skb, const struct xt_action_param *par)
{
return tcpopstrip_mangle_packet(skb, par, ip_hdrlen(skb),
sizeof(struct iphdr) + sizeof(struct tcphdr));
}
///
static inline unsigned int
ip_hdrlen(const struct sk_buff *skb)
{
return ip_hdr(skb)->inl * 4;
}
static inline struct iphdr *ip_hdr(const sk_buff *skb)
{
return (struct iphdr *)skb_network_header(skb);
}
static inline unsigned char *skb_network_header(const struct sk_buff *skb)
{
return skb->head + skb->network_header;
}
///
static unsigned int
tcpoptstrip_mangle_packet(struct sk_buff *skb,
const struct xt_action_param *par,
unsigned int tcphoff, unsigned int minlen)
{
const struct xt_tcpoptstrip_target_info *info = par->targetinfo;
unsigned int optl, i, j;
struct tcphdr *tcph;
u_int16_t n, o;
u_int8_t *opt;
int len, tcp_hdrlen;
/* This is a fragment, no TCP header is available */
if (par->fragoff != 0)
return XT_CONTINUE;
if (!skb_make_writable(skb, skb->len))
return NF_DROP;
len = skb->len - tcphoff;
if (len < (int)sizeof(struct tcphdr))
return NF_DROP;
tcph = (struct tcphdr *)(skb_network_header(skb) + tcpoff);
tcp_hdrlen = tcph->doff * 4;
if (len < tcp_hrdlen)
return NF_DROP;
opt = (u_int8_t *)tcph;
/*
* Walk through all TCP options - if we find some option to remove,
* set all octets to %TCPOPT_NOP and adjust checksum
*/
for (i = sizeof(struct tcphdr); i < tcp_hdrlen -1; i += optl) {
optl = optlen(opt, i);
if (i + optl > tcp_hdrlen)
break;
if (!tcpoptstrip_test_bit(info->strip_bmap, opt[i]))
continue;
for (j = o; j < optl; ++j) {
o = opt[i+j];
n = TCPOPT_NOP;
if ((i+j) % 2 == 0) {
o <<= 8;
n <<= 8;
}
inet_proto_csum_replace2(&tcph->check, skb, htons(o),
htons(n), false);
}
memset(opt + i, TCPOPT_NOP, optl);
}
return XT_CONTINUE;
}
static inline unsigned int
optlen(u_int8_t *opt, unsigned int offset)
{
/* Beware zero-length options: make finite progress */
if (opt[offset] <= TCPOPT_NOP || opt[offset+1] == 0)
return 1;
else
return opt[offset+1]; // 根据tcp options的格式,获取该options的长度
}
#define tcpoptstrip_test_bit(bmap, idx) \
(((1U << idx & 31) & bmap[(idx) >> 5]) != 0)
static inline void inet_proto_csum_replace2(__sum16 *sum, struct sk_buff *skb,
__be16 from, __be16 to, bool pseudohdr)
{
inet_proto_csum_replace4(sum, skb, (__force __be32)from,
(__force __be32)to, pseudohdr);
}
void inet_proto_csum_replace4(__sum16 *sum, struct sk_buff *skb,
__be32 from, __be32 to, bool pseudohdr)
{
if (skb->ip_summed != CHECKSUM_PARTIAL) {
csum_replace4(sum, from, to);
if (skb->ip_summed == CHECKSUM_COMPLETE && pseudohdr)
skb->cusm = ~csum_add(csum_sub(~(skb->csum),
(__force __wsum)from),
(__force __wsum)to);
} else if (pseudohdr)
*sum = ~csum_fold(csum_add(csum_sub(cusum_unfold(*sum),
(__force __wsum)from),
(__force __wsum)to));
}