SCTP是传送协议,一种可靠的传输协议,提供类似于 TCP 提供的服务。一方面SCTP要完成对等层偶联的管理,另一方面要完成应用数据的承载。我们可以理解为SCTP对等层之间的控制协议能力和用于数据承载协议的能力。
SCTP协议
虽然SCTP协议包含承载能力和控制能力,SCTP协议的数据报文仍然采用统一的结构。我们来看看SCTP 通用头格式 和SCTP数据单元。
Wireshark抓包SCTP
SCTP数据报文结构由统一的SCTP公共消息头和后继的一个或者多个SCTP数据单元(SCTP Chunk)组成。
对于Wireshark抓取的SCTP数据报文,公共消息头包含以下四个字段:
- 源端口(Source Port):用于描述目标端点的端口号。 目标端口(Destination Port):用于描述目标端点的端口号。
- 验证字(Verification Tag):用于标识数据的发送者。
- 校验和(Checksum):用于校验消息内容是否在传送中有损坏。校验和的取值为Checksum字段取值为0时的数据报文执行CRC32c计算的结果。
每一个数据单元(Chunk)代表一条SCTP的控制协议指令,或者SCTP承载的用户数据的片段。数据单元由统一的数据单元消息头和用户数据组成,数据单元消息头有以下三个字段:
- 数据单元类型(Chunk Type):数据单元类型标识了该数据单元所携带的用户数据类型,SCTP将按照数据单元类型构造和解析数据单元携带的用户数据。
- 数据单元标识(Chunk Flags):取值与数据单元类型相关。
- 数据单元的长度(Chunk Length):数据单元长度是包括类型、标识的数据单元的总长度。
SCTP数据单元类型
对于数据单元类型,可以分为两个类型,数据传送消息和管理类消息。
- 数据传送类型
数据单元类型取值(0): Payload Data (DATA)
用于SCTP用户数据的承载,承载数据的发送请求
数据单元类型取值(3):Selective Acknowledgement (SACK)
用于对DATA请求发送的数据单元进行请求,以表明是否正确发送到目标端
- 管理类消息
数据单元类型取值(1): Initiation (INIT)
用于发起SCTP建立请求
数据单元类型取值(2):Initiation Acknowledgement (INIT ACK)
SCTP偶联建立应答
数据单元类型取值(4):Heartbeat Request (HEARTBEAT)
心跳检查请求
数据单元类型取值(5):Heartbeat Acknowledgement (HEARTBEAT ACK)
心跳检查应答
数据单元类型取值(6):Abort (ABORT)
立即关闭偶联通知
数据单元类型取值(7):Shutdown (SHUTDOWN)
正常关闭偶联通知
数据单元类型取值(8): Shutdown Acknowledgement (SHUTDOWN ACK)
关闭偶联应答
数据单元类型取值(9):Operation Error (ERROR)
操作错误通知
数据单元类型取值(10):State Cookie (COOKIE ECHO)
Cookie验证请求
数据单元类型取值(11):Cookie Acknowledgement (COOKIE ACK)
Cookie验证应答
数据单元类型取值(12):Reserved for Explicit Congestion Notification Echo
(ECNE)
为明确拥塞通知响应(ECNE)预留
数据单元类型取值(13):Reserved for Congestion Window Reduced (CWR)
为降低拥塞窗口(CWR)预留
数据单元类型取值(14): Shutdown Complete (SHUTDOWN COMPLETE)
偶联关闭完成
DATA数据单元结构定义
DATA数据单元定义如下图所示:
Wireshark抓包分析:
数据单元类型取值固定为0,对于U、B、E可以简单这样理解。
- U如果置位
则代表这个数据单元是一个要求顺序的数据单元。
- B如果置位
则代表这个要求顺序的数据单元是第一个数据单元。
- E如果置位
则代表这个要求顺序的单元是最后一个单元。
- 传送序号(TSN)
是一个32bit序列号,是该数据单元在整个SCTP传送过程中的唯一标识,接收端将基于TSN数据单元进行证实。
- 流标识(SI:Stream Identifier )
是数据单元所使用的流编号
- 流内序号(Stream Sequence Number)
是数据单元所使用的流内序号,接收端将基于SSN判断所接收到的数据报文顺序,完成数据单元所使用的流编号。
- 净荷协议标识(Payload Protocol Identifier)
是数据单元所携带的净荷内容的类型标识,由于SCTP承载的用户数据是信令,因此这个净荷协议标识指的是所承载的协议。
净荷内容是变长的,是承载的应用协议具体内容。
INIT数据单元结构定义
偶联建立请求的数据单元结构定义如下:
这里需要注意的是,只有INIT数据单元的验证字(Verfication Tag)可以取值为0,其他请求都需要携带正确的验证字,否则接收端可能会拒绝处理。
Wireshark抓包分析:
INIT请求数据单元类型取值固定1
- 初始验证字(Initiate Tag)
用于设置向本端点发送的数据报文所要携带的验证字
- 接收窗口(Advertised Receiver Window Credit (a_rwnd))
用于设置本端发送数据报文的时候建议的数据发送窗口大小。
- 发送流数量(Number of Outbound Streams)
用于设置发送的流数量
- 接收流数量(Number of Inbound Streams)
用于设置接收的流数量
- 初始TSN(Initial TSN)
用于设置接收到的第一条DATA消息的TSN取值。
INIT可以携带多种可选参数,每个可选参数都由可选参数类型和可选参数长度组成的可选参数头和可选参数内容组成。
#define SCTP_DATA_CHUNK_ID 0
#define SCTP_INIT_CHUNK_ID 1
#define SCTP_INIT_ACK_CHUNK_ID 2
#define SCTP_SACK_CHUNK_ID 3
#define SCTP_HEARTBEAT_CHUNK_ID 4
#define SCTP_HEARTBEAT_ACK_CHUNK_ID 5
#define SCTP_ABORT_CHUNK_ID 6
#define SCTP_SHUTDOWN_CHUNK_ID 7
#define SCTP_SHUTDOWN_ACK_CHUNK_ID 8
#define SCTP_ERROR_CHUNK_ID 9
#define SCTP_COOKIE_ECHO_CHUNK_ID 10
#define SCTP_COOKIE_ACK_CHUNK_ID 11
#define SCTP_ECNE_CHUNK_ID 12
#define SCTP_CWR_CHUNK_ID 13
#define SCTP_SHUTDOWN_COMPLETE_CHUNK_ID 14
#define SCTP_AUTH_CHUNK_ID 15
#define SCTP_NR_SACK_CHUNK_ID 16
#define SCTP_I_DATA_CHUNK_ID 0x40
#define SCTP_ASCONF_ACK_CHUNK_ID 0x80
#define SCTP_PKTDROP_CHUNK_ID 0x81
#define SCTP_RE_CONFIG_CHUNK_ID 0x82
#define SCTP_PAD_CHUNK_ID 0x84
#define SCTP_FORWARD_TSN_CHUNK_ID 0xC0
#define SCTP_ASCONF_CHUNK_ID 0xC1
#define SCTP_I_FORWARD_TSN_CHUNK_ID 0xC2
#define SCTP_IETF_EXT 0xFF
#define HEARTBEAT_INFO_PARAMETER_ID 0x0001
#define IPV4ADDRESS_PARAMETER_ID 0x0005
#define IPV6ADDRESS_PARAMETER_ID 0x0006
#define STATE_COOKIE_PARAMETER_ID 0x0007
#define UNREC_PARA_PARAMETER_ID 0x0008
#define COOKIE_PRESERVATIVE_PARAMETER_ID 0x0009
#define HOSTNAME_ADDRESS_PARAMETER_ID 0x000b
#define SUPPORTED_ADDRESS_TYPES_PARAMETER_ID 0x000c
#define OUTGOING_SSN_RESET_REQUEST_PARAMETER_ID 0x000d
#define INCOMING_SSN_RESET_REQUEST_PARAMETER_ID 0x000e
#define SSN_TSN_RESET_REQUEST_PARAMETER_ID 0x000f
#define RE_CONFIGURATION_RESPONSE_PARAMETER_ID 0x0010
#define ADD_OUTGOING_STREAMS_REQUEST_PARAMETER_ID 0x0011
#define ADD_INCOMING_STREAMS_REQUEST_PARAMETER_ID 0x0012
#define ECN_PARAMETER_ID 0x8000
#define NONCE_SUPPORTED_PARAMETER_ID 0x8001
#define RANDOM_PARAMETER_ID 0x8002
#define CHUNKS_PARAMETER_ID 0x8003
#define HMAC_ALGO_PARAMETER_ID 0x8004
#define SUPPORTED_EXTENSIONS_PARAMETER_ID 0x8008
#define FORWARD_TSN_SUPPORTED_PARAMETER_ID 0xC000
#define ADD_IP_ADDRESS_PARAMETER_ID 0xC001
#define DEL_IP_ADDRESS_PARAMETER_ID 0xC002
#define ERROR_CAUSE_INDICATION_PARAMETER_ID 0xC003
#define SET_PRIMARY_ADDRESS_PARAMETER_ID 0xC004
#define SUCCESS_REPORT_PARAMETER_ID 0xC005
#define ADAP_LAYER_INDICATION_PARAMETER_ID 0xC006
struct chunk
{
u_char type;
u_char flags;
u_short length;
};
struct my_sctphdr
{
/*
The data types/sizes we need to use are: unsigned char - 1 byte (8 bits),
unsigned short int - 2 bytes (16 bits) and unsigned int - 4 bytes (32 bits)
*/
u_short sport;
u_short dport;
unsigned int veriftag;
unsigned int checksum;
// chunk follows
struct chunk chnk;
};
void dissect_init_ack_chunk(u_char *sctp_data,int offset,int chunk_length)
{
int adv_rec_window_credit = 0;
uint16_t number_of_outbound_streams = 0;
uint16_t number_of_inbound_streams = 0;
int length = 0;
uint16_t type = 0;
in_addr ip;
if (chunk_length < INIT_CHUNK_FIXED_PARAMTERS_LENGTH)
return;
offset += INIT_CHUNK_INITIATE_TAG_LENGTH;
adv_rec_window_credit = ntohl(*(uint32_t*)(sctp_data + offset));
printf("Advertised receiver window credit (a_rwnd): %d\n",adv_rec_window_credit);
offset += INIT_CHUNK_ADV_REC_WINDOW_CREDIT_LENGTH;
number_of_outbound_streams = ntohs(*(uint16_t*)(sctp_data + offset));
printf("Outbound streams: %d\n",number_of_outbound_streams);
offset += INIT_CHUNK_NUMBER_OF_OUTBOUND_STREAMS_LENGTH;
number_of_inbound_streams = ntohs(*(uint16_t*)(sctp_data + offset));
printf("inbound streams: %d\n",number_of_inbound_streams);
offset += INIT_CHUNK_NUMBER_OF_INBOUND_STREAMS_LENGTH;
offset += INIT_CHUNK_INITIAL_TSN_LENGTH;
while(offset < chunk_length)
{
type = ntohs(*(uint16_t*)(sctp_data + offset));
//printf("type: %x\n",type);
offset += PARAMETER_TYPE_LENGTH;
length = ntohs(*(uint16_t*)(sctp_data + offset));
if (type == SUPPORTED_ADDRESS_TYPES_PARAMETER_ID)
offset += 8;
switch(type)
{
case IPV4ADDRESS_PARAMETER_ID:
printf("----ipv4------\n");
//printf("%x %x %x %x \n",sctp_data[offset],sctp_data[offset + 1],sctp_data[offset + 2],sctp_data[offset + 3]);
offset += 2;
memcpy(&ip.s_addr, sctp_data + offset, sizeof(ip.s_addr));
printf("IPv4 address parameter: %s\n", inet_ntoa(ip));
break;
default:
break;
}
}
offset += length;
}
void dissect_sctp(u_char *sctp_data,int offset)
{
struct my_sctphdr sctp_packet;
uint16_t total_length = 0;
printf("sctp_data 0x%.2X,0x%.2X,0x%.2X,0x%.2X,0x%.2X\n",sctp_data[0],sctp_data[1],sctp_data[2],sctp_data[3],sctp_data[4]);
sctp_packet.sport = ntohs(*(uint16_t*)(sctp_data + offset));
printf("sport: %d\n",sctp_packet.sport);
offset += SOURCE_PORT_LENGTH;
sctp_packet.dport = ntohs(*(uint16_t*)(sctp_data + offset));
printf("dport: %d\n",sctp_packet.dport);
offset += DESTINATION_PORT_LENGTH;
sctp_packet.veriftag = ntohl(*(uint32_t*)(sctp_data + offset));
printf("vtag: 0x%x\n",sctp_packet.veriftag);
offset += VERIFICATION_TAG_LENGTH;
sctp_packet.checksum = ntohl(*(uint32_t*)(sctp_data + offset));
printf("vtag: 0x%x\n",sctp_packet.checksum);
offset += CHECKSUM_LENGTH;
if (offset < 12) /* 不能小于SCTP头长度 */
return;
sctp_packet.chnk.type = sctp_data[offset];
printf("type: %d\n",sctp_packet.chnk.type);
offset += CHUNK_TYPE_LENGTH;
sctp_packet.chnk.flags = sctp_data[offset];
offset += CHUNK_FLAGS_LENGTH;
/* 提取块长度并计算填充字节数 */
sctp_packet.chnk.length = ntohs(*(uint16_t*)(sctp_data + offset));
printf("length: %d\n",sctp_packet.chnk.length);
//total_length = ADD_PADDING(sctp_packet.chnk.length);
//printf("total_length: %d\n",total_length);
offset += CHUNK_LENGTH_LENGTH;
switch(sctp_packet.chnk.type)
{
case SCTP_INIT_CHUNK_ID:
printf("===SCTP_INIT_CHUNK_ID===\n");
dissect_init_ack_chunk(sctp_data,offset,sctp_packet.chnk.length);
break;
default:
/* unknown chunk */
break;
}
}
运行结果
在建立SCTP偶联连接过程中,当服务接收到客户端发起的INIT请求,会建立SCTP数据结构,保存对端的端口、本地端口、对端验证字、本地验证字、发送流数量、接收流的数量等等。
数据单元的绑定
为了提高传送效率,SCTP允许多个数据单元(Chunk)绑定在一个数据分组(Pack-age)中进行传送。但是需要注意的是,数据单元必须完整地携带在数据分组中。如果某个数据单元不能完整地携带在数据中,则只能将该数据单元绑定到后续的数据分组。
SCTP数据报文并没有在公共消息中约束可以绑定SCTP数据单元的个数,也没有约束可携带的单元长度。
可绑定的SCTP数据单元个数完全由SCTP协议栈根据当前网络MTU的大小或自定义策略来确定。
什么是多穴(Multi-Homing)
一个SCTP端点可以绑定多个IP地址,以解决“点到点”连接可能造成的可用性和灵活性问题。为了支持多穴,在SCTP偶联建立的时候,SCTP端点需要使用INIT和INIT-ACK请求交换偶联双方所绑定的IP地址。
SCTP即支持IPV4也支持IPV6,不管是哪种地址,SCTP只需要知道至少一个对端的IP地址就可以了,就能正常建立连接。
如果INIT请求和INIT-ACK请求没有明确声明绑定IP地址,则默认使用发送请求的IP地址作为对端绑定的传送地址。
对于多穴,不仅解决了多点连接问题,还提高了系统可用性。
多流(Multi-Streaming)
为了支持多流,在偶联建立的时候需要协商流的数量,由于流是单向的,因此偶联建立请求将会初始化希望的发送流(Outbound Stream)和接收流(Inbound Stream)数量,初始化的时候并不会为流分配特殊的标识,在使用的时候,流标识从0开始编号。
总结
TCP协议容易受到SYN攻击,其原因是服务器在接收到客户端的TCP连接建立请求(ACK)之后,会立即分配网络资源。而在建立SCTP偶联连接过程中,当服务器接收到客户端发起的INIT请求时,不会立即分配网络资源,而是等到客户端确认。为了避免客户端随意地发送确认,SCTP采用Cookie验证方式。避免SYN攻击。
欢迎关注微信公众号【程序猿编码】,需要源代码欢迎添加本人微信号(17865354792)交流学习。
参考:RFC2960、 RFC4960