Linux内核传输层源码分析SCTP

一、流控制传输协议(SCTP)

SCTP(Stream Control Transmission Protocol,流控制传输协议)是在 2007 年发布的 RFC 4960 中定义的,但它首次被定义则是在 2000 年。SCTP 设计用于通过 IP 网络传输公共交换电话网络(Public Switched Telephone Network, PSTN)信令。

SCTP 初始化操作,方法sctp_init()可为各种结构分配内存,并在 IPv4 和 IPv6 中注册 SCTP,内核源码如下:

1.流控制传输协议(SCTP)的主要特征

(1)SCTP兼具TCP和UDP的特点;

(2)SCTP使用四次握手来防范SYN洪泛攻击,提高安全性;

(3)SCTP支持多宿主,即两端有多个ip地址;

(4)SCTP支持多流;

(5)SCTP在多宿主情形下使用心跳机制来检测空闲、不可达的对等体。

(1)SCTP 兼具 TCP 和 UDP 的特点

  • 详细解释
    • TCP 是面向连接、可靠的传输协议,它通过三次握手建立连接,确保数据有序、无差错地传输,有流量控制和拥塞控制机制。而 UDP 是无连接、不可靠的传输协议,它不需要建立连接,传输速度快,但不保证数据的可靠到达和顺序。SCTP 结合了两者的优点,它是面向连接的,能提供可靠的数据传输,就像 TCP 一样;同时它也支持消息边界,即可以像 UDP 那样以独立的消息为单位进行传输。
  • 使用案例:在 VoIP(网络电话)应用中,SCTP 可以像 TCP 一样保证语音数据包的可靠传输,避免语音数据丢失或乱序,从而保证通话质量;又能像 UDP 一样以消息为单位进行传输,提高传输效率,减少延迟。

(2)SCTP 使用四次握手来防范 SYN 洪泛攻击,提高安全性

  • 详细解释
    • SYN 洪泛攻击是一种常见的 DoS(拒绝服务)攻击,攻击者发送大量的 SYN 包,耗尽服务器资源,使服务器无法响应正常的连接请求。TCP 使用三次握手建立连接,容易受到这种攻击。而 SCTP 使用四次握手,在建立连接时,服务器会先发送一个包含状态 cookie 的响应,客户端需要使用这个 cookie 来完成连接建立。这样即使攻击者发送大量的 SYN 包,服务器也不会为其分配资源,只有合法的客户端使用正确的 cookie 才能完成连接,从而有效防范 SYN 洪泛攻击。
  • 使用案例:在企业的网络服务器中,当面临来自外部网络的恶意攻击风险时,使用 SCTP 协议可以增强服务器的安全性。例如,企业的邮件服务器采用 SCTP 协议,能够有效抵御 SYN 洪泛攻击,保证邮件服务的正常运行,防止大量虚假连接请求导致服务器崩溃。

(3)SCTP 支持多宿主,即两端有多个 ip 地址

  • 详细解释
    • 多宿主意味着一个 SCTP 端点可以有多个 IP 地址,这些 IP 地址可以属于不同的子网或网络接口。当一个 IP 地址出现故障或不可达时,SCTP 可以自动切换到其他可用的 IP 地址继续进行数据传输,从而提高了通信的可靠性和可用性。
  • 使用案例:在移动办公场景中,企业员工的笔记本电脑可能同时连接到公司的无线网络和移动数据网络,即拥有多个 IP 地址。当员工在移动过程中,无线网络信号不稳定时,SCTP 协议可以自动切换到移动数据网络的 IP 地址,保证与公司服务器的通信不中断,员工可以继续正常工作,如访问公司内部的文件系统、进行视频会议等。

(4)SCTP 支持多流

  • 详细解释
    • 多流是指在一个 SCTP 连接中可以同时存在多个独立的数据流,每个数据流都有自己的序列号和确认机制,它们之间相互独立,互不影响。这样可以提高数据传输的效率,特别是在处理不同类型的数据时。例如,一个应用程序可能需要同时传输音频、视频和控制信息,使用 SCTP 的多流功能可以将这些数据分别放在不同的流中进行传输,即使某个流出现丢包或延迟,也不会影响其他流的数据传输。
  • 使用案例:在在线视频会议系统中,音频、视频和文字聊天信息可以分别通过不同的 SCTP 流进行传输。如果视频流因为网络拥塞出现丢包,只会影响视频的播放质量,而不会影响音频和文字聊天的正常进行,用户仍然可以清晰地听到声音和进行文字交流。

(5)SCTP 在多宿主情形下使用心跳机制来检测空闲、不可达的对等体

  • 详细解释
    • 在多宿主环境中,由于端点有多个 IP 地址,可能会出现某些 IP 地址对应的网络链路不可用的情况。SCTP 通过心跳机制,定期向对等体发送心跳消息,以检测对等体是否可达和是否处于空闲状态。如果在一定时间内没有收到对等体的响应,就认为该对等体不可达,然后可以采取相应的措施,如切换到其他可用的 IP 地址或关闭连接。
  • 使用案例:在分布式数据中心中,不同的数据中心节点之间通过 SCTP 协议进行通信,每个节点可能有多个网络接口和 IP 地址。当一个节点的某个网络接口出现故障时,SCTP 的心跳机制会及时检测到该链路不可达,然后自动切换到其他可用的链路,保证数据中心之间的通信正常进行,例如,实时同步数据、协同处理任务等。

对于上述这些特征,对应的代码案例如下:

sctp_server.c

cpp 复制代码
//gcc sctp_server.c -o sctp_server -lsctp
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <netinet/sctp.h>
#include <unistd.h>

#define PORT 5000         // 服务器监听端口号
#define BUFFER_SIZE 1024  // 数据缓冲区大小
#define NUM_STREAMS 3     // 支持的 SCTP 流数量

//该函数处理客户端连接,接收并响应多流数据。
void handle_connection(int connfd){
    char buffer[BUFFER_SIZE];  //数据缓冲区
    ssize_t bytes_received;   //接收的字节数
    struct sctp_sndrcvinfo sndrcvinfo; //SCTP发送、接收信息的结构体
    int flags;                //接收标志

    //多流数据接收循环
    for(int stream = 0;stream < NUM_STREAMS;stream++){
        //接收指定流的数据
        /*
        sctp_recvmsg: SCTP 专用接收函数,支持多流
        参数说明:
        connfd: 连接文件描述符
        buffer: 接收缓冲区
        BUFFER_SIZE: 缓冲区大小
        NULL: 发送方地址(未使用)
        0: 地址长度(未使用)
        &sndrcvinfo: 获取 SCTP 消息元数据(如流 ID)
        &flags: 接收标志(未使用)
        */
        bytes_received = sctp_recvmsg(connfd,buffer,BUFFER_SIZE,NULL,0,&sndrcvinfo,&flags);
        if (bytes_received < 0) {
            perror("recvmsg failed");
            continue;
        }
        buffer[bytes_received] = '\0';
        printf("Received on stream %d: %s\n", sndrcvinfo.sinfo_stream, buffer);

        // 发送响应到相应的流
        const char *response = "Message received";
        /*
        sctp_sendmsg: SCTP 专用发送函数,支持多流
        参数说明:
        connfd: 连接文件描述符
        response: 发送的数据
        strlen(response): 数据长度
        NULL: 目标地址(未使用)
        0: 地址长度(未使用)
        0: 优先级(未使用)
        0: 标记(未使用)
        sndrcvinfo.sinfo_stream: 目标流 ID
        0: 生存时间(未使用)
        0: 上下文(未使用)
        */
        sctp_sendmsg(connfd, response, strlen(response), NULL, 0, 0, 0, sndrcvinfo.sinfo_stream, 0, 0);
    }
}

int main(int argc,char *argv[]){
    int sockfd, connfd;                // 套接字文件描述符
    struct sockaddr_in server_addr;    // 服务器地址结构体
    struct sctp_initmsg initmsg;       // SCTP 初始化消息结构体

    //创建 SCTP 套接字
    sockfd = socket(AF_INET, SOCK_STREAM, IPPROTO_SCTP);
    if (sockfd < 0) {
        perror("socket creation failed");
        return -1;
    }

    //初始化服务器地址
    memset(&server_addr, 0, sizeof(server_addr));
    server_addr.sin_family = AF_INET;
    server_addr.sin_addr.s_addr = INADDR_ANY; // 绑定所有网络接口
    server_addr.sin_port = htons(PORT);      // 端口号转换为网络字节序

    //绑定套接字
    if (bind(sockfd, (struct sockaddr *)&server_addr, sizeof(server_addr)) < 0) {
        perror("bind failed");
        close(sockfd);
        return -1;
    }

    //配置SCTP参数(多流处理)
    memset(&initmsg,0,sizeof(initmsg));
    initmsg.sinit_num_ostreams = NUM_STREAMS;  //输出流数量
    initmsg.sinit_max_instreams = NUM_STREAMS;  //输入流最大数量
    initmsg.sinit_max_attempts = 4;    //连接重试次数
    /*
    sockfd:这是之前通过 socket 函数创建的套接字描述符,代表了当前的 SCTP 套接字。
    level:指定选项所在的协议层。这里使用 IPPROTO_SCTP,表示选项是针对 SCTP 协议的。
    optname:指定要设置的具体选项。SCTP_INITMSG 表示要设置 SCTP 连接初始化时的参数。
    optval:指向包含要设置选项值的缓冲区。这里是 &initmsg,即 initmsg 结构体的地址,initmsg 结构体中包含了之前设置的 SCTP 初始化参数,如传出流数量、传入流最大数量、连接重试次数等。
    optlen:指定 optval 所指向缓冲区的大小。这里使用 sizeof(initmsg),确保传递了完整的 initmsg 结构体。
    */
    if(setsockopt(sockfd,IPPROTO_SCTP,SCTP_INITMSG,&initmsg,sizeof(initmsg)) < 0){
        perror("setsockopt SCTP_INITMSG failed");
        close(sockfd);
        return -1;
    }

    //监听连接
    if (listen(sockfd, 5) < 0) {
        perror("listen failed");
        close(sockfd);
        return -1;
    }

    printf("SCTP server listening on port %d...\n", PORT);

    //接受客户端连接
    connfd = accept(sockfd, NULL, NULL);
    if (connfd < 0) {
        perror("accept failed");
        close(sockfd);
        return -1;
    }

    //处理连接
    handle_connection(connfd);

    close(connfd);
    close(sockfd);
    return 0;
}

sctp_client.c

cpp 复制代码
//gcc sctp_client.c -o sctp_client -lsctp
#include <stdio.h>      // 标准输入输出函数
#include <stdlib.h>     // 内存分配、进程控制等函数
#include <string.h>     // 字符串操作函数
#include <sys/socket.h> // 套接字编程相关函数
#include <netinet/in.h> // 网络地址结构(如 sockaddr_in)
#include <arpa/inet.h>  // IP 地址转换函数(如 inet_addr)
#include <netinet/sctp.h> // SCTP 协议专用函数和结构体
#include <unistd.h>     // 通用 Unix 函数(如 close、sleep)

#define SERVER_IP1 "127.0.0.1"  // 第一个服务器 IP 地址(本地回环)
#define SERVER_IP2 "192.168.186.138" // 第二个服务器 IP 地址(假设的另一个接口)
#define PORT 5000               // 服务器端口号
#define BUFFER_SIZE 1024        // 数据缓冲区大小
#define NUM_STREAMS 3           // 支持的 SCTP 流数量

//该函数尝试连接服务器,并返回连接结果。
int connect_to_server(int sockfd,struct sockaddr_in *server_addr){
    if (connect(sockfd, (struct sockaddr *)server_addr, sizeof(*server_addr)) < 0) {
        perror("connect failed");
        return -1;
    }
    return 0;
}

int main(int argc,char *argv[]){
    int sockfd;                         // SCTP 套接字文件描述符
    struct sockaddr_in server_addr;      // 服务器地址结构体
    char buffer[BUFFER_SIZE];            // 数据缓冲区
    struct sctp_initmsg initmsg;         // SCTP 初始化消息结构体

    sockfd = socket(AF_INET, SOCK_STREAM, IPPROTO_SCTP);
    if (sockfd < 0) {
        perror("socket creation failed");
        return -1;
    }

    //配置 SCTP 参数(多流支持)
    memset(&initmsg, 0, sizeof(initmsg));
    initmsg.sinit_num_ostreams = NUM_STREAMS;   // 传出流数量
    initmsg.sinit_max_instreams = NUM_STREAMS; // 传入流最大数量
    initmsg.sinit_max_attempts = 4;            // 连接重试次数
    if (setsockopt(sockfd, IPPROTO_SCTP, SCTP_INITMSG, &initmsg, sizeof(initmsg)) < 0) {
        perror("setsockopt SCTP_INITMSG failed");
        close(sockfd);
        return -1;
    }

    memset(&server_addr, 0, sizeof(server_addr));
    server_addr.sin_family = AF_INET;
    server_addr.sin_port = htons(PORT);
    server_addr.sin_addr.s_addr = inet_addr(SERVER_IP1);

    //尝试连接第一个ip地址
    if (connect_to_server(sockfd, &server_addr) < 0) {
        //尝试连接第二个ip地址
        server_addr.sin_addr.s_addr = inet_addr(SERVER_IP2);
        if (connect_to_server(sockfd, &server_addr) < 0) {
            close(sockfd);
            return -1;
        }  
    }

    //多流数据发送循环
    for(int stream = 0;stream < NUM_STREAMS;stream++){
        snprintf(buffer, BUFFER_SIZE, "Message from stream %d", stream);
        sctp_sendmsg(sockfd, buffer, strlen(buffer), NULL, 0, 0, 0, stream, 0, 0);
    
        // 接收响应
        ssize_t bytes_received = sctp_recvmsg(sockfd, buffer, BUFFER_SIZE, NULL, 0, NULL, NULL);
        if (bytes_received < 0) {
            perror("recvmsg failed");
            continue;
        }
        buffer[bytes_received] = '\0';
        printf("Received response for stream %d: %s\n", stream, buffer);
    }

    // 模拟心跳机制
    const char *heartbeat_msg = "HEARTBEAT";
    for (int i = 0; i < 3; i++) {
        sctp_sendmsg(sockfd, heartbeat_msg, strlen(heartbeat_msg), NULL, 0, 0, 0, 0, 0, 0);
        sleep(1);
    }

    close(sockfd);
    return 0;
}

编译运行:

gcc sctp_server.c -o sctp_server -lsctp

gcc sctp_client.c -o sctp_client -lsctp

2.SCTP数据包和数据块

每个SCTP数据包都有一个通用的SCTP报头,后面紧跟着一个或者多个块。块包含数据或者SCTP控制信息。SCTP报头以及块内核源码如下:

一、sctp_chunkhdr 结构体重要字段讲解

  1. __u8 type

    • 作用 :标识 SCTP 数据块的类型。不同取值对应不同功能,例如:
      • SCTP_DATA:数据块;
      • SCTP_ACK:确认块;
      • SCTP_INIT:初始化块。
    • 意义:接收方通过该字段判断块的功能,从而执行对应处理逻辑。
  2. __u8 flags

    • 作用 :保留字段,当前协议设计中发送方需将其全置为 0,接收方忽略该字段。未来协议扩展可能赋予其新功能。
  3. __be16 length

    • 作用:表示 SCTP 块的长度(包括块头部自身),单位为字节。通过该字段可确定块数据的边界,避免解析越界。

二、使用方式(内核编程场景)

在 Linux 内核中处理 SCTP 数据时,通常结合 sk_buff(套接字缓冲区)使用,示例如下:

cpp 复制代码
// 1. 解析SCTP通用头部
struct sctphdr *sctp_hdr = (struct sctphdr *)skb_transport_header(skb);
u16 src_port = ntohs(sctp_hdr->source);
u16 dest_port = ntohs(sctp_hdr->dest);
u32 verification_tag = ntohl(sctp_hdr->vtag);

// 2. 计算校验和(若失败则丢弃报文)
if (sctp_checksum_disable || !sctp_checksum(skb, sctp_hdr)) {
    kfree_skb(skb);
    return;
}

// 3. 遍历所有块
unsigned int chunk_len;
struct sctp_chunkhdr *chunk_hdr;
skb_pull(skb, sizeof(struct sctphdr)); // 剥离SCTP头部

while (skb->len > 0) {
    chunk_hdr = (struct sctp_chunkhdr *)skb->data;
    chunk_len = ntohs(chunk_hdr->length);

    switch (chunk_hdr->type) {
        case SCTP_CID_DATA:
            // 解析数据块
            struct sctp_data_chunk *data_chunk = (struct sctp_data_chunk *)chunk_hdr;
            u32 tsn = ntohl(data_chunk->tsn);
            process_data_chunk(tsn, data_chunk->payload, chunk_len - sizeof(*data_chunk));
            break;
        case SCTP_CID_INIT:
            // 解析初始化块
            struct sctp_init_chunk *init_chunk = (struct sctp_init_chunk *)chunk_hdr;
            handle_init_chunk(init_chunk);
            break;
        // 其他块类型(如SACK、HEARTBEAT等)...
    }

    skb_pull(skb, chunk_len); // 移动到下一个块
}

不同类型块的结构体示例

1. 数据块(DATA chunk)
cpp 复制代码
struct sctp_data_chunk {
    struct sctp_chunkhdr common;  /* 通用块头部 */
    __be32 tsn;                   /* 传输序列号 */
    __be32 stream_id;             /* 流标识符 */
    __be32 stream_seq;            /* 流序列号 */
    __be32 payload_protocol_id;   /* 有效负载协议标识符 */
    /* 实际的数据部分紧跟在这些字段之后 */
};
  • tsn:传输序列号,用于保证数据的顺序和完整性。
  • stream_id:标识数据所属的流,SCTP 支持多流传输。
  • stream_seq:流内的序列号,用于在每个流中保证数据的顺序。
  • payload_protocol_id:表示有效负载所使用的协议,例如可能是某种应用层协议。
2. 初始化块(INIT chunk)
cpp 复制代码
struct sctp_init_chunk {
    struct sctp_chunkhdr common;  /* 通用块头部 */
    __be32 init_tag;              /* 初始化标签 */
    __be32 a_rwnd;                /* 初始接收窗口大小 */
    __be16 outbound_streams;      /* 出站流的数量 */
    __be16 inbound_streams;       /* 入站流的数量 */
    __be32 initial_tsn;           /* 初始传输序列号 */
    /* 可能还会有一些可选的参数 */
};
  • init_tag:用于标识本次连接初始化的唯一标签。
  • a_rwnd:发送方初始的接收窗口大小,用于流量控制。
  • outbound_streamsinbound_streams:分别表示发送方和接收方支持的流的数量。
  • initial_tsn:初始传输序列号,用于后续数据传输的顺序编号。

也就是说,当sk_buff结构体被链路层和网络层解析之后到传输层需要被SCTP协议解析,首先解析sctp头部sctphdr,然后后面跟着若干个块,有通用块头部和其他类型的块。因此先解析通用块头sctp_chunkhdr中的type字段得知后续块的具体类型,比如数据块sctp_data_chunk或者初始化块sctp_init_chunk等。

3.SCTP关联

SCTP关联而不是连接,连接指的是两个ip地址之间的通信,而关联指的是两个端点之间的通信,端点可能有多个ip地址,内核源码将SCTP关联由结构sctp_association表示如下:

1. sctp_association 结构体的作用

在 Linux 内核中,sctp_association 结构体代表一个 SCTP(流控制传输协议)关联。它是 SCTP 协议栈的核心数据结构,用于管理两个端点之间的逻辑连接状态、配置参数和传输行为。其主要功能包括:

  • 状态管理:跟踪关联的建立、数据传输、关闭等生命周期状态。
  • 多宿支持:维护本地和对端的多 IP 地址,实现路径冗余和切换。
  • 流与块管理:支持多流传输和 SCTP 块(如 DATA、INIT、SACK 等)的处理。
  • 定时器与重传:管理心跳检测、超时重传等协议机制。
  • 统计与调试:记录关联的统计信息,辅助性能分析和故障排查。

2. 重要字段讲解

由于该结构体非常复杂这里只讲解重要字段:

cpp 复制代码
struct sctp_association {
    struct sctp_ep_common base;          // 关联的基础信息
    struct list_head asocs;              // 同一端点的关联链表
    sctp_assoc_t assoc_id;               // 关联 ID
    struct sctp_endpoint *ep;            // 所属的端点
    struct sctp_cookie c;                // 用于关联建立的 cookie
    struct {
        struct list_head transport_addr_list; // 对端传输地址列表
        __u32 rwnd;                       // 对端通告的接收窗口
        __u16 transport_count;            // 对端地址数量
        __u16 port;                       // 对端端口
        struct sctp_transport *primary_path; // 主传输路径
        struct sctp_transport *active_path;  // 当前活动路径
        // ...其他 peer 相关字段...
    } peer;
    enum sctp_state state;               // 关联状态(如 ESTABLISHED、SHUTDOWN)
    struct sctp_tsnmap tsn_map;          // TSN 序列号管理
    struct sctp_bind_addr *bind_addrs;   // 本地绑定的地址链表(在 sctp_socket 中)
    // ...其他字段...
};
2.1 基础信息与关联标识
  • struct sctp_ep_common base
    包含关联的基础信息,如 struct sock *sk(通过 base.sk 访问),指向关联的 sctp_socket 结构体。
  • sctp_assoc_t assoc_id
    全局唯一的关联标识符,用于区分不同的 SCTP 连接。
2.2 对端信息管理
  • peer.transport_addr_list
    对端的多宿地址列表,每个元素为 struct sctp_transport,包含 IP 地址和端口。
  • peer.primary_path
    当前主传输路径,用于数据传输的首选对端地址。
  • peer.port
    对端的传输层端口号。
2.3 关联状态与定时器
  • enum sctp_state state
    关联的当前状态,如 SCTP_CLOSED(关闭)、SCTP_ESTABLISHED(已建立)等。
  • struct timer_list timers[SCTP_NUM_TIMEOUT_TYPES]
    定时器数组,用于管理心跳检测、重传超时等协议事件。
2.4 多宿与路径管理
  • struct sctp_bind_addr *bind_addrs
    本地绑定的多宿地址链表(需通过 sctp_socket 访问)。
  • peer.active_path
    当前用于数据传输的活动路径,支持动态切换。
2.5 序列号与窗口管理
  • struct sctp_tsnmap tsn_map
    管理数据序列号(TSN)的接收和确认,支持乱序处理。
  • peer.rwnd
    对端通告的接收窗口大小,用于流量控制。
2.6 统计与调试
  • struct sctp_priv_assoc_stats stats
    存储关联的统计信息,如发送 / 接收字节数、重传次数等。

4.建立SCTP关联(这是一个四次握手的流程)

a. 端点(A)向要与通信的端口(Z)发送INIT块。INIT块的发起标签字符包含本地生成的标签,还包含一个值为0的验证标签;

b. 发送INIT块后,关联进入SCTP_STATE_COOKIE_WAIT状态;

c. 作为应答,端点Z会向端点A发送一个INIT-ACK块。这个块的发起标签字段包含一个本地生成的标签,同时,它还会将远程端点的发起标签用作验证标签。端点Z还需要生成一个状态cookie,并且通过INIT-ACK应答发送它;

d. 端点A收到INIT-ACK块后,该会退出SCTP_STATE_COOKIE_WAIT状态;从此开始,在传输所有数据中,A都会将远程端点的发起标签用作验证标签,接下来,A将通过一个COOKIE ECHO块发送状态cookie,并进入SCTP_STATE_COOKIE_ECHOED状态;

e. 收到COOKIE ECHO块后,端点Z将创建一个传输控制块(Transmission control block,TCB),TCB是包含SCTP连接一端的连接信息的数据结构,接下来,Z将切换到状态SCTP_STATE_ESTABLISHED,并使用COOKIE ACK块进行应答。到此为止,在Z端点处就可以建立关联,该关联将使用保存的标签;

f. 收到COOKIE ACK后,A端点从状态SCTP_STATE_COOKIE_ECHOED切换到SCTP_STATE_ESTABLISHED状态。

SCTP 的四次握手建立关联,就像两个人确认合作前的 "暗号对接",确保双方准备就绪且身份可靠:

  1. INIT 块发送(客户端发起)

    客户端(端点 A)先发送 INIT 块,就像敲门说:"我准备好合作了,这是我的'临时暗号'(本地标签),验证标签先设为 0"。此时客户端进入 SCTP_STATE_COOKIE_WAIT 状态,开始等待服务端回应。

  2. INIT-ACK 响应(服务端处理)

    服务端(端点 Z)收到 INIT 后,用客户端的标签作为验证依据,同时生成自己的 "状态饼干"(状态 cookie,包含连接关键信息)。然后回复 INIT-ACK,里面既有服务端的标签,也带着这个 "状态饼干",相当于回应:"收到你的暗号,这是确认饼干,拿好!"

  3. COOKIE ECHO 验证(客户端二次确认)

    客户端收到 INIT-ACK 后,退出等待状态,带着服务端给的 "状态饼干" 发送 COOKIE ECHO,就像提交作业:"这是你给的饼干,现在验证!" 同时进入 SCTP_STATE_COOKIE_ECHOED 状态。

  4. COOKIE ACK 确认(服务端完成建立)

    服务端收到 COOKIE ECHO 后,创建传输控制块(TCB,存储连接信息),切换到已建立状态,并用 COOKIE ACK 回应:"饼干验证通过,合作正式开始!" 客户端收到 COOKIE ACK 后,也切换到已建立状态,双方正式建立关联。

5.如何由sctp_association获取绑定的多个地址

一、关键结构体关系

1.本端地址相关字

cpp 复制代码
struct sctp_association {
    struct sctp_ep_common base;  // 包含绑定地址信息
};

struct sctp_ep_common {
    struct sctp_bind_addr bind_addr;  // 直接存储本地绑定地址
};

struct sctp_bind_addr {
    __u16 port;                    // 共享端口号
    struct list_head address_list; // 地址链表
};
2. 对端地址相关字
cpp 复制代码
struct sctp_association {  
    struct {  
        struct list_head transport_addr_list;  // 对端传输地址链表  
        // ...其他字段...  
    } peer;  
    // ...其他字段...  
};  
struct sctp_transport {  
    union sctp_addr ipaddr;  // 存储对端 IP 地址和端口  
    // ...其他字段...  
};  

二、获取本地多个 IP 地址和端口的步骤

1. 访问 sctp_bind_addr
cpp 复制代码
struct sctp_association *assoc = ...;
struct sctp_bind_addr *bind_addr = &assoc->base.bind_addr;
2. 遍历地址链表
cpp 复制代码
#include <linux/inet.h>
#include <linux/sctp.h>

void get_local_addrs(struct sctp_association *assoc) {
    struct sctp_bind_addr *bind_addr = &assoc->base.bind_addr;
    struct sctp_address *addr_node;
    char ip_str[INET6_ADDRSTRLEN];

    list_for_each_entry(addr_node, &bind_addr->address_list, list) {
        struct sockaddr *sa = (struct sockaddr *)&addr_node->addr;

        switch (sa->sa_family) {
            case AF_INET: {
                struct sockaddr_in *sin = (struct sockaddr_in *)sa;
                inet_ntop(AF_INET, &sin->sin_addr, ip_str, INET_ADDRSTRLEN);
                printk(KERN_INFO "Local IPv4: %s:%d\n", 
                       ip_str, ntohs(bind_addr->port));
                break;
            }
            case AF_INET6: {
                struct sockaddr_in6 *sin6 = (struct sockaddr_in6 *)sa;
                inet_ntop(AF_INET6, &sin6->sin6_addr, ip_str, INET6_ADDRSTRLEN);
                printk(KERN_INFO "Local IPv6: %s:%d\n", 
                       ip_str, ntohs(bind_addr->port));
                break;
            }
            default:
                printk(KERN_WARNING "Unsupported address family: %d\n", sa->sa_family);
        }
    }
}

三、获取对端多个 IP 地址和端口的步骤

1. 遍历对端地址链表
cpp 复制代码
void get_peer_addrs(struct sctp_association *assoc) {  
    struct sctp_transport *transport;  
    char ip_str[INET6_ADDRSTRLEN];  

    list_for_each_entry(transport, &assoc->peer.transport_addr_list, transports) {  
        if (transport->ipaddr.sa.sa_family == AF_INET) {  
            struct sockaddr_in *sin = &transport->ipaddr.sin;  
            inet_ntop(AF_INET, &sin->sin_addr, ip_str, INET_ADDRSTRLEN);  
            printk(KERN_INFO "Peer IPv4: %s:%d\n", ip_str, ntohs(sin->sin_port));  
        } else if (transport->ipaddr.sa.sa_family == AF_INET6) {  
            struct sockaddr_in6 *sin6 = &transport->ipaddr.sin6;  
            inet_ntop(AF_INET6, &sin6->sin6_addr, ip_str, INET6_ADDRSTRLEN);  
            printk(KERN_INFO "Peer IPv6: %s:%d\n", ip_str, ntohs(sin6->sin6_port));  
        }  
    }  
}  

6.接收SCTP数据包

负责接收SCTP数据包主要由sctp_rcv()处理。

重要流程讲解:

1. 数据包基本检查与统计

cpp 复制代码
if (skb->pkt_type != PACKET_HOST)
    goto discard_it;

__SCTP_INC_STATS(net, SCTP_MIB_INSCTPPACKS);

if (skb->len < sizeof(struct sctphdr) + sizeof(struct sctp_chunkhdr) +
               skb_transport_offset(skb))
    goto discard_it;
  • 功能 :这部分代码是处理数据包的第一步。首先检查数据包类型是否为 PACKET_HOST,即是否是发给本机的数据包,如果不是则直接丢弃。接着对接收的 SCTP 数据包数量进行统计,方便后续性能分析。最后检查数据包长度是否足够包含 SCTP 头部和一个数据块头部,若不足则丢弃,避免无效处理。

2. 校验和检查

cpp 复制代码
skb->csum_valid = 0; 
if (skb_csum_unnecessary(skb))
    __skb_decr_checksum_unnecessary(skb);
else if (!sctp_checksum_disable &&
         !is_gso &&
         sctp_rcv_checksum(net, skb) < 0)
    goto discard_it;
skb->csum_valid = 1;
  • 功能 :校验和检查是确保数据包在传输过程中没有被损坏的重要步骤。先将校验和有效性标志置为 0,然后判断是否不需要校验和,如果需要则调用 sctp_rcv_checksum 函数进行校验。若校验失败且未禁用校验功能,则丢弃该数据包,保证后续处理的数据包都是完整的。

3. 关联和端点查找

cpp 复制代码
asoc = __sctp_rcv_lookup(net, skb, &src, &dest, &transport);

if (!asoc)
    ep = __sctp_rcv_lookup_endpoint(net, skb, &dest, &src);

rcvr = asoc ? &asoc->base : &ep->base;
sk = rcvr->sk;
  • 功能 :这部分代码的核心是找到与当前数据包匹配的 SCTP 关联(sctp_association)或端点(sctp_endpoint)。先尝试查找关联,如果没找到则查找端点。找到后获取对应的公共输入处理子结构和套接字,后续的数据包处理将基于此进行。

4. 处理 "Out of the blue" 数据包

cpp 复制代码
if (!asoc) {
    if (sctp_rcv_ootb(skb)) {
        __SCTP_INC_STATS(net, SCTP_MIB_OUTOFBLUES);
        goto discard_release;
    }
}
  • 功能 :当未找到与数据包匹配的关联时,认为这是一个 "Out of the blue"(意外)数据包。调用 sctp_rcv_ootb 函数对其进行处理,如果处理失败则丢弃该数据包,并对 "Out of the blue" 数据包的统计值进行增加,方便后续分析异常情况。

5. 加入接收队列

cpp 复制代码
if (sock_owned_by_user(sk) || !sctp_newsk_ready(sk)) {
    if (sctp_add_backlog(sk, skb)) {
        bh_unlock_sock(sk);
        sctp_chunk_free(chunk);
        skb = NULL; 
        goto discard_release;
    }
    __SCTP_INC_STATS(net, SCTP_MIB_IN_PKT_BACKLOG);
} else {
    __SCTP_INC_STATS(net, SCTP_MIB_IN_PKT_SOFTIRQ);
    sctp_inq_push(&chunk->rcvr->inqueue, chunk);
}
  • 功能:根据套接字的状态决定将数据包加入积压队列还是接收队列。如果套接字被用户占用或者未准备好,尝试将数据包加入积压队列,若加入失败则丢弃数据包;若套接字状态正常,则将数据块加入接收队列,等待后续处理。同时对相应的统计值进行增加,用于性能监控。

7.发送SCTP数据包

发送SCTP数据包由sctp_sendmsg()处理。

重要流程讲解:

1. 用户调用 sctp_sendmsg

函数入口

用户调用 sctp_sendmsg(),传入套接字(struct sock *sk)、用户数据(struct msghdr *msg)和长度(size_t msg_len)。

主要任务

  • 解析用户参数(目标地址、流 ID、标志位等)。

  • 查找或创建 SCTP 关联(struct sctp_association)。

  • 将数据分派到关联的发送队列。


2. 参数解析与关联管理

关键调用链
cpp 复制代码
sctp_sendmsg()
  → sctp_sendmsg_parse()          // 解析用户参数,填充 sctp_sndrcvinfo
  → sctp_sendmsg_get_daddr()      // 获取目标地址(多宿主场景)
  → sctp_endpoint_lookup_assoc()  // 查找现有 SCTP 关联
  → sctp_sendmsg_new_asoc()       // 若关联不存在,创建新关联(触发 INIT 块)

详细流程

  1. 参数解析

    • msghdr 提取流 ID(sinfo_stream)、标志位(如 SCTP_SENDALL)、目标地址等。

    • 若启用认证(SCTP-AUTH),提取密钥信息。

  2. 关联查找

    • 根据目标地址查找现有关联(sctp_association)。

    • 若不存在,通过四次握手创建新关联,初始化状态为 SCTP_STATE_COOKIE_WAIT


3. 数据分片与 DATA Chunk 构造

关键调用链
cpp 复制代码
sctp_sendmsg_to_asoc()
  → sctp_datamsg_from_user()       // 将用户数据转换为 sctp_datamsg(含多个 DATA Chunk)
    → asoc->stream.si->make_datafrag() // 实际调用 sctp_make_datafrag_empty()
    → sctp_user_addto_chunk()      // 填充用户数据到 Chunk 的 skb

详细流程

  1. 分片处理

    • 根据路径 MTU(asoc->frag_point)计算最大分片长度 max_data

    • 若用户数据长度超过 max_data,分割为多个分片。

  2. 构造 DATA Chunk

    • 块头设置

      • type = SCTP_CID_DATA

      • flags 标记分片位置(FIRST_FRAG/MIDDLE_FRAG/LAST_FRAG)。

      • stream_idssn(流序列号)从 sctp_sndrcvinfo 获取。

    • 数据填充

      • 通过 sctp_user_addto_chunk 将用户数据拷贝到 chunk->skb
    • 序列号分配

      • TSN(传输序列号)由关联的 next_tsn 递增生成,确保全局有序。

示例数据结构

cpp 复制代码
struct sctp_datamsg {
    struct list_head chunks;    // 链表管理所有 DATA Chunk
    ktime_t expires_at;         // 消息过期时间(PR-SCTP)
    refcount_t refcnt;
};

struct sctp_chunk {
    struct sk_buff *skb;        // 包含 DATA Chunk 头和用户数据
    struct sctp_association *asoc;
    __u32 tsn;                  // 传输序列号
    __u16 stream_id;            // 流 ID
    __u16 ssn;                  // 流序列号
    struct list_head frag_list; // 分片链表
};

4. SCTP 头部封装与 sk_buff 构造

关键调用链
cpp 复制代码
sctp_primitive_SEND()
  → sctp_outq_tail()            // 将 Chunk 加入发送队列
  → sctp_do_sm()               // 触发状态机处理
    → sctp_packet_transmit()    // 构造 SCTP 头部并发送
      → sctp_packet_config()    // 初始化 SCTP 头部
      → sctp_packet_append_chunk() // 添加 DATA Chunk
      → ip_queue_xmit()         // 传递到 IP 层

详细流程

  1. 初始化 SCTP 头部

    • 源/目的端口:从关联的端点(sctp_endpoint)获取。

    • 验证标签(Verification Tag):使用关联建立时协商的标签。

    • 校验和:预留空间,后续计算填充。

  2. 封装 DATA Chunk

    • sctp_chunkskb 添加到 sctp_packet 的块列表。

    • 若需捆绑控制块(如 SACK、HEARTBEAT),在此阶段添加。

  3. 构造 sk_buff

    • 分配 sk_buff,添加 SCTP 头部和所有 DATA Chunk。

    • 计算 CRC32c 校验和,填充到 SCTP 头部。

关键代码片段

cpp 复制代码
// net/sctp/output.c
int sctp_packet_transmit(struct sctp_packet *packet) {
    struct sk_buff *skb = packet->skb;
    struct sctphdr *sh = skb_transport_header(skb);

    // 设置 SCTP 头部字段
    sh->source = packet->source_port;
    sh->dest = packet->destination_port;
    sh->vtag = htonl(packet->vtag);
    sh->checksum = 0;

    // 计算校验和
    sh->checksum = sctp_compute_cksum(skb, skb_headlen(skb));

    // 传递到 IP 层
    ip_queue_xmit(skb);
}

5. 发送队列管理与网络层传递

关键机制
  1. 发送队列(sctp_outq

    • 所有未确认的 DATA Chunk 存储在 asoc->outqueue 中。

    • 队列管理包括重传(Retransmit Queue)和未确认数据(Outstanding Queue)。

  2. 拥塞控制

    • 根据当前拥塞窗口(cwnd)和 RTT 决定发送速率。

    • 若网络拥塞,触发快重传(Fast Retransmit)或超时重传。

  3. 多宿主路径切换

    • 若主路径(peer.primary_path)不可达,切换到备用路径。

发送触发逻辑

  • 立即发送:若拥塞窗口允许,直接调用 sctp_packet_transmit

  • 延迟发送:将数据暂存队列,等待定时器或 ACK 触发发送。


6. 完整流程总结

  1. 用户空间到内核

    • 用户调用 sctp_sendmsg,数据通过 copy_from_user 进入内核。
  2. 参数解析与关联

    • 解析目标地址、流 ID,查找或创建关联。
  3. 数据分片与块构造

    • 分割用户数据,生成多个 DATA Chunk,分配 TSN/SSN。
  4. 封装与发送队列

    • 封装 SCTP 头部,构造 sk_buff,加入发送队列。
  5. 网络层传递

    • 通过 IP 层发送,处理拥塞控制和路径切换。

7. 关键源码文件

  • net/sctp/socket.csctp_sendmsgsctp_sendmsg_to_asoc

  • net/sctp/ulpevent.csctp_datamsg_from_user

  • net/sctp/sm_make_chunk.csctp_make_datafrag_empty

  • net/sctp/output.csctp_packet_transmit

  • net/sctp/outqueue.csctp_outq_tail、拥塞控制逻辑。


8.流程图

8. SCTP心跳

心跳机制通过交换SCTP数据包HEARTABLE和HEARTABLE-ACK来检测路径的连通性。到达无返回心跳阈值后,它将宣布该ip地址失效,默认每30秒将发送一个HEARTABLE块,用来对空闲的目标传输地址进行监视。如果要配置这个时间间隔,可以设置虚拟文件系统/proc/sys/net/sctp/hb_interval,默认值30000(30秒)。

9.SCTP与TCP的区别

  • 连接特性:SCTP 支持多宿主连接 ,即连接的两端点可声明多个 IP 地址,当当前连接失效时能切换到其他地址且无需重新建立连接;而 TCP 一般是单地址连接。
  • 数据流:SCTP 基于消息流,一个连接可支持多个流,发送和应答数据的最小单位是消息包;TCP 基于字节流,一个连接只能支持一个流。
  • 安全性:SCTP 采用四次握手机制,能有效防止类似于 SYN Flooding 的拒绝服务攻击,增加了防止恶意攻击的措施;TCP 采用三次握手机制,在防范此类攻击方面相对较弱 。
  • 数据传输顺序:SCTP 中有序传输是可选项,还支持无序数据传输;TCP 则要求数据按序传输。
  • 消息边界:SCTP 能保留消息边界;TCP 不保留消息边界,需要在应用层支持消息框架来区分不同消息。
  • 连接关闭:SCTP 不支持 "半关闭" 状态;TCP 在 "关联关闭" 过程中能保持连接开启,并从对端接收新数据。
  • 校验和:SCTP 没有用于校验和的伪标头;TCP 有校验和伪标头。
对比项 SCTP TCP
连接类型 多宿主连接 单地址连接
数据流支持 支持多流 仅支持单流
传输类型 面向消息 面向字节
握手方式 四次握手 三次握手
安全性 较高,能防范部分攻击 相对较低
数据传输顺序 有序可选择,支持无序传输 有序传输
消息边界 保留 不保留
半关闭状态 不允许 允许
校验和伪标头

虽然 SCTP 在某些方面比 TCP 更具优势,但它不能完全代替 TCP,主要有以下原因:

  • 应用场景的适配性:TCP 经过多年发展,在众多传统应用场景中已经非常成熟且适配,像网页浏览、电子邮件、文件传输等,这些场景对数据可靠性和按序传输要求高,TCP 能很好满足。而 SCTP 主要设计用于传输公共交换电话网络(PSTN)信令等特定场景,在传统场景的生态完善度和适配性不如 TCP。
  • 兼容性和部署难度:TCP 是网络协议栈中广泛实现和支持的协议,几乎所有的操作系统、网络设备都原生支持,其部署和集成成本低。SCTP 作为相对新的协议,在一些老旧系统和设备上可能缺乏支持,要大规模部署替换 TCP,需要对大量的现有基础设施和软件进行更新改造,成本和难度都很高。
  • 开发和维护成本:开发人员对 TCP 的原理、编程模型和调试方法都非常熟悉,围绕 TCP 已经有大量的开发工具、类库和框架。使用 TCP 进行应用开发和维护的成本较低。而使用 SCTP,开发人员需要重新学习相关知识和技能,相关的开发资源也相对较少,这会增加开发和维护成本。
  • 生态系统和标准的成熟度:TCP 已经有了非常完善的生态系统,相关的标准和规范也被广泛接受和遵循。SCTP 虽然也有相应的标准,但在生态系统的完善程度和行业认可度上,还无法与 TCP 相比,这也限制了它全面替代 TCP 的可能性。
相关推荐
用户02731754117916 分钟前
dig 命令深入学习
linux·后端
渗透测试老鸟-九青27 分钟前
关于缓存欺骗的小总结
网络·经验分享·安全·web安全·缓存·面试
v维焓1 小时前
网络编程之客户端聊天(服务器加客户端共三种方式)
运维·服务器·网络
栀子花不不不想开2 小时前
OSPF与RIP联动实验
网络·智能路由器·信息与通信
数据链路摸索者2 小时前
ospf动态路由
前端·网络·网络安全·智能路由器
nihuhui6662 小时前
Vlan初级实验
服务器·网络·网络协议
IT小饕餮2 小时前
Linux 安装apache服务
linux·运维·apache
小狗爱吃黄桃罐头2 小时前
正点原子[第三期]Arm(iMX6U)Linux移植学习笔记-6.2uboot启动流程-lowlevel_init,s_init,_main函数执行
linux·arm开发·学习
FreeBuf_3 小时前
Apache Tomcat漏洞公开发布仅30小时后即遭利用
网络·安全·tomcat·apache
源远流长jerry3 小时前
Linux内核Netfilter框架分析
linux·网络