编写一个TCP/IP栈3-TCP基础与握手
如今我们的用户空间TCP/IP堆栈在以太网和IPv4上的实现已降至最低,现在是时候研究令人棘手的传输控制协议(TCP)了。
TCP运行在OSI网络的第4层负责端对端传输操作, 负责恢复数据包递送中的错误连接和故障。事实上,TCP 是互联网的主力,如今几乎在所有计算机网络中都提供了可靠的通信服务。
TCP 并非全新的协议——第一个规范于1974年问世。从那以后,很多事情都发生了变化,也获得了许多扩展和修正。
本文将深入探讨TCP背后的基本理论,并尝试为其设计提供动力。此外,我们将查看TCP头部并讨论建立连接(TCP握手)。作为最后一步,我们将展示TCP在网络栈中的首个功能。
可靠性机制
可靠地发送数据的问题看似肤浅,但其实际实现却涉及问题。主要涉及数据报式网络中的错误修复问题:
- 发送方应等待接收方确认多长时间?
- 如果接收方无法按发送速度处理数据怎么办?
- 如果中间的网络(例如路由器)无法像发送数据时那样快速处理数据,该怎么办?
在所有情况下,数据包交换网络的潜在危险都适用——接收方的确认可能会被损坏,甚至在传输过程中丢失,从而使发送方陷入棘手的境地。
为解决这些问题,可以使用多种机制。最常见的就是滑动窗口技术,即双方对传输的数据进行记录。窗口数据被视为顺序(如数组的切片),该窗口在数据被双方处理(并确认)时向前滑动:

使用这种滑动窗口的便利之处在于,它能缓解流量控制。当接收端无法以发送速度快速处理数据时,需要进行流量控制。在这种情况下,滑动窗口的大小将被协商以降低,从而限制发送方的字节流输出。
拥塞控制有助于发送方和接收方之间的网络堆栈不受拥堵影响。有两种通用方法:在显式版本中,协议有一个字段,用于向发送方明确告知拥塞状态。在隐式版本中,发送方会尝试猜测网络何时出现拥堵,并限制其输出。总体而言,拥塞控制是一个复杂且反复出现的网络问题,相关研究至今仍在进行。
TCP基础
TCP 中的实现机制比其他协议(如 UDP 和 IP)要多得多。即TCP是一种面向连接的协议,这意味着作为第一步,在通信双方之间建立统一通信通道。双方积极维护这一联系:建立联系(握手),向对方告知数据状况等可能的问题。
TCP的另一个重要特性是它是一种流式协议。与UDP不同,TCP 无法保证应用程序在发送和接收时能够稳定地“记录”数据。TCP 实现必须缓冲数据,当数据包丢失、重新排序或损坏时,TCP 必须等待并组织缓冲区中的数据。只有当数据被认定为完整时,TCP 才能将数据交给应用程序的套接字。
当TCP以数据流的形式运行时,来自该流的“块”必须转换为IP可以承载的数据包。这称为包化,其中TCP头块包含流中当前索引的序列号。这还具有方便的属性,即流可以分成许多可变大小的段,而TCP则知道如何重新集成它们。
与IP类似,TCP也会检查消息是否具有完整性。这是通过与IP相同的校验和算法实现的,但增加了更多细节。主要情况下,校验和是端到端的,这意味着报文头和数据都包含在校验中。此外,还包含一个由IP头编造的伪头。
如果TCP实现接收损坏的片段,就会丢弃它们,并且不会通知发送方。此错误由发送方设置的定时器进行纠正,如果接收方从未确认该片段,则可用于重新传输片段。
TCP full-duplex是一个全双工系统,意味着流量可以双向同时流动。通信端必须将两个方向数据顺序保存在内存中。更具体地说,TCP 通过在发送的数据段中包含对反向流量的确认信息来减少流量占用。
从本质上讲,数据流的排序是 TCP 的主要原则。然而,保持数据流同步并非易事。
TCP 报头格式
接下来,我们将定义消息头并描述其字段。TCP 消息头看似简单,但实际上包含大量关于通信状态的信息。
TCP 报头大小为 20 字节:

源端口和目标端口字段用于建立与主机之间的多个连接。具体来说,伯克利套接字是应用程序绑定到 TCP 网络协议栈的常用接口。网络协议栈通过端口来确定流量的路由。由于这些字段为 16 位,因此端口值的范围为 0 到 65535。
由于数据流中的每个字节都已编号,因此序列号代表 TCP 段的窗口索引。在握手过程中,该序列号包含初始序列号 (ISN)。
确认号包含发送方预期接收的下一个字节的窗口索引。握手完成后,ACK 字段必须始终填充。
头部长度 (HL) 字段以 32 位表示头部的长度。
接下来,会显示几个标志位。前 4 位( rsvd )未使用。
- 拥塞窗口减少 (C) 用于通知发送方降低发送速率。
- ECN 回显 (E) 表示发送方已收到拥塞通知。
- 紧急指针 (U) 表示该段包含优先数据。
- ACK (A)字段用于传递 TCP 握手的状态。在连接的剩余时间内,它将一直保持开启状态。
- PSH (P)用于指示接收方应尽快将数据“推送”到应用程序。
- RST (R)重置 TCP 连接。
- SYN (S) 用于在初始握手中同步序列号。
- FIN (F)表示发送方已完成数据发送。
窗口大小字段用于指定窗口大小。换句话说,它表示接收方愿意接收的字节数。由于这是一个 16 位字段,因此最大窗口大小为 65,535 字节。
TCP 校验和字段用于验证 TCP 数据段的完整性。其算法与互联网协议相同,但输入数据段除了包含 TCP 数据外,还包含来自 IP 数据报的伪头部。
当设置了 U 标志时,会使用紧急指针 。该指针指示数据流中紧急数据的位置。
在邮件头之后,可以提供多个选项。例如, 最大段大小 (MSS) 就是其中之一,发送方可以通过它告知接收方数据段的最大长度。
在列出所有可选方案之后,才是实际数据。然而,这些数据并非必需。例如,握手过程仅需 TCP 头部即可完成。
TCP握手
TCP 连接通常经历以下几个阶段:连接建立(握手)、数据传输和连接关闭。下图展示了 TCP 的典型握手过程:

- 主机 A 的套接字处于关闭状态,这意味着它不接受连接。相反,主机 B 绑定到特定端口的套接字正在监听新的连接。
- 主机 A 打算与主机 B 建立连接。因此,A 构造了一个 TCP 段,该段的 SYN 标志已设置,并且序列字段的值也已填充为 (100)。
- 主机 B 以一个设置了 SYN 和 ACK 字段的 TCP 段进行响应,并通过将 A 的序列号加 1 来确认(ACK=101)。同样,B 也生成一个序列号(300)。
- 三次握手以连接请求发起方 (A) 发送的确认 (ACK) 结束。确认字段反映了主机接下来期望从另一方收到的序列号。
- 数据开始流动,主要是因为双方都确认了对方的数据序号。
这是建立 TCP 连接的常见场景。然而,由此会产生几个问题:
- 初始序列号是如何选择的?
- 如果双方同时向对方提出连接请求呢?
- 如果某些环节延迟一段时间甚至无限期延迟怎么办?
初始序列号 (ISN) 由通信双方在首次连接时独立选择。由于它是识别连接的关键部分,因此必须选择尽可能唯一且不易猜测的序列号。事实上, TCP 序列号攻击 指的是攻击者可以复制 TCP 连接并有效地向目标发送数据,从而冒充可信主机的情况。
The original specification suggests that the ISN is chosen by a counter that increments every 4 microseconds. This, however, can be guessed by an attacker. In reality, modern networking stacks generate the ISN by more complicated methods.
原始规范表明,ISN 由一个每 4 微秒递增一次的计数器生成。然而,攻击者可以猜到这一点。实际上,现代网络协议栈采用更复杂的方法生成 ISN。
当两端都收到对方的连接请求(SYN)时,这种情况称为同时打开 。TCP 握手过程中会通过额外的消息交换来解决这个问题:双方都发送一个 ACK(彼此并不知道对方是否也发送了 ACK),然后双方都发送 SYN-ACK 来确认请求。之后,数据传输开始。
最后,TCP 实现必须包含一个计时器,用于判断何时放弃建立连接。系统会尝试重新建立连接,通常采用指数退避算法,但一旦达到最大重试次数或时间阈值,连接就会被视为已断开。
TCP 选项
TCP 报头段的最后一个字段保留用于存储 TCP 选项。最初的规范提供了三个选项,但后来的规范又增加了更多选项。接下来,我们将介绍最常用的选项。
最大段大小 (MSS) 选项指定 TCP 实现能够接收的最大 TCP 段大小。在 IPv4 中,该值的典型值为 1460 字节。
选择性确认 (SACK) 选项优化了传输过程中大量数据包丢失、接收端数据窗口出现“空洞”的情况。为了弥补由此导致的吞吐量下降,TCP 实现可以通过 SACK 通知发送方哪些数据包未收到。因此,与累积确认机制相比,发送方能够以更直接的方式获取数据状态信息。
窗口缩放选项可以增大原本受限的 16 位窗口大小。具体来说,如果双方都在握手段中包含此选项,则窗口大小将乘以该缩放比例。更大的窗口大小对于大批量数据传输尤为重要。
时间戳选项允许发送方在 TCP 数据段中插入时间戳,该时间戳可用于计算每个 ACK 数据段的往返时间 (RTT)。此信息随后可用于计算 TCP 重传超时时间。
测试TCP握手
现在我们已经模拟了TCP握手程序,并且它有效地监听了每个端口,接下来我们来测试一下:

由于nmap进行SYN扫描(它只等待 SYN-ACK 来判断目标端口是否打开),因此很容易误以为我们只需返回一个SYN-ACK TCP段即可在端口上监听应用程序。
关键实现
struct net_ops tcp_ops = {
.alloc_sock = &tcp_alloc_sock,
.init = &tcp_v4_init_sock,
.connect = &tcp_v4_connect,
.disconnect = &tcp_disconnect,
.write = &tcp_write,
.read = &tcp_read,
.recv_notify = &tcp_recv_notify,
.close = &tcp_close,
.abort = &tcp_abort,
};
void tcp_init()
{
}
static void tcp_init_segment(struct tcphdr *th, struct iphdr *ih, struct sk_buff *skb)
{
th->sport = ntohs(th->sport);
th->dport = ntohs(th->dport);
th->seq = ntohl(th->seq);
th->ack_seq = ntohl(th->ack_seq);
th->win = ntohs(th->win);
th->csum = ntohs(th->csum);
th->urp = ntohs(th->urp);
skb->seq = th->seq;
skb->dlen = ip_len(ih) - tcp_hlen(th);
skb->len = skb->dlen + th->syn + th->fin;
skb->end_seq = skb->seq + skb->dlen;
skb->payload = th->data;
}
static void tcp_clear_queues(struct tcp_sock *tsk) {
skb_queue_free(&tsk->ofo_queue);
}
void tcp_in(struct sk_buff *skb)
{
struct sock *sk;
struct iphdr *iph;
struct tcphdr *th;
iph = ip_hdr(skb);
th = (struct tcphdr*) iph->data;
tcp_init_segment(th, iph, skb);
sk = inet_lookup(skb, th->sport, th->dport);
if (sk == NULL) {
print_err("No TCP socket for sport %d dport %d\n",
th->sport, th->dport);
free_skb(skb);
return;
}
socket_wr_acquire(sk->sock);
tcp_in_dbg(th, sk, skb);
/* if (tcp_checksum(iph, th) != 0) { */
/* goto discard; */
/* } */
tcp_input_state(sk, th, skb);
socket_release(sk->sock);
}
int tcp_udp_checksum(uint32_t saddr, uint32_t daddr, uint8_t proto,
uint8_t *data, uint16_t len)
{
uint32_t sum = 0;
sum += saddr;
sum += daddr;
sum += htons(proto);
sum += htons(len);
return checksum(data, len, sum);
}
int tcp_v4_checksum(struct sk_buff *skb, uint32_t saddr, uint32_t daddr)
{
return tcp_udp_checksum(saddr, daddr, IP_TCP, skb->data, skb->len);
}
struct sock *tcp_alloc_sock()
{
struct tcp_sock *tsk = malloc(sizeof(struct tcp_sock));
memset(tsk, 0, sizeof(struct tcp_sock));
tsk->sk.state = TCP_CLOSE;
tsk->sackok = 1;
tsk->rmss = 1460;
// Default to 536 as per spec
tsk->smss = 536;
skb_queue_init(&tsk->ofo_queue);
return (struct sock *)tsk;
}
int tcp_v4_init_sock(struct sock *sk)
{
tcp_init_sock(sk);
return 0;
}
int tcp_init_sock(struct sock *sk)
{
return 0;
}
void __tcp_set_state(struct sock *sk, uint32_t state)
{
sk->state = state;
}
static uint16_t generate_port()
{
/* TODO: Generate a proper port */
static int port = 40000;
pthread_rwlock_wrlock(&tcplock);
int copy = ++port + (timer_get_tick() % 10000);
pthread_rwlock_unlock(&tcplock);
return copy;
}
int generate_iss()
{
/* TODO: Generate a proper ISS */
return (int)time(NULL) * rand();
}
int tcp_v4_connect(struct sock *sk, const struct sockaddr *addr, int addrlen, int flags)
{
uint16_t dport = ((struct sockaddr_in *)addr)->sin_port;
uint32_t daddr = ((struct sockaddr_in *)addr)->sin_addr.s_addr;
sk->dport = ntohs(dport);
sk->sport = generate_port();
sk->daddr = ntohl(daddr);
/* TODO: Do not hardcode lvl-ip local interface */
sk->saddr = parse_ipv4_string("10.0.0.4");
return tcp_connect(sk);
}
int tcp_disconnect(struct sock *sk, int flags)
{
return 0;
}
int tcp_write(struct sock *sk, const void *buf, int len)
{
struct tcp_sock *tsk = tcp_sk(sk);
int ret = sk->err;
if (ret != 0) goto out;
switch (sk->state) {
case TCP_ESTABLISHED:
case TCP_CLOSE_WAIT:
break;
default:
ret = -EBADF;
goto out;
}
return tcp_send(tsk, buf, len);
out:
return ret;
}
int tcp_read(struct sock *sk, void *buf, int len)
{
struct tcp_sock *tsk = tcp_sk(sk);
int ret = -1;
switch (sk->state) {
case TCP_CLOSE:
ret = -EBADF;
goto out;
case TCP_LISTEN:
case TCP_SYN_SENT:
case TCP_SYN_RECEIVED:
/* Queue for processing after entering ESTABLISHED state. If there
is no room to queue this request, respond with "error:
insufficient resources". */
case TCP_ESTABLISHED:
case TCP_FIN_WAIT_1:
case TCP_FIN_WAIT_2:
/* If insufficient incoming segments are queued to satisfy the
request, queue the request. */
break;
case TCP_CLOSE_WAIT:
/* If no text is awaiting delivery, the RECEIVE will get a
"error: connection closing" response. Otherwise, any remaining
text can be used to satisfy the RECEIVE. */
if (!skb_queue_empty(&tsk->sk.receive_queue)) break;
if (tsk->flags & TCP_FIN) {
tsk->flags &= ~TCP_FIN;
return 0;
}
break;
case TCP_CLOSING:
case TCP_LAST_ACK:
case TCP_TIME_WAIT:
ret = sk->err;
goto out;
default:
goto out;
}
return tcp_receive(tsk, buf, len);
out:
return ret;
}
int tcp_recv_notify(struct sock *sk)
{
if (&(sk->recv_wait)) {
return wait_wakeup(&sk->recv_wait);
}
// No recv wait lock
return -1;
}
int tcp_close(struct sock *sk)
{
switch (sk->state) {
case TCP_CLOSE:
case TCP_CLOSING:
case TCP_LAST_ACK:
case TCP_TIME_WAIT:
case TCP_FIN_WAIT_1:
case TCP_FIN_WAIT_2:
/* Respond with "error: connection closing". */
sk->err = -EBADF;
return -1;
case TCP_LISTEN:
case TCP_SYN_SENT:
case TCP_SYN_RECEIVED:
return tcp_done(sk);
case TCP_ESTABLISHED:
/* Queue this until all preceding SENDs have been segmentized, then
form a FIN segment and send it. In any case, enter FIN-WAIT-1
state. */
tcp_set_state(sk, TCP_FIN_WAIT_1);
tcp_queue_fin(sk);
break;
case TCP_CLOSE_WAIT:
/* Queue this request until all preceding SENDs have been
segmentized; then send a FIN segment, enter LAST_ACK state. */
tcp_queue_fin(sk);
break;
default:
print_err("Unknown TCP state for close\n");
return -1;
}
return 0;
}
int tcp_abort(struct sock *sk)
{
struct tcp_sock *tsk = tcp_sk(sk);
tcp_send_reset(tsk);
return tcp_done(sk);
}
static int tcp_free(struct sock *sk)
{
struct tcp_sock *tsk = tcp_sk(sk);
tcp_clear_timers(sk);
tcp_clear_queues(tsk);
wait_wakeup(&sk->sock->sleep);
return 0;
}
int tcp_done(struct sock *sk)
{
tcp_set_state(sk, TCP_CLOSING);
tcp_free(sk);
return socket_delete(sk->sock);
}
void tcp_clear_timers(struct sock *sk)
{
struct tcp_sock *tsk = tcp_sk(sk);
tcp_stop_rto_timer(tsk);
tcp_stop_delack_timer(tsk);
timer_cancel(tsk->keepalive);
tsk->keepalive = NULL;
timer_cancel(tsk->linger);
tsk->linger = NULL;
}
void tcp_stop_rto_timer(struct tcp_sock *tsk)
{
if (tsk) {
timer_cancel(tsk->retransmit);
tsk->retransmit = NULL;
tsk->backoff = 0;
}
}
void tcp_release_rto_timer(struct tcp_sock *tsk)
{
if (tsk) {
timer_release(tsk->retransmit);
tsk->retransmit = NULL;
}
}
void tcp_stop_delack_timer(struct tcp_sock *tsk)
{
timer_cancel(tsk->delack);
tsk->delack = NULL;
}
void tcp_release_delack_timer(struct tcp_sock *tsk)
{
timer_release(tsk->delack);
tsk->delack = NULL;
}
void tcp_handle_fin_state(struct sock *sk)
{
switch (sk->state) {
case TCP_CLOSE_WAIT:
tcp_set_state(sk, TCP_LAST_ACK);
break;
case TCP_ESTABLISHED:
tcp_set_state(sk, TCP_FIN_WAIT_1);
break;
}
}
static void *tcp_linger(void *arg)
{
struct sock *sk = (struct sock *) arg;
socket_wr_acquire(sk->sock);
struct tcp_sock *tsk = tcp_sk(sk);
tcpsock_dbg("TCP time-wait timeout, freeing TCB", sk);
timer_cancel(tsk->linger);
tsk->linger = NULL;
tcp_done(sk);
socket_release(sk->sock);
return NULL;
}
static void *tcp_user_timeout(void *arg)
{
struct sock *sk = (struct sock *) arg;
socket_wr_acquire(sk->sock);
struct tcp_sock *tsk = tcp_sk(sk);
tcpsock_dbg("TCP user timeout, freeing TCB and aborting conn", sk);
timer_cancel(tsk->linger);
tsk->linger = NULL;
tcp_abort(sk);
socket_release(sk->sock);
return NULL;
}
void tcp_enter_time_wait(struct sock *sk)
{
struct tcp_sock *tsk = tcp_sk(sk);
tcp_set_state(sk, TCP_TIME_WAIT);
tcp_clear_timers(sk);
/* RFC793 arbitrarily defines MSL to be 2 minutes */
tsk->linger = timer_add(TCP_2MSL, &tcp_linger, sk);
}
void tcp_rearm_user_timeout(struct sock *sk)
{
struct tcp_sock *tsk = tcp_sk(sk);
if (sk->state == TCP_TIME_WAIT) return;
timer_cancel(tsk->linger);
/* RFC793 set user timeout */
tsk->linger = timer_add(TCP_USER_TIMEOUT, &tcp_user_timeout, sk);
}
void tcp_rtt(struct tcp_sock *tsk)
{
if (tsk->backoff > 0 || !tsk->retransmit) {
// Karn's Algorithm: Don't measure retransmissions
return;
}
int r = timer_get_tick() - (tsk->retransmit->expires - tsk->rto);
if (r < 0) return;
if (!tsk->srtt) {
/* RFC6298 2.2 first measurement is made */
tsk->srtt = r;
tsk->rttvar = r / 2;
} else {
/* RFC6298 2.3 a subsequent measurement is made */
double beta = 0.25;
double alpha = 0.125;
tsk->rttvar = (1 - beta) * tsk->rttvar + beta * abs(tsk->srtt - r);
tsk->srtt = (1 - alpha) * tsk->srtt + alpha * r;
}
int k = 4 * tsk->rttvar;
/* RFC6298 says RTO should be at least 1 second. Linux uses 200ms */
if (k < 200) k = 200;
tsk->rto = tsk->srtt + k;
}
int tcp_calculate_sacks(struct tcp_sock *tsk)
{
struct tcp_sack_block *sb = &tsk->sacks[tsk->sacklen];
sb->left = 0;
sb->right = 0;
struct sk_buff *next;
struct list_head *item, *tmp;
list_for_each_safe(item, tmp, &tsk->ofo_queue.head) {
next = list_entry(item, struct sk_buff, list);
if (sb->left == 0) {
sb->left = next->seq;
tsk->sacklen++;
}
if (sb->right == 0) sb->right = next->end_seq;
else if (sb->right == next->seq) sb->right = next->end_seq;
else {
if (tsk->sacklen >= tsk->sacks_allowed) break;
sb = &tsk->sacks[tsk->sacklen];
sb->left = next->seq;
sb->right = next->end_seq;
tsk->sacklen++;
}
}
return 0;
}
该项目的源代码托管在 GitHub 上。