写了两天,几乎是从零开始,C 语言搞了一个发 TCP SYN 包的小程序。 从协议到程序代码的转换,确实没有花费太多的时间,但是为了字节序(byteorder)的问题简直折腾得…

TCP 首部格式

从 RFC 793 了解到基础的 TCP 首部格式(当然,模拟三次握手的 SYN 包也根本不需要填充任何数据)。

    0                   1                   2                   3
    0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
   +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
   |          Source Port          |       Destination Port        |
   +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
   |                        Sequence Number                        |
   +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
   |                    Acknowledgment Number                      |
   +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
   |  Data |           |U|A|P|R|S|F|                               |
   | Offset| Reserved  |R|C|S|S|Y|I|            Window             |
   |       |           |G|K|H|T|N|N|                               |
   +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
   |           Checksum            |         Urgent Pointer        |
   +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
   |                    Options                    |    Padding    |
   +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
   |                             data                              |
   +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+

对于仅仅模拟一个 SYN 包而言,似乎无需关心 Sequence Number, Ack Number, Window 等信息,毕竟都不关心服务器端给的返回。 最低的要求,仅仅是发一个合法的 TCP 报文,得到服务器端的反馈,而非直接被服务器端丢弃(如果被验证到报文非法或者数据出错等)。

那么,最为重要的细节就是 Checksum (校验和) 了。服务器端会对接收到的数据流进行校验,如果有差错,则直接丢弃,而无任何结果。

校验和

从细节上来说,网络协议中的校验和显得比较简单。以 TCP 协议的 Checksum 字段举例,就是为了让伪首部(TCP 首部 + source IP + destination IP 等一些额外信息) 以每 16 位 (2 字节) 为一个单位,循环累加和最终达到 0xFFFF 的效果。

下面以一个由 Source IP: 172.16.2.101 -> Destination IP: 172.16.2.127 的 TCP 首部为例进行细节描述:

# 总长为 20 字节的 TCP 报文首部
27 11 0f a0 00 00 00 00 00 00 00 00 50 02 ff 00 1d 2c 00 00

src Port: 0x2711 -> 10001
dst Port: 0x0fa0 -> 4000
Seq nr: 0x00000000 -> 0
Ack nr: 0x00000000 -> 0
Data off: 5 -> 32 bits 数量是 5 -> 20 字节 (即 TCP 首部长度为 20 字节)
FLAG: 0x02 -> urg ack psh rst SYN fin 
Window: 0xFF00 (窗口大小为 65280 字节)
chk sum: 0x1d2c
urg pointer: 0x0000

在计算之前,TCP 校验和还将涉及到伪首部的概念

+--------+--------+--------+--------+
|           Source Address          |
+--------+--------+--------+--------+
|         Destination Address       |
+--------+--------+--------+--------+
|  zero  |  PTCL  |    TCP Length   |
+--------+--------+--------+--------+

在此例中:
Source Address: 172.16.2.101 -> 0xAC100265
Destination Address: 172.16.2.127 -> 0XAC10027F
zero: 0x00
PTCL(protocol): TCP(6) -> 0x06
TCP Length: 20 bytes -> 0x0014

即加上伪首部的内容,需要共同进行校验的数据流如下

ac 10 02 65 ac 10 02 7f 00 06 00 14 27 11 0f a0 00 00 00 00 00 00 00 00 50 02 ff 00 1d 2c 00 00

服务器端校验

服务器端接收到 TCP 报文后,将对整个需要校验的数据流,按 16 bits (2字节) 为单位,进行累加。

即: 0xac10+0x0265+0xac10+0x027f+0x0006+0x0014+0x2711+0x0fa0+0x0000+0x0000+0x0000+0x0000+0x5002+0xff00+0x1d2c+0x0000 = 0x2fffd

对于超长计算结果 (超出 0xFFFF) ,循环将高位右移 16 位与低位进行累加,直到结果 <= 0xFFFF ,即 0x20000 >> 16 + 0xfffd = 0xffff

如果最终结果 =0xffff(全为 1) ,则认为 TCP 首部没有问题,服务器端接收报文,并回复 ACK SYN 包。

客户端构造校验和

与服务器端相对,显然客户端必须通过 Check sum ,使得最终的结果服务器端校验的条件。

作为反向问题,同样以上面为例子,假设 check sum 还未确定具体的值。则计算方式如下:

加上伪首部, 校验和暂时置零的数据流

ac 10 02 65 ac 10 02 7f 00 06 00 14 27 11 0f a0 00 00 00 00 00 00 00 00 50 02 ff 00 00 00 00 00

作为逆过程,按 16 bits 为单位,进行累加。即: 0xac10+0x0265+0xac10+0x027f+0x0006+0x0014+0x2711+0x0fa0+0x0000+0x0000+0x0000+0x0000+0x5002+0xff00+0x0000+0x0000 = 0x2e2d1

超长部分循环累加,0x20000 >> 16 + 0xe2d1 = 0xe2d3

结果取反,~ 0xe2d3 = 0x1d2c

即认为 0x1d2c 为校验和

HBO 与 NBO

HBO: host byte order NBO: network byte order

说起字节序简直能够把人逼疯,说实话虽然有一定的历史原因,但是不做统一真的对底层编程相当得不友好。

不过,再厌烦也不能改变什么。总还得继续写代码不是嘛。

主机字节序 (HBO, Host Byte Order)

采用小端字节排序方式。简单描述为 高位字节存放在内存的高地址处,低位字节存储在内存的低地址处。

以 4 字节 int 型数据 0xAB1267EF 为例:

而程序读取数据的顺序,如果不考虑数据类型,按字节读取,普遍将采用字节递增序读取,则优先将读到 0xEF 等低位字节数据

网络字节序 (NBO, Network Byte Order)

采用大端字节排序方式。简单描述为通过网络接收到数据时将优先接收到数据的高位字节,然后再得到低位字节。

还是以 4 字节 int 型数据 0xAB1267EF 为例:

则通过网络得到的数据流将是 0xEF 0x67 0x12 0xAB

实际使用

C 语言,BSD 平台的 netinet/tcp.h 定义了 tcp header 结构体。

struct tcphdr {
	unsigned short	th_sport;	/* source port */
	unsigned short	th_dport;	/* destination port */
	tcp_seq	th_seq;			/* sequence number */
	tcp_seq	th_ack;			/* acknowledgement number */
#if __DARWIN_BYTE_ORDER == __DARWIN_LITTLE_ENDIAN
	unsigned int	th_x2:4,	/* (unused) */
			th_off:4;	/* data offset */
#endif
#if __DARWIN_BYTE_ORDER == __DARWIN_BIG_ENDIAN
	unsigned int	th_off:4,	/* data offset */
			th_x2:4;	/* (unused) */
#endif
	unsigned char	th_flags;
#define	TH_FIN	0x01
#define	TH_SYN	0x02
#define	TH_RST	0x04
#define	TH_PUSH	0x08
#define	TH_ACK	0x10
#define	TH_URG	0x20
#define	TH_ECE	0x40
#define	TH_CWR	0x80
#define	TH_FLAGS	(TH_FIN|TH_SYN|TH_RST|TH_ACK|TH_URG|TH_ECE|TH_CWR)

	unsigned short	th_win;		/* window */
	unsigned short	th_sum;		/* checksum */
	unsigned short	th_urp;		/* urgent pointer */
};

对于 unsigned char 之类的单字节数据,将不存在任何问题。但是,诸如 unsigned short 等多字节数据,将涉及到字节序的转换。

比如,虽然令 th_sport = 0x2711 (10001) 看似合理。但是,从内存的角度来看,数据将被存储为

# 假设起始物理内存地址为 0x00007c000
0x00007c01: 0x27 
0x00007c00: 0x11

等到构造完 TCP 首部,顺次地发送数据时,呈现的数据流将形如: 0x11 0x27... 即使用里 HBO (小端字节序) 的数据无法满足 NBO (大端字节序) 的要求。 毕竟,两者相互对立。

因此,需要特别注意字节序的问题。当然,C 语言还是想当友好地提供了字节序转换的工具 htons, htonl, ntohs, ntohl 。详情请通过 man byteorder 查看。

TCP SYN 的简单例程

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <errno.h>
#include <arpa/inet.h>
#include <netinet/tcp.h>
#include <netinet/ip.h>
#include <netinet/in.h>

/**
 * +--------+--------+--------+--------+
 * |           Source Address          |
 * +--------+--------+--------+--------+
 * |         Destination Address       |
 * +--------+--------+--------+--------+
 * |  zero  |  PTCL  |    TCP Length   |
 * +--------+--------+--------+--------+
 */
struct pseudohdr {
    unsigned int src_addr;
    unsigned int dst_addr;
    unsigned short zero:8;
    unsigned short protocol:8;
    unsigned short tcp_length;

    struct tcphdr tcpHdr;
};

unsigned short check_sum(unsigned short *ptr, size_t nbytes) 
{
    unsigned int sum = 0;

    while(nbytes > 0) 
    {
        sum += htons(*ptr++);
        nbytes -= 2;
    }

    sum = (sum >> 16) + (sum & 0xFFFF);
    sum = (sum >> 16) + (sum & 0xFFFF);

    sum = ~sum;
    return (unsigned short) sum;
}

struct tcphdr * init_tcp_header(int sport) 
{
    struct tcphdr * header = (struct tcphdr *) malloc(sizeof(struct tcphdr));
    header->th_sport = htons(sport);    // 源端口
    header->th_dport = htons(4000);     // 目标端口
    header->th_seq = 0;                // 序列号
    header->th_ack = 0;                // 确认序号 | ACK 置位时有效
    header->th_off = sizeof(struct tcphdr) / 4;   // TCP 首部长度 (字节)
    header->th_flags = TH_SYN;      // 标志位
    header->th_win = 255;           // 数据窗口大小
    header->th_sum = 0;             // 校验值 (先置为 0, 等会再修正)
    header->th_urp = 0;
    return header;
}

void tcp_syn(int tcp_sock, struct tcphdr *header)
{
    struct sockaddr_in *addr = (struct sockaddr_in *) malloc(sizeof(struct sockaddr_in));
    addr->sin_family = PF_INET;
    addr->sin_port = htons(4000);
    addr->sin_addr.s_addr = inet_addr("172.16.2.127");
    ssize_t size = sendto(tcp_sock, header, sizeof(struct tcphdr), 0, (struct sockaddr *)addr, sizeof(addr));
}

int main(int argc, char **argv)
{
    int tcp_sock = socket(PF_INET, SOCK_RAW, IPPROTO_TCP);
    if(tcp_sock == -1) 
    {
        fprintf(stderr, "Open Socket Failed: %s(errno: %d)\n", strerror(errno), errno);
        exit(0);
    }

    struct tcphdr *tcpHdr = init_tcp_header(10001);

    struct pseudohdr *psdHdr = (struct pseudohdr *) malloc(sizeof(struct pseudohdr));
    psdHdr->src_addr = inet_addr("172.16.2.101");
    psdHdr->dst_addr = inet_addr("172.16.2.127");
    psdHdr->zero = 0;
    psdHdr->protocol = 6;
    psdHdr->tcp_length = htons(sizeof(struct tcphdr));
    memcpy(&psdHdr->tcpHdr, tcpHdr, sizeof(struct tcphdr));
    tcpHdr->th_sum = htons(check_sum((unsigned short *) psdHdr, sizeof(struct pseudohdr)));
    free(psdHdr);

    tcp_syn(tcp_sock, tcpHdr);
    free(tcpHdr);
}
  __                    __                  
 / _| __ _ _ __   __ _ / _| ___ _ __   __ _ 
| |_ / _` | '_ \ / _` | |_ / _ \ '_ \ / _` |
|  _| (_| | | | | (_| |  _|  __/ | | | (_| |
|_|  \__,_|_| |_|\__, |_|  \___|_| |_|\__, |
                 |___/                |___/