SCTP 프로토콜
Linux SCTP(Stream Control Transmission Protocol) 구현을 심층 설명합니다. 멀티스트리밍·멀티호밍 기반 Association 모델, 초기 4-way handshake, Path 관리와 failover, 메시지 단위 전송 보장, TCP/UDP 대비 설계 차이, 커널 자료구조와 타이머 동작, 통신 장애·재전송·경로 전환 이슈에 대한 운영 디버깅 포인트까지 다룹니다.
핵심 요약
- Association — TCP 연결과 유사하지만 쿠키 기반 4-way handshake
- Multi-streaming — 독립 스트림으로 HoL blocking 완화
- Multi-homing — 다중 경로 + heartbeat failover
- Chunk — 제어/데이터를 하나의 패킷 구조로 구성
- WebRTC DataChannel — 실무 대표 활용 사례
단계별 이해
- 연결 모델
4-way handshake와 state cookie 방식을 먼저 이해합니다. - 스트림 모델
스트림 번호 기반 데이터 분리를 확인합니다. - 경로 모델
멀티호밍에서 primary/backup 경로 전환을 살펴봅니다. - 운영 모델
sysctl과 소켓 옵션으로 타임아웃/재전송 정책을 조정합니다.
SCTP (Stream Control Transmission Protocol)
SCTP(IP 프로토콜 132)는 TCP의 신뢰성과 UDP의 메시지 경계 보존을 결합한 전송 프로토콜입니다. 원래 SIGTRAN(SS7 시그널링 전송)을 위해 설계되었으나, 범용 전송 프로토콜로 발전하여 RFC 4960으로 표준화되었습니다. 멀티스트리밍, 멀티호밍, 4-way handshake, 메시지 지향 전달 등 TCP와 UDP 모두에 없는 고유 기능을 제공합니다.
리눅스 커널은 net/sctp/ 디렉터리에 SCTP 프로토콜 스택을 구현하며,
CONFIG_IP_SCTP 커널 설정으로 빌드합니다. 모듈 이름은 sctp이고,
사용자 공간에서는 lksctp-tools 패키지가 헤더 파일과 유틸리티를 제공합니다.
# SCTP 커널 모듈 로드
$ modprobe sctp
# 모듈 확인
$ lsmod | grep sctp
sctp 413696 0
libcrc32c 16384 2 sctp,nf_conntrack
# lksctp-tools 설치 (Debian/Ubuntu)
$ apt-get install lksctp-tools libsctp-dev
# lksctp-tools 설치 (RHEL/CentOS)
$ yum install lksctp-tools lksctp-tools-devel
iptables -A INPUT -p sctp --dport 9999 -j ACCEPT
SCTP의 역사는 1990년대 후반 IETF SIGTRAN 워킹 그룹에서 시작됩니다. 기존 SS7 시그널링을 IP 네트워크로 전송하기 위해 TCP와 UDP 모두 부적합했습니다. TCP는 HoL blocking과 단일 경로 문제가 있었고, UDP는 신뢰성이 없었습니다. 이 요구사항에서 SCTP가 탄생했으며, 2000년 RFC 2960(이후 RFC 4960으로 갱신)으로 표준화되었습니다.
| 연도 | RFC | 내용 |
|---|---|---|
| 2000 | RFC 2960 | SCTP 최초 표준화 |
| 2004 | RFC 3758 | PR-SCTP (부분 신뢰성) 확장 |
| 2004 | RFC 3873 | SCTP MIB (관리 정보 베이스) |
| 2007 | RFC 4820 | Padding Chunk |
| 2007 | RFC 4895 | AUTH 청크 (인증) |
| 2007 | RFC 4960 | SCTP 개정판 (현재 주요 표준) |
| 2007 | RFC 5061 | 동적 주소 재설정 (ASCONF) |
| 2011 | RFC 6458 | 소켓 API 확장 (sendv/recvv) |
| 2013 | RFC 6951 | UDP 캡슐화 (NAT 통과) |
| 2017 | RFC 8260 | 스트림 스케줄러 및 메시지 인터리빙 |
/* SCTP 프로토콜 등록 — net/sctp/protocol.c */
static const struct net_protocol sctp_protocol = {
.handler = sctp_rcv, /* 패킷 수신 핸들러 */
.err_handler = sctp_v4_err, /* ICMP 오류 핸들러 */
.no_policy = 1, /* xfrm 정책 미적용 */
};
/* 모듈 초기화 — sctp_init() */
static int __init sctp_init(void)
{
/* 1. 프로토콜 구조체 등록 */
inet_add_protocol(&sctp_protocol, IPPROTO_SCTP);
/* 2. 소켓 ops 등록 (SOCK_STREAM, SOCK_SEQPACKET) */
inet_register_protosw(&sctp_stream_protosw);
inet_register_protosw(&sctp_seqpacket_protosw);
/* 3. 해시 테이블 초기화 (포트, 엔드포인트 검색용) */
sctp_hash_init();
/* 4. sysctl 등록 */
sctp_sysctl_register();
/* 5. proc 파일시스템 등록 */
sctp_proc_init();
return 0;
}
SCTP 핵심 특성
SCTP는 TCP와 UDP의 장점을 결합하면서도 각각의 한계를 극복하도록 설계되었습니다. 다음 표는 세 프로토콜의 핵심 특성을 비교합니다.
| 특성 | TCP | UDP | SCTP |
|---|---|---|---|
| 연결 지향 | O | X | O (Association) |
| 신뢰적 전달 | O | X | O (선택적 비순서 전달도 가능) |
| 메시지 경계 보존 | X (바이트 스트림) | O | O (청크 단위) |
| 멀티스트리밍 | X | X | O (독립 스트림, HoL blocking 방지) |
| 멀티호밍 | X | X | O (다수 IP 주소 바인딩, failover) |
| SYN Flood 방어 | SYN Cookie | 해당 없음 | 4-way handshake + Cookie (내장) |
| 부분 신뢰성 | X | 해당 없음 | O (PR-SCTP, RFC 3758) |
| Handshake | 3-way | 없음 | 4-way (쿠키 기반) |
| 종료 | 4-way (FIN/ACK) | 없음 | 3-way (SHUTDOWN) |
| 확인응답 | Byte 기반 ACK | 없음 | TSN 기반 SACK |
| 혼잡 제어 | 단일 경로 | 없음 | 경로별 독립 혼잡 제어 |
SCTP 데이터 흐름
SCTP 데이터 전송은 애플리케이션 메시지를 청크(Chunk) 단위로 분할하고, TSN(Transmission Sequence Number)을 부여하여 신뢰적으로 전달합니다. 수신 측은 SACK 청크로 수신 상태를 알려주며, 송신 측은 이를 기반으로 재전송을 결정합니다.
위 다이어그램에서 핵심 포인트는 다음과 같습니다:
- 단편화: 메시지가 경로 MTU보다 클 경우 여러 DATA 청크로 분할합니다. B/E 플래그로 첫/마지막 단편을 표시합니다.
- 번들링: 여러 DATA 청크를 하나의 SCTP 패킷에 묶어 전송 효율을 높입니다.
- TSN 기반 확인: 각 DATA 청크에 고유 TSN이 부여되며, 수신 측은 SACK로 수신된 TSN과 Gap을 보고합니다.
- 재전송: SACK의 Gap Report를 통해 손실된 청크를 선택적으로 재전송합니다.
SCTP Association과 4-Way Handshake
SCTP Association은 TCP 연결에 해당하는 개념으로, 양쪽 엔드포인트 간의 통신 채널을 나타냅니다. TCP의 3-way handshake와 달리 SCTP는 4-way handshake를 사용하여 DoS(Denial-of-Service) 공격에 대한 근본적인 방어를 제공합니다.
핵심 차이점은 서버가 INIT-ACK를 보낼 때 어떤 상태도 저장하지 않는다는 것입니다. 대신 Association 설정에 필요한 모든 정보를 State Cookie에 HMAC 서명과 함께 인코딩하여 클라이언트에게 돌려보냅니다. 클라이언트가 COOKIE-ECHO로 이 쿠키를 되돌려보내야만 서버가 상태를 할당합니다.
| 단계 | 방향 | 청크 | 포함 정보 | 서버 상태 |
|---|---|---|---|---|
| 1 | 클라이언트 → 서버 | INIT | Initiate Tag, OS, MIS, a-rwnd, 주소 목록 | 상태 없음 |
| 2 | 서버 → 클라이언트 | INIT-ACK | Initiate Tag, OS, MIS, a-rwnd, State Cookie | 상태 없음 (Cookie에 인코딩) |
| 3 | 클라이언트 → 서버 | COOKIE-ECHO | State Cookie (+ DATA 가능) | Cookie 검증 중 |
| 4 | 서버 → 클라이언트 | COOKIE-ACK | 검증 완료 확인 | ESTABLISHED |
/* SCTP 4-way handshake (INIT → INIT-ACK → COOKIE-ECHO → COOKIE-ACK) */
/*
* 1. 클라이언트 → INIT 청크 (자신의 Tag, 스트림 수, 주소 목록)
* 2. 서버 → INIT-ACK 청크 (자신의 Tag + State Cookie)
* → 서버는 이 시점에서 상태를 저장하지 않음 (SYN Flood 면역)
* → State Cookie에 검증 정보를 HMAC으로 서명하여 인코딩
* 3. 클라이언트 → COOKIE-ECHO (State Cookie 그대로 반환)
* 4. 서버 → COOKIE-ACK (Cookie 검증 후 Association 생성)
* → 3단계에서 이미 데이터 포함 가능 (TCP Fast Open과 유사)
*/
/* include/net/sctp/structs.h */
struct sctp_association {
struct sctp_ep_common base;
struct list_head transports; /* 원격 주소 목록 (멀티호밍) */
struct sctp_stream stream; /* 스트림 관리 */
__u16 c.sinit_num_ostreams; /* 출력 스트림 수 */
__u16 c.sinit_max_instreams; /* 입력 스트림 수 */
__u32 c.my_vtag; /* 자신의 Verification Tag */
__u32 c.peer_vtag; /* 상대의 Verification Tag */
/* ... */
};
/* State Cookie 생성 과정 — net/sctp/sm_make_chunk.c */
/* sctp_pack_cookie():
* 1. 양쪽 INIT 파라미터를 cookie에 직렬화
* 2. 타임스탬프 + Association 설정 정보 포함
* 3. HMAC-SHA1으로 서명 (sctp_cookie->hmac)
* 4. 선택적으로 AES-CBC 암호화 가능
*/
static struct sctp_cookie *sctp_pack_cookie(
const struct sctp_endpoint *ep,
const struct sctp_association *asoc,
const struct sctp_chunk *init_chunk,
int *cookie_len)
{
struct sctp_signed_cookie *cookie;
int headersize, bodysize;
headersize = sizeof(struct sctp_paramhdr) +
sizeof(struct sctp_signed_cookie);
bodysize = ntohs(init_chunk->chunk_hdr->length);
/* HMAC 서명 — endpoint의 secret_key 사용 */
sctp_hash_digest(ep->secret_key, SCTP_SECRET_SIZE,
(__u8 *)cookie, bodysize,
cookie->signature);
return cookie;
}
net.sctp.valid_cookie_life (기본 60초) 이내에 COOKIE-ECHO가 도착해야 합니다.
만료된 쿠키로 COOKIE-ECHO를 보내면 서버는 ABORT를 전송합니다.
고지연 네트워크에서는 이 값을 늘릴 필요가 있습니다.
SCTP의 4-way handshake는 TCP의 3-way handshake보다 한 단계가 더 필요하지만, 이로 인해 서버 측에서 SYN Flood와 같은 자원 고갈 공격에 근본적으로 면역이 됩니다. TCP에서는 SYN Cookie라는 추가 메커니즘으로 이 문제를 완화하지만, SCTP는 프로토콜 자체에 이 보호가 내장되어 있습니다.
| 공격 유형 | TCP 대응 | SCTP 대응 |
|---|---|---|
| SYN Flood (반개방 연결) | SYN Cookie (추가 구현 필요) | 서버 무상태 → 근본적 면역 |
| RST 공격 | 취약 (시퀀스 번호 추측) | Verification Tag 검증으로 방어 |
| 연결 하이재킹 | 시퀀스 번호 추측으로 가능 | 32비트 Verification Tag + CRC32c |
| ICMP 공격 | ICMP로 연결 방해 가능 | Verification Tag로 ICMP 검증 |
/* Association 종료 과정 — 3-way SHUTDOWN */
/*
* TCP 종료 (4-way): SCTP 종료 (3-way):
* FIN → SHUTDOWN →
* ← ACK ← SHUTDOWN-ACK
* ← FIN SHUTDOWN-COMPLETE →
* ACK →
* (TIME_WAIT 2*MSL 대기) (즉시 CLOSED)
*
* SCTP 장점: TIME_WAIT 상태 없음 → 포트 즉시 재사용 가능
*/
/* 정상 종료: close() 호출 시 */
/* net/sctp/socket.c — sctp_close():
* 1. SHUTDOWN_PENDING 상태로 전이
* 2. 미전송 DATA 청크가 모두 확인될 때까지 대기
* 3. SHUTDOWN 청크 전송 (CumTSN 포함)
* 4. SHUTDOWN-ACK 수신 시 SHUTDOWN-COMPLETE 전송
* 5. Association 즉시 해제 (TIME_WAIT 없음)
*/
/* 비정상 종료: ABORT 전송 */
struct sctp_sndrcvinfo sinfo;
sinfo.sinfo_flags = SCTP_ABORT; /* ABORT 플래그 */
sinfo.sinfo_assoc_id = assoc_id;
sctp_send(fd, NULL, 0, &sinfo, 0);
/* → Association 즉시 종료, 미전송 데이터 폐기 */
close()는 미전송 데이터를 모두 보낸 후 SHUTDOWN 시퀀스를 수행합니다 (graceful).
SCTP_ABORT 플래그는 즉시 ABORT 청크를 보내고 Association을 파괴합니다 (ungraceful).
SO_LINGER 옵션으로 graceful 종료의 대기 시간을 제한할 수 있습니다.
멀티스트리밍
멀티스트리밍은 SCTP의 가장 혁신적인 기능 중 하나입니다. 하나의 Association 내에 여러 독립적인 스트림을 두어, 한 스트림에서 패킷 손실이 발생해도 다른 스트림의 데이터 전달에 영향을 주지 않습니다. 이는 TCP에서 발생하는 Head-of-Line(HoL) blocking 문제를 프로토콜 수준에서 해결합니다.
각 스트림은 자체적인 SSN(Stream Sequence Number)을 유지합니다.
스트림 내에서는 순서가 보장되지만, 서로 다른 스트림 간에는 순서 관계가 없습니다.
또한 SCTP_UNORDERED 플래그를 사용하면 스트림 내에서도 비순서 전달이 가능합니다.
| 스트림 스케줄러 | 소켓 옵션 값 | 동작 | 사용 사례 |
|---|---|---|---|
| FCFS | SCTP_SS_FCFS |
선착순 (기본값) | 일반적인 사용 |
| Round Robin | SCTP_SS_RR |
스트림 단위 라운드 로빈 | 공정한 대역폭 분배 |
| RR Packet | SCTP_SS_RR_PKT |
패킷 단위 라운드 로빈 | 세밀한 공정성 |
| Priority | SCTP_SS_PRIO |
스트림별 우선순위 | 제어 vs 데이터 분리 |
| Fair Capacity | SCTP_SS_FC |
큐잉 비율 기반 공정 분배 | 멀티미디어 스트리밍 |
/* SCTP 멀티스트리밍: 하나의 Association 내에 독립적인 스트림들 */
/* 장점: 한 스트림의 패킷 손실이 다른 스트림에 영향을 주지 않음
* → HTTP/2의 Head-of-Line blocking 문제를 프로토콜 수준에서 해결
*/
/* 사용자 공간: sctp_sendmsg로 스트림 지정 */
struct sctp_sndrcvinfo sinfo = {
.sinfo_stream = 3, /* 스트림 번호 3으로 전송 */
.sinfo_flags = SCTP_UNORDERED, /* 비순서 전달 (선택) */
.sinfo_ppid = htonl(42), /* Payload Protocol ID */
};
sctp_sendmsg(fd, data, len, NULL, 0,
sinfo.sinfo_ppid, sinfo.sinfo_flags,
sinfo.sinfo_stream, 0, 0);
/* 수신: sctp_recvmsg로 스트림 정보 확인 */
struct sctp_sndrcvinfo rinfo;
int flags = 0;
sctp_recvmsg(fd, buf, buflen, NULL, 0, &rinfo, &flags);
printf("stream=%u ppid=%u\\n", rinfo.sinfo_stream, ntohl(rinfo.sinfo_ppid));
/* 스트림 스케줄러 설정 — RFC 8260 */
struct sctp_assoc_value av;
av.assoc_id = 0;
av.assoc_value = SCTP_SS_PRIO; /* 우선순위 기반 스케줄러 */
setsockopt(fd, IPPROTO_SCTP, SCTP_STREAM_SCHEDULER, &av, sizeof(av));
/* 스트림 0에 높은 우선순위 부여 (제어 메시지용) */
struct sctp_stream_value sv;
sv.assoc_id = 0;
sv.stream_id = 0;
sv.stream_value = 0; /* 값이 낮을수록 높은 우선순위 */
setsockopt(fd, IPPROTO_SCTP, SCTP_STREAM_SCHEDULER_VALUE, &sv, sizeof(sv));
/* 스트림 1에 낮은 우선순위 부여 (데이터 전송용) */
sv.stream_id = 1;
sv.stream_value = 10;
setsockopt(fd, IPPROTO_SCTP, SCTP_STREAM_SCHEDULER_VALUE, &sv, sizeof(sv));
/* 커널 내부: 스트림 관리 구조체 — include/net/sctp/structs.h */
struct sctp_stream {
struct {
struct sctp_stream_out_ext *ext;
__u16 ssn; /* Stream Sequence Number */
__u8 state; /* 스트림 상태 */
} *out; /* 출력 스트림 배열 */
struct {
__u16 ssn; /* 수신 SSN */
__u16 mid; /* Message ID (I-DATA용) */
} *in; /* 입력 스트림 배열 */
__u16 outcnt; /* 출력 스트림 수 */
__u16 incnt; /* 입력 스트림 수 */
struct sctp_stream_interleave *si; /* 인터리빙 핸들러 */
};
멀티호밍과 Failover
SCTP의 멀티호밍은 하나의 Association에서 양쪽 엔드포인트가 각각 여러 IP 주소를 가질 수 있는 기능입니다. INIT/INIT-ACK 교환 시 양쪽이 자신의 주소 목록을 알려주고, 이후 Heartbeat 메커니즘으로 각 경로의 활성 상태를 확인합니다. Primary 경로에 장애가 발생하면 자동으로 보조(Alternate) 경로로 전환합니다.
| 경로 상태 | 상수 | 설명 | 전환 조건 |
|---|---|---|---|
| Active | SCTP_ACTIVE |
정상 사용 가능 | Heartbeat-ACK 수신 또는 DATA-ACK 수신 |
| Inactive | SCTP_INACTIVE |
장애로 비활성화 | error_count > path_max_retrans |
| PF (Potentially Failed) | SCTP_PF |
잠재적 장애 (빠른 전환용) | error_count > pf_retrans |
| Unconfirmed | SCTP_UNCONFIRMED |
아직 확인되지 않은 주소 | 초기 상태 (INIT에서 알려진 주소) |
/* SCTP 멀티호밍: 양쪽 엔드포인트가 여러 IP 주소를 가질 수 있음 */
/* → 한 경로 실패 시 자동으로 다른 경로로 전환 (heartbeat 기반) */
/* 다수 주소 바인딩 */
struct sockaddr_in addrs[2];
addrs[0].sin_addr.s_addr = inet_addr("10.0.0.1");
addrs[0].sin_port = htons(9999);
addrs[1].sin_addr.s_addr = inet_addr("10.0.1.1");
addrs[1].sin_port = htons(9999);
sctp_bindx(fd, (struct sockaddr *)addrs, 2, SCTP_BINDX_ADD_ADDR);
/* sctp_connectx: 여러 서버 주소로 동시에 연결 시도 */
struct sockaddr_in dests[2];
dests[0].sin_addr.s_addr = inet_addr("10.0.0.2");
dests[0].sin_port = htons(9999);
dests[1].sin_addr.s_addr = inet_addr("10.0.1.2");
dests[1].sin_port = htons(9999);
sctp_assoc_t assoc_id;
sctp_connectx(fd, (struct sockaddr *)dests, 2, &assoc_id);
/* Primary 주소 수동 변경 */
struct sctp_prim prim;
prim.ssp_assoc_id = assoc_id;
prim.ssp_addr = *(struct sockaddr_storage *)&dests[1];
setsockopt(fd, IPPROTO_SCTP, SCTP_PRIMARY_ADDR, &prim, sizeof(prim));
/* Heartbeat: 보조 경로의 상태를 주기적으로 확인
* net.sctp.hb_interval = 30000 (ms, 기본 30초)
* 연속 path_max_retrans 회 실패 시 경로 비활성화
* → primary 경로 실패 시 active 보조 경로로 자동 전환
*/
/* Potentially Failed (PF) 상태 — 빠른 경로 전환 */
/* pf_retrans 값을 path_max_retrans보다 작게 설정하면
* 경로가 완전히 DOWN되기 전에 보조 경로로 빠르게 전환 */
struct sctp_paddrparams params;
params.spp_assoc_id = assoc_id;
params.spp_address = *(struct sockaddr_storage *)&dests[0];
params.spp_pathmaxrxt = 5; /* 5회 실패시 DOWN */
params.spp_hbinterval = 10000; /* 10초 Heartbeat */
params.spp_flags = SPP_HB_ENABLE;
setsockopt(fd, IPPROTO_SCTP, SCTP_PEER_ADDR_PARAMS,
¶ms, sizeof(params));
/* 커널 내부: net/sctp/transport.c */
/* sctp_assoc_control_transport():
* SCTP_TRANSPORT_DOWN → 해당 경로 비활성화
* → sctp_assoc_update_retran_path()로 재전송 경로 변경
*/
/* net/sctp/associola.c — 경로 전환 로직 */
void sctp_assoc_update_retran_path(struct sctp_association *asoc)
{
struct sctp_transport *t, *next;
struct list_head *head = &asoc->peer.transport_addr_list;
struct list_head *pos;
t = asoc->peer.retran_path;
/* 현재 재전송 경로 다음부터 active 경로 탐색 */
list_for_each(pos, &t->transports) {
if (pos == head)
continue;
next = list_entry(pos, struct sctp_transport, transports);
if (next->state != SCTP_UNCONFIRMED &&
next->state != SCTP_PF) {
asoc->peer.retran_path = next;
return;
}
}
}
ASCONF(Address Configuration Change) 청크를 통해 동적으로 주소를 추가하거나
삭제할 수 있습니다 (RFC 5061). sctp_bindx(fd, addr, 1, SCTP_BINDX_ADD_ADDR)로
새 주소를 추가하면 커널이 자동으로 ASCONF를 전송합니다.
이 기능을 사용하려면 net.sctp.addip_enable = 1 설정이 필요합니다.
SCTP 청크 타입
SCTP 패킷은 하나 이상의 청크(Chunk)로 구성됩니다. 각 청크는 공통 헤더(Type, Flags, Length)와 가변 길이 Value로 이루어집니다. 제어 청크와 데이터 청크가 하나의 패킷에 함께 번들링될 수 있습니다.
| 청크 | 타입 | 용도 | RFC |
|---|---|---|---|
| DATA | 0 | 사용자 데이터 전달 (TSN, 스트림 번호, 시퀀스 번호 포함) | 4960 |
| INIT | 1 | Association 시작 요청 | 4960 |
| INIT ACK | 2 | INIT 응답 + State Cookie | 4960 |
| SACK | 3 | 선택적 확인응답 (TCP SACK와 유사) | 4960 |
| HEARTBEAT | 4 | 경로 활성 확인 (멀티호밍) | 4960 |
| HEARTBEAT ACK | 5 | Heartbeat 응답 | 4960 |
| ABORT | 6 | Association 즉시 종료 | 4960 |
| SHUTDOWN | 7 | 정상 종료 시작 | 4960 |
| SHUTDOWN ACK | 8 | SHUTDOWN 확인 | 4960 |
| ERROR | 9 | 오류 보고 (원인 코드 포함) | 4960 |
| COOKIE ECHO | 10 | State Cookie 반환 (handshake 3단계) | 4960 |
| COOKIE ACK | 11 | Cookie 확인 (handshake 4단계) | 4960 |
| ECNE | 12 | ECN Echo | 4960 |
| CWR | 13 | Congestion Window Reduced | 4960 |
| SHUTDOWN COMPLETE | 14 | 종료 완료 | 4960 |
| AUTH | 15 | 인증 청크 (HMAC) | 4895 |
| ASCONF ACK | 128 | 주소 설정 변경 확인 | 5061 |
| RE-CONFIG | 130 | 스트림 재설정 | 6525 |
| PAD | 132 | 패딩 | 4820 |
| FORWARD TSN | 192 | 수신 불필요한 TSN 건너뛰기 (부분 신뢰성) | 3758 |
| ASCONF | 193 | 주소 설정 변경 요청 | 5061 |
| I-DATA | 64 | 인터리빙 데이터 (MID 기반) | 8260 |
| I-FORWARD-TSN | 194 | 인터리빙용 FORWARD TSN | 8260 |
/* 청크 공통 헤더 — include/uapi/linux/sctp.h */
struct sctp_chunkhdr {
__u8 type; /* 청크 타입 (0-255) */
__u8 flags; /* 타입별 플래그 */
__be16 length; /* 청크 전체 길이 (헤더 포함) */
};
/* DATA 청크 헤더 */
struct sctp_datahdr {
__be32 tsn; /* Transmission Sequence Number */
__be16 stream; /* Stream Identifier */
__be16 ssn; /* Stream Sequence Number */
__be32 ppid; /* Payload Protocol Identifier */
__u8 payload[]; /* 사용자 데이터 */
};
/* INIT 청크 파라미터 */
struct sctp_inithdr {
__be32 init_tag; /* Initiate Tag */
__be32 a_rwnd; /* Advertised Receiver Window Credit */
__be16 num_outbound_streams; /* 출력 스트림 수 */
__be16 num_inbound_streams; /* 입력 스트림 수 */
__be32 initial_tsn; /* 초기 TSN */
};
sctp를 로드해야 하며, lksctp-tools 패키지가 사용자 공간 유틸리티를 제공합니다.
청크 번들링과 단편화
SCTP는 효율적인 전송을 위해 두 가지 메커니즘을 제공합니다. 번들링(Bundling)은 여러 작은 메시지를 하나의 SCTP 패킷에 묶어 전송하며, 단편화(Fragmentation)는 큰 메시지를 경로 MTU에 맞게 분할합니다.
/* 번들링 비활성화 (지연 민감 애플리케이션) */
int nodelay = 1;
setsockopt(fd, IPPROTO_SCTP, SCTP_NODELAY, &nodelay, sizeof(nodelay));
/* 최대 단편 크기 설정 */
struct sctp_assoc_value maxseg;
maxseg.assoc_id = 0;
maxseg.assoc_value = 1400; /* DATA 페이로드 최대 1400 바이트 */
setsockopt(fd, IPPROTO_SCTP, SCTP_MAXSEG, &maxseg, sizeof(maxseg));
/* 커널 내부: 번들링 판단 — net/sctp/output.c */
/* sctp_packet_append_chunk():
* 패킷에 청크를 추가할 때 남은 공간을 확인하고
* MTU 초과 시 새 패킷을 생성
* SCTP_NODELAY 설정 시 즉시 전송
*/
TSN 기반 신뢰적 전송과 SACK 처리
SCTP는 TSN(Transmission Sequence Number)을 사용하여 각 DATA 청크의 전달을 추적합니다. TCP의 바이트 기반 시퀀스 번호와 달리, SCTP는 청크 단위로 TSN을 부여합니다. 수신 측은 SACK(Selective Acknowledgement) 청크로 수신 상태를 보고합니다.
| SACK 필드 | 크기 | 설명 |
|---|---|---|
| Cumulative TSN Ack | 32비트 | 연속으로 수신된 마지막 TSN (Gap 없는 지점) |
| Advertised Receiver Window (a-rwnd) | 32비트 | 수신자의 여유 버퍼 크기 |
| Number of Gap Ack Blocks | 16비트 | Gap Block 수 (비연속 수신 영역) |
| Number of Dup TSNs | 16비트 | 중복 수신된 TSN 수 |
| Gap Ack Block Start/End | 16비트 x 2 | CumTSN 기준 상대 오프셋 (시작-끝) |
/* SACK 청크 구조체 — include/uapi/linux/sctp.h */
struct sctp_sackhdr {
__be32 cum_tsn_ack; /* 연속 확인된 마지막 TSN */
__be32 a_rwnd; /* 수신 윈도우 크기 */
__be16 num_gap_blocks; /* Gap Ack Block 수 */
__be16 num_dup_tsns; /* 중복 TSN 수 */
};
/* Gap Ack Block */
struct sctp_gap_ack_block {
__be16 start; /* CumTSN 기준 시작 오프셋 */
__be16 end; /* CumTSN 기준 끝 오프셋 */
};
/* 재전송 판단 — net/sctp/outqueue.c */
/* sctp_outq_sack():
* 1. Cumulative TSN Ack까지의 청크를 전송 큐에서 제거
* 2. Gap Ack Block에 포함되지 않는 TSN을 재전송 후보로 표시
* 3. 3번 Gap Report가 누적되면 Fast Retransmit 수행
* 4. a_rwnd 업데이트하여 흐름 제어에 반영
*/
net.sctp.sack_timeout (기본 200ms) 동안 기다려서 여러 DATA 청크에 대해 하나의 SACK를 보냅니다.
단, 2개 이상의 패킷이 도착하면 즉시 SACK를 전송합니다.
SCTP 상태 머신
SCTP Association은 13개의 상태를 거칩니다. TCP의 상태 머신과 유사하지만, 4-way handshake에 대응하는 COOKIE_WAIT와 COOKIE_ECHOED 상태가 추가됩니다. 종료 과정은 TCP의 4-way FIN 교환 대신 3-way SHUTDOWN 교환을 사용합니다.
| 상태 | enum 값 | 설명 | 진입 조건 |
|---|---|---|---|
| CLOSED | SCTP_STATE_CLOSED | 초기/종료 상태 | 시작 또는 종료 완료 |
| COOKIE_WAIT | SCTP_STATE_COOKIE_WAIT | INIT 전송 후 대기 | INIT 전송 |
| COOKIE_ECHOED | SCTP_STATE_COOKIE_ECHOED | COOKIE-ECHO 전송 후 대기 | INIT-ACK 수신 |
| ESTABLISHED | SCTP_STATE_ESTABLISHED | 데이터 교환 가능 | COOKIE-ACK 수신/전송 |
| SHUTDOWN_PENDING | SCTP_STATE_SHUTDOWN_PENDING | 종료 요청, 미전송 데이터 있음 | close() 호출 |
| SHUTDOWN_SENT | SCTP_STATE_SHUTDOWN_SENT | SHUTDOWN 전송 완료 | 미전송 데이터 완료 |
| SHUTDOWN_RECEIVED | SCTP_STATE_SHUTDOWN_RECEIVED | 상대의 SHUTDOWN 수신 | SHUTDOWN 청크 수신 |
| SHUTDOWN_ACK_SENT | SCTP_STATE_SHUTDOWN_ACK_SENT | SHUTDOWN-ACK 전송 완료 | 수신 데이터 처리 완료 |
/* 커널 내부: Association 상태 전이 — include/net/sctp/constants.h */
enum sctp_state {
SCTP_STATE_CLOSED = 0,
SCTP_STATE_COOKIE_WAIT = 1,
SCTP_STATE_COOKIE_ECHOED = 2,
SCTP_STATE_ESTABLISHED = 3,
SCTP_STATE_SHUTDOWN_PENDING = 4,
SCTP_STATE_SHUTDOWN_SENT = 5,
SCTP_STATE_SHUTDOWN_RECEIVED = 6,
SCTP_STATE_SHUTDOWN_ACK_SENT = 7,
};
/* 상태 전이 테이블 — net/sctp/sm_statetable.c */
/* 이벤트(청크 수신/타이머/사용자 명령) + 현재 상태 → 동작 함수 매핑
* sctp_sm_statetable[event][state] = { .fn = handler, .name = "..." }
*
* 예: COOKIE_ECHO 수신 + CLOSED 상태
* → sctp_sf_do_5_1D_ce() 호출
* → State Cookie 검증 → Association 생성 → ESTABLISHED 전이
*/
혼잡 제어
SCTP의 혼잡 제어는 TCP의 혼잡 제어 알고리즘을 기반으로 하되, 경로별(per-destination) 독립적인 혼잡 상태를 유지합니다. 각 경로(transport)마다 별도의 cwnd(congestion window), ssthresh, RTO를 관리합니다.
| 파라미터 | 초기값 | sysctl/소켓 옵션 | 설명 |
|---|---|---|---|
| cwnd | min(4*MTU, max(2*MTU, 4380)) | 자동 계산 | 혼잡 윈도우 (경로별) |
| ssthresh | a-rwnd (상대 수신 윈도우) | 자동 계산 | Slow Start 임계값 |
| RTO | 3000ms | net.sctp.rto_initial |
재전송 타임아웃 (경로별) |
| RTO min | 1000ms | net.sctp.rto_min |
RTO 최소값 |
| RTO max | 60000ms | net.sctp.rto_max |
RTO 최대값 |
| SRTT | 자동 측정 | 자동 계산 | Smoothed Round-Trip Time |
| RTTVAR | 자동 측정 | 자동 계산 | RTT 변이 |
/* 경로별 혼잡 제어 변수 — include/net/sctp/structs.h */
struct sctp_transport {
/* ... */
__u32 cwnd; /* 혼잡 윈도우 */
__u32 ssthresh; /* Slow Start 임계값 */
__u32 partial_bytes_acked; /* CA 모드 cwnd 증가 추적 */
__u32 flight_size; /* 미확인 데이터 크기 */
/* RTO 계산 */
unsigned long rto; /* 재전송 타임아웃 (jiffies) */
__u32 srtt; /* Smoothed RTT */
__u32 rttvar; /* RTT Variation */
__u32 rto_pending; /* RTT 측정 진행 중 */
/* 장애 카운터 */
__u16 error_count; /* 연속 오류 횟수 */
__u16 pathmaxrxt; /* 경로 최대 재전송 횟수 */
/* ... */
};
/* Slow Start 구현 — net/sctp/transport.c */
static void sctp_transport_raise_cwnd(
struct sctp_transport *transport,
__u32 sack_ctsn, __u32 bytes_acked)
{
__u32 cwnd = transport->cwnd;
__u32 ssthresh = transport->ssthresh;
__u32 pmtu = transport->pathmtu;
if (cwnd <= ssthresh) {
/* Slow Start: cwnd += min(bytes_acked, pmtu) */
cwnd += min(bytes_acked, pmtu);
} else {
/* Congestion Avoidance: cwnd += pmtu per RTT */
transport->partial_bytes_acked += bytes_acked;
if (transport->partial_bytes_acked >= cwnd) {
cwnd += pmtu;
transport->partial_bytes_acked -= cwnd;
}
}
transport->cwnd = cwnd;
}
ssthresh = max(cwnd/2, 2*MTU)로 설정하고, cwnd = ssthresh로 축소합니다.
TCP Reno와 달리 SCTP는 경로별로 독립적인 Fast Recovery를 수행하므로,
한 경로의 손실이 다른 경로의 전송률에 영향을 주지 않습니다.
/* Fast Retransmit 조건 — net/sctp/outqueue.c */
/* sctp_outq_sack() 내부:
* 각 미확인 청크에 대해 gap_report 카운터를 추적
* gap_report가 3 이상이면 Fast Retransmit 대상으로 표시
*/
static void sctp_check_transmitted(
struct sctp_outq *q,
struct list_head *transmitted_queue,
struct sctp_transport *transport,
union sctp_addr *saddr,
struct sctp_sackhdr *sack,
__u32 *highest_new_tsn)
{
struct sctp_chunk *tchunk;
list_for_each_entry(tchunk, transmitted_queue, transmitted_list) {
if (!tchunk->tsn_gap_acked) {
/* Gap에 포함되지 않은 TSN → 손실 후보 */
tchunk->tsn_missing_report++;
if (tchunk->tsn_missing_report >= 3) {
/* Fast Retransmit: 재전송 큐로 이동 */
sctp_retransmit_mark(q, tchunk, 0);
}
}
}
}
/* T3-rtx 타이머 만료 시 (RTO 기반 재전송) */
/* → cwnd = 1 MTU로 축소 (Slow Start 재시작)
* → ssthresh = max(cwnd/2, 4*MTU)
* → 해당 경로의 미확인 청크 모두 재전송 표시
* → 보조 경로로 재전송 (primary 장애 가능성)
*/
| 혼잡 이벤트 | cwnd 변화 | ssthresh 변화 | 추가 동작 |
|---|---|---|---|
| Slow Start (cwnd ≤ ssthresh) | +min(bytes_acked, MTU) per SACK | 변경 없음 | 지수적 증가 |
| Congestion Avoidance | +MTU per RTT | 변경 없음 | 선형적 증가 |
| Fast Retransmit (3 Gap Report) | = ssthresh | = max(cwnd/2, 2*MTU) | 즉시 재전송 |
| T3-rtx 타이머 만료 | = 1 MTU | = max(cwnd/2, 4*MTU) | Slow Start 재시작 |
| ECN Echo (ECNE 청크) | = ssthresh | = max(cwnd/2, 2*MTU) | CWR 청크 전송 |
| 유휴 후 재개 | = max(cwnd, 4*MTU) | 변경 없음 | Heartbeat 기반 RTT 갱신 |
SCTP 소켓 API
SCTP는 두 가지 소켓 스타일을 제공합니다: TCP와 유사한 one-to-one (SOCK_STREAM) 스타일과 UDP와 유사한 one-to-many (SOCK_SEQPACKET) 스타일입니다.
| 특성 | one-to-one (SOCK_STREAM) | one-to-many (SOCK_SEQPACKET) |
|---|---|---|
| 소켓 타입 | SOCK_STREAM |
SOCK_SEQPACKET |
| Association 수 | 1:1 (소켓당 하나) | 1:N (소켓에 여러 Association) |
| listen/accept | 사용 (TCP와 동일) | listen만 (자동 Association) |
| API 호환성 | TCP 소켓 API 호환 | SCTP 전용 API 필요 |
| peeloff | 해당 없음 | Association을 별도 소켓으로 분리 |
| 사용 사례 | 기존 TCP 코드 마이그레이션 | 서버 (다중 클라이언트 처리) |
/* ===== one-to-one 스타일 (TCP 호환) ===== */
int fd = socket(AF_INET, SOCK_STREAM, IPPROTO_SCTP);
/* bind + listen + accept (TCP와 동일한 패턴) */
struct sockaddr_in addr = {
.sin_family = AF_INET,
.sin_port = htons(9999),
.sin_addr.s_addr = INADDR_ANY,
};
bind(fd, (struct sockaddr *)&addr, sizeof(addr));
/* 스트림 수 설정 (bind 후, listen 전) */
struct sctp_initmsg initmsg = {
.sinit_num_ostreams = 10, /* 출력 스트림 10개 */
.sinit_max_instreams = 10, /* 입력 스트림 최대 10개 */
.sinit_max_attempts = 4, /* INIT 재전송 횟수 */
.sinit_max_init_timeo = 30000, /* INIT 타임아웃 (ms) */
};
setsockopt(fd, IPPROTO_SCTP, SCTP_INITMSG, &initmsg, sizeof(initmsg));
listen(fd, 5);
int client_fd = accept(fd, NULL, NULL);
/* 데이터 송수신 — send/recv 또는 sctp_sendmsg/sctp_recvmsg */
send(client_fd, "hello", 5, 0); /* 기본 스트림 0 */
/* ===== one-to-many 스타일 (SCTP 고유) ===== */
int fd = socket(AF_INET, SOCK_SEQPACKET, IPPROTO_SCTP);
/* bind + listen (accept 없음 — 자동 Association) */
bind(fd, (struct sockaddr *)&addr, sizeof(addr));
listen(fd, 5);
/* 수신: 여러 클라이언트의 메시지를 하나의 소켓으로 */
struct sctp_sndrcvinfo sinfo;
int flags = 0;
int n = sctp_recvmsg(fd, buf, sizeof(buf),
(struct sockaddr *)&peer, &peerlen,
&sinfo, &flags);
printf("assoc=%u stream=%u\\n",
sinfo.sinfo_assoc_id, sinfo.sinfo_stream);
/* peeloff: Association을 별도의 one-to-one 소켓으로 분리 */
int peeled_fd = sctp_peeloff(fd, sinfo.sinfo_assoc_id);
/* 이제 peeled_fd로 해당 Association과 1:1 통신 */
send(peeled_fd, "dedicated", 9, 0);
/* RFC 6458: 새로운 sendv/recvv API */
/* sctp_sendv: cmsg 기반 확장 전송 */
struct iovec iov;
struct sctp_sendv_spa spa;
iov.iov_base = data;
iov.iov_len = len;
memset(&spa, 0, sizeof(spa));
spa.sendv_flags = SCTP_SEND_SNDINFO_VALID;
spa.sendv_sndinfo.snd_sid = 3; /* 스트림 3 */
spa.sendv_sndinfo.snd_ppid = htonl(42);
sctp_sendv(fd, &iov, 1,
(struct sockaddr *)&dest, 1,
&spa, sizeof(spa),
SCTP_SENDV_SPA, 0);
/* sctp_recvv: cmsg 기반 확장 수신 */
struct sctp_rcvinfo rcvinfo;
socklen_t infolen = sizeof(rcvinfo);
unsigned int infotype;
sctp_recvv(fd, &iov, 1,
(struct sockaddr *)&peer, &peerlen,
&rcvinfo, &infolen, &infotype, &flags);
PR-SCTP (부분 신뢰성)
PR-SCTP(Partially Reliable SCTP)(RFC 3758)는 SCTP의 신뢰적 전송을 선택적으로 완화하는 확장입니다. 실시간 미디어나 게임 데이터처럼 오래된 데이터보다 최신 데이터가 중요한 경우, 일정 조건에서 재전송을 포기하고 수신 측에 FORWARD-TSN으로 건너뛰기를 알립니다.
| PR-SCTP 정책 | 플래그 | 동작 | 사용 사례 |
|---|---|---|---|
| Timed Reliability | SCTP_PR_SCTP_TTL |
지정 시간 후 재전송 포기 | 실시간 미디어, VoIP |
| Limited Retransmission | SCTP_PR_SCTP_RTX |
N회 재전송 후 포기 | 게임 상태 업데이트 |
| Priority Based | SCTP_PR_SCTP_PRIO |
버퍼 초과 시 낮은 우선순위 폐기 | 다중 품질 수준 스트리밍 |
/* PR-SCTP 사용 예시: Timed Reliability */
struct sctp_sndinfo sndinfo;
memset(&sndinfo, 0, sizeof(sndinfo));
sndinfo.snd_sid = 0;
sndinfo.snd_flags = SCTP_UNORDERED;
sndinfo.snd_ppid = htonl(51); /* WebRTC DCEP */
/* PR-SCTP 정책 설정: 500ms 후 재전송 포기 */
struct sctp_prinfo prinfo;
prinfo.pr_policy = SCTP_PR_SCTP_TTL;
prinfo.pr_value = 500; /* 밀리초 */
/* sctp_sendv로 PR-SCTP 메시지 전송 */
struct sctp_sendv_spa spa;
spa.sendv_flags = SCTP_SEND_SNDINFO_VALID | SCTP_SEND_PRINFO_VALID;
spa.sendv_sndinfo = sndinfo;
spa.sendv_prinfo = prinfo;
sctp_sendv(fd, &iov, 1, NULL, 0,
&spa, sizeof(spa), SCTP_SENDV_SPA, 0);
/* Limited Retransmission: 최대 2회 재전송 */
prinfo.pr_policy = SCTP_PR_SCTP_RTX;
prinfo.pr_value = 2; /* 재전송 횟수 */
SCTP_UNORDERED와 함께 사용합니다.
SCTP 인증 (AUTH 청크)
SCTP AUTH(RFC 4895)는 특정 청크 타입에 대해 HMAC 기반 인증을 제공합니다. Association 설정 시 양쪽이 공유 키와 인증할 청크 타입을 협상하며, AUTH 청크가 인증 대상 청크 앞에 번들링됩니다.
| AUTH 구성 요소 | 설명 |
|---|---|
| Shared Key | 양쪽이 사전에 설정하는 공유 비밀 키 |
| HMAC Identifier | SHA-1(1) 또는 SHA-256(3) 선택 |
| Chunk List | 인증할 청크 타입 목록 (ASCONF, ASCONF-ACK 등) |
| Key ID | 여러 키 중 선택을 위한 식별자 |
/* SCTP 인증 설정 예시 */
/* 1. 공유 키 설정 */
struct sctp_authkey *authkey;
int keylen = sizeof(struct sctp_authkey) + 16;
authkey = malloc(keylen);
authkey->sca_keynumber = 1; /* Key ID */
authkey->sca_keylength = 16; /* 키 길이 */
memcpy(authkey->sca_key, "shared_secret_16", 16);
setsockopt(fd, IPPROTO_SCTP, SCTP_AUTH_KEY,
authkey, keylen);
/* 2. Active Key 설정 */
struct sctp_authkeyid keyid;
keyid.scact_keynumber = 1;
keyid.scact_assoc_id = 0;
setsockopt(fd, IPPROTO_SCTP, SCTP_AUTH_ACTIVE_KEY,
&keyid, sizeof(keyid));
/* 3. 인증할 청크 타입 설정 */
struct sctp_authchunks *chunks;
int chunklen = sizeof(struct sctp_authchunks) + 2;
chunks = malloc(chunklen);
chunks->gauth_assoc_id = 0;
chunks->gauth_chunks[0] = SCTP_CID_ASCONF; /* 193 */
chunks->gauth_chunks[1] = SCTP_CID_ASCONF_ACK; /* 128 */
setsockopt(fd, IPPROTO_SCTP, SCTP_AUTH_CHUNK,
chunks, chunklen);
UDP 캡슐화 (NAT 통과)
대부분의 NAT 장비와 방화벽은 TCP/UDP만 지원하며 SCTP 패킷을 차단합니다.
UDP 캡슐화(RFC 6951)는 SCTP 패킷을 UDP 내부에 넣어 NAT를 통과할 수 있게 합니다.
리눅스 커널은 net.sctp.encap_port sysctl로 이 기능을 제어합니다.
# UDP 캡슐화 활성화 (커널 5.11+)
$ sysctl -w net.sctp.encap_port=9899
$ sysctl -w net.sctp.udp_port=9899
/* 소켓 옵션으로 UDP 캡슐화 설정 */
struct sctp_udpencaps encaps;
encaps.sue_assoc_id = 0;
encaps.sue_address = *(struct sockaddr_storage *)&dest;
encaps.sue_port = htons(9899); /* UDP 캡슐화 포트 */
setsockopt(fd, IPPROTO_SCTP, SCTP_REMOTE_UDP_ENCAPS_PORT,
&encaps, sizeof(encaps));
커널 소스 구조
리눅스 커널의 SCTP 구현은 net/sctp/ 디렉터리에 위치합니다.
각 파일의 역할을 이해하면 커널 코드를 효율적으로 탐색할 수 있습니다.
| 소스 파일 | 역할 | 주요 함수/구조체 |
|---|---|---|
net/sctp/socket.c |
소켓 계층 인터페이스 | sctp_sendmsg(), sctp_recvmsg(), setsockopt() |
net/sctp/sm_statetable.c |
상태 머신 전이 테이블 | sctp_sm_statetable[][] |
net/sctp/sm_sideeffect.c |
상태 머신 부작용 (타이머, 청크 전송) | sctp_side_effects(), sctp_cmd_interpreter() |
net/sctp/sm_statefuns.c |
상태 전이 함수 구현 | sctp_sf_do_5_1B_init(), sctp_sf_do_5_1D_ce() |
net/sctp/sm_make_chunk.c |
청크 생성 | sctp_make_init(), sctp_make_sack() |
net/sctp/output.c |
패킷 출력/번들링 | sctp_packet_transmit(), sctp_packet_append_chunk() |
net/sctp/input.c |
패킷 입력 처리 | sctp_rcv(), sctp_v4_rcv() |
net/sctp/outqueue.c |
전송/재전송 큐 | sctp_outq_sack(), sctp_outq_flush() |
net/sctp/transport.c |
경로(transport) 관리 | sctp_transport_raise_cwnd(), sctp_transport_route() |
net/sctp/associola.c |
Association 관리 | sctp_association_new(), sctp_assoc_update_retran_path() |
net/sctp/endpointola.c |
엔드포인트 관리 | sctp_endpoint_new(), sctp_endpoint_lookup_assoc() |
net/sctp/stream.c |
스트림 관리/스케줄러 | sctp_stream_init(), sctp_sched_* |
net/sctp/auth.c |
인증(AUTH) 처리 | sctp_auth_init_hmacs(), sctp_auth_send_cid() |
net/sctp/sysctl.c |
sysctl 파라미터 | sctp_sysctl_table[] |
net/sctp/protocol.c |
프로토콜 등록/초기화 | sctp_init(), sctp_exit() |
include/net/sctp/structs.h |
핵심 자료구조 정의 | sctp_sock, sctp_association, sctp_transport |
include/net/sctp/constants.h |
상수/열거형 정의 | sctp_state, sctp_cid, sctp_event |
include/uapi/linux/sctp.h |
사용자 공간 API 정의 | 소켓 옵션, 청크 헤더, 이벤트 구조체 |
/* 패킷 수신 진입점 — net/sctp/input.c */
int sctp_rcv(struct sk_buff *skb)
{
struct sctp_association *asoc;
struct sctp_endpoint *ep;
struct sctp_chunk *chunk;
struct sctphdr *sh;
/* 1. SCTP 공통 헤더 파싱 */
sh = sctp_hdr(skb);
/* 2. CRC32c 체크섬 검증 */
if (sctp_rcv_checksum(skb) < 0)
goto discard;
/* 3. Verification Tag로 Association/Endpoint 검색 */
asoc = sctp_lookup_association(skb, &ep);
/* 4. 청크별 파싱 및 상태 머신 전달 */
chunk = sctp_inq_pop(&asoc->base.inqueue);
sctp_do_sm(SCTP_EVENT_T_CHUNK, subtype,
asoc->state, ep, asoc, chunk, GFP_ATOMIC);
/* 5. 상태 머신이 생성한 부작용(side effect) 실행 */
sctp_cmd_interpreter(SCTP_EVENT_T_CHUNK, subtype,
asoc->state, ep, asoc, chunk,
&commands);
return 0;
discard:
kfree_skb(skb);
return 0;
}
/* 상태 머신 실행 — net/sctp/sm_sideeffect.c */
/* 각 상태 전이는 Command 시퀀스를 생성하며,
* sctp_cmd_interpreter()가 이를 순서대로 실행합니다.
*
* 주요 Command 종류:
* SCTP_CMD_REPLY — 응답 청크 전송
* SCTP_CMD_SEND_PKT — 패킷 전송
* SCTP_CMD_NEW_STATE — 상태 전이
* SCTP_CMD_TIMER_START — 타이머 시작
* SCTP_CMD_TIMER_STOP — 타이머 정지
* SCTP_CMD_NEW_ASOC — 새 Association 생성
* SCTP_CMD_DEL_TCB — Association 삭제
* SCTP_CMD_EVENT_ULP — 사용자에게 이벤트 알림
* SCTP_CMD_PROCESS_SACK — SACK 처리
*/
/* 타이머 종류 — Association/Transport 단위 */
/*
* T1-init : INIT 재전송 (Association)
* T1-cookie: COOKIE-ECHO 재전송 (Association)
* T2-shutdown: SHUTDOWN 재전송 (Association)
* T3-rtx : DATA 재전송 (Transport/경로별)
* T4-rto : ASCONF 재전송 (Association)
* T5-shutdown-guard: 전체 종료 타임아웃 (Association)
* Heartbeat: 경로 활성 확인 (Transport/경로별)
* SACK : 지연 SACK 전송 (Association)
* Autoclose: 자동 종료 (Association)
*/
ftrace로 sctp_do_sm 함수를 추적하거나,
net/sctp/의 pr_debug() 메시지를 활성화하면 됩니다.
커널 빌드 시 CONFIG_SCTP_DBG_MSG=y를 설정하면 상세 디버그 로그를 볼 수 있습니다.
커널 자료구조
리눅스 커널의 SCTP 구현은 여러 계층의 자료구조로 이루어져 있습니다.
최상위 sctp_sock에서 시작하여 sctp_endpoint, sctp_association,
sctp_transport까지 이어지는 계층 구조를 이해하면 커널 코드를 읽기 쉽습니다.
/* 핵심 자료구조 요약 — include/net/sctp/structs.h */
/* sctp_sock: 소켓 계층 */
struct sctp_sock {
struct inet_sock inet; /* IPv4/IPv6 소켓 기본 */
struct sctp_endpoint *ep; /* 엔드포인트 포인터 */
struct sctp_bind_bucket *bind_hash;
__u16 default_stream; /* 기본 출력 스트림 */
__u32 default_ppid; /* 기본 PPID */
__u16 default_flags; /* 기본 전송 플래그 */
__u32 default_context; /* 기본 컨텍스트 */
__u32 default_timetolive; /* 기본 PR-SCTP TTL */
__u32 default_rcv_context; /* 기본 수신 컨텍스트 */
struct sctp_initmsg initmsg; /* 초기화 파라미터 */
/* ... */
};
/* sctp_endpoint: 엔드포인트 — 바인딩된 주소와 인증 키 관리 */
struct sctp_endpoint {
struct sctp_ep_common base;
struct hlist_node node; /* 해시 테이블 연결 */
struct list_head asocs; /* Association 목록 */
__u8 secret_key[SCTP_SECRET_SIZE]; /* Cookie HMAC 키 */
__u8 *auth_hmacs_list; /* 지원 HMAC 목록 */
__u8 *auth_chunk_list; /* 인증 청크 목록 */
/* ... */
};
/* sctp_transport: 경로 (원격 주소 하나에 대응) */
struct sctp_transport {
struct list_head transports; /* Association의 경로 목록 */
union sctp_addr ipaddr; /* 원격 주소 */
__u8 state; /* ACTIVE/INACTIVE/PF */
__u32 cwnd; /* 혼잡 윈도우 */
__u32 ssthresh; /* SS 임계값 */
unsigned long rto; /* 재전송 타임아웃 */
__u32 srtt; /* Smoothed RTT */
__u32 pathmtu; /* 경로 MTU */
__u16 error_count; /* 오류 카운터 */
__u16 pathmaxrxt; /* 경로 최대 재전송 */
struct timer_list T3_rtx_timer; /* 재전송 타이머 */
struct timer_list hb_timer; /* Heartbeat 타이머 */
/* ... */
};
| 자료구조 | 소스 위치 | 역할 | 주요 필드 |
|---|---|---|---|
sctp_sock |
include/net/sctp/structs.h |
소켓 계층, 사용자 설정 보관 | ep, default_stream, initmsg |
sctp_endpoint |
include/net/sctp/structs.h |
바인딩 주소, 인증 키, Association 목록 | asocs, secret_key, auth_* |
sctp_association |
include/net/sctp/structs.h |
Association 상태, 스트림, 전송 큐 | state, stream, outqueue, peer |
sctp_transport |
include/net/sctp/structs.h |
원격 주소별 경로 상태 | cwnd, ssthresh, rto, error_count |
sctp_stream |
include/net/sctp/structs.h |
입출력 스트림 관리 | in[], out[], outcnt, incnt |
sctp_outq |
include/net/sctp/structs.h |
전송/재전송 큐 | out_chunk_list, retransmit, sacked |
sctp_chunk |
include/net/sctp/structs.h |
청크 래퍼 (sk_buff 포함) | skb, chunk_hdr, transport, tsn |
이벤트 알림
SCTP는 Association 상태 변화, 경로 상태 변화, 오류 발생 등의 이벤트를
애플리케이션에 알림으로 전달할 수 있습니다. SCTP_EVENTS 소켓 옵션으로
수신할 이벤트를 선택하면, recvmsg()의 ancillary data(cmsg)나
메시지 플래그(MSG_NOTIFICATION)를 통해 이벤트를 수신합니다.
| 이벤트 | 타입 상수 | 설명 | 주요 정보 |
|---|---|---|---|
| Association 변화 | SCTP_ASSOC_CHANGE |
Association 상태 전이 | state (COMM_UP, COMM_LOST, RESTART, ...) |
| 경로 주소 변화 | SCTP_PEER_ADDR_CHANGE |
원격 주소 상태 변경 | 주소, state (ADDR_AVAILABLE, ADDR_UNREACHABLE) |
| 원격 오류 | SCTP_REMOTE_ERROR |
상대가 ERROR 청크 전송 | 에러 원인 코드 |
| 전송 실패 | SCTP_SEND_FAILED |
메시지 전달 실패 | 실패한 메시지 데이터, 오류 코드 |
| 종료 이벤트 | SCTP_SHUTDOWN_EVENT |
상대가 SHUTDOWN 시작 | Association ID |
| 적응 레이어 표시 | SCTP_ADAPTATION_INDICATION |
상대의 Adaptation Layer 정보 | Adaptation code |
| 부분 전달 | SCTP_PARTIAL_DELIVERY_EVENT |
부분 전달 중단 | 중단 원인 |
| 인증 이벤트 | SCTP_AUTHENTICATION_EVENT |
인증 관련 이벤트 | 성공/실패, Key ID |
| 송신자 건조 | SCTP_SENDER_DRY_EVENT |
전송 큐 비어짐 | Association ID |
/* 이벤트 알림 구독 설정 */
struct sctp_event_subscribe events;
memset(&events, 0, sizeof(events));
events.sctp_data_io_event = 1; /* DATA 수신 정보 */
events.sctp_association_event = 1; /* Association 변화 */
events.sctp_address_event = 1; /* 경로 상태 변화 */
events.sctp_send_failure_event = 1; /* 전송 실패 */
events.sctp_peer_error_event = 1; /* 원격 오류 */
events.sctp_shutdown_event = 1; /* 종료 이벤트 */
events.sctp_sender_dry_event = 1; /* 전송 큐 비어짐 */
setsockopt(fd, IPPROTO_SCTP, SCTP_EVENTS, &events, sizeof(events));
/* 이벤트 수신 루프 */
while (1) {
char buf[4096];
struct sctp_sndrcvinfo sinfo;
int flags = 0;
int n = sctp_recvmsg(fd, buf, sizeof(buf),
NULL, 0, &sinfo, &flags);
if (flags & MSG_NOTIFICATION) {
/* 알림 메시지 처리 */
union sctp_notification *snp =
(union sctp_notification *)buf;
switch (snp->sn_header.sn_type) {
case SCTP_ASSOC_CHANGE: {
struct sctp_assoc_change *sac = &snp->sn_assoc_change;
printf("Association %s (assoc=%u)\\n",
sac->sac_state == SCTP_COMM_UP ? "UP" : "DOWN",
sac->sac_assoc_id);
break;
}
case SCTP_PEER_ADDR_CHANGE: {
struct sctp_paddr_change *spc = &snp->sn_paddr_change;
printf("Path state: %d\\n", spc->spc_state);
break;
}
case SCTP_SHUTDOWN_EVENT:
printf("Peer initiated shutdown\\n");
break;
}
} else {
/* 일반 데이터 처리 */
printf("Data on stream %u: %.*s\\n",
sinfo.sinfo_stream, n, buf);
}
}
SCTP vs TCP vs UDP 상세 비교
전송 프로토콜 선택은 애플리케이션의 요구사항에 따라 달라집니다. 다음은 세 프로토콜의 상세한 비교입니다.
| 항목 | TCP | UDP | SCTP |
|---|---|---|---|
| IP 프로토콜 번호 | 6 | 17 | 132 |
| 데이터 단위 | 바이트 스트림 | 데이터그램 | 메시지 (청크) |
| 연결 설정 | 3-way handshake | 없음 | 4-way handshake |
| 연결 종료 | 4-way (FIN/ACK) | 없음 | 3-way (SHUTDOWN) |
| 멀티호밍 | 불가능 | 불가능 | 지원 (자동 failover) |
| 멀티스트리밍 | 불가능 | 불가능 | 지원 (독립 스트림) |
| HoL Blocking | 발생 | 해당 없음 | 스트림 독립으로 방지 |
| 메시지 경계 | 보존하지 않음 | 보존 | 보존 |
| 혼잡 제어 | 단일 경로 | 없음 | 경로별 독립 |
| 부분 신뢰성 | 불가능 | 해당 없음 | PR-SCTP 지원 |
| NAT 통과 | 자연 지원 | 자연 지원 | UDP 캡슐화 필요 |
| 커널 기본 지원 | 항상 내장 | 항상 내장 | 모듈 로드 필요 |
| 대표 사용 사례 | HTTP, SSH, DB | DNS, 게임, VoIP | 텔레콤, WebRTC |
| 사용 사례 | 권장 프로토콜 | 이유 |
|---|---|---|
| 웹 서비스 (HTTP/HTTPS) | TCP (QUIC) | 범용 지원, NAT 통과 용이 |
| DNS 조회 | UDP | 단일 요청/응답, 오버헤드 최소 |
| 텔레콤 시그널링 (Diameter, SS7) | SCTP | 멀티호밍 failover, 메시지 경계 필수 |
| WebRTC DataChannel | SCTP (over DTLS/UDP) | 멀티스트리밍, 부분 신뢰성 |
| 실시간 게임 | UDP / SCTP | SCTP PR-SCTP로 오래된 데이터 폐기 |
| 고가용성 클러스터 | SCTP | 멀티호밍으로 경로 이중화 |
| 데이터베이스 복제 | TCP | 완전한 신뢰성, 순서 보장 필요 |
| VoIP 시그널링 (SIP) | TCP / SCTP | SCTP의 메시지 경계가 SIP에 적합 |
/* 완전한 one-to-one SCTP 서버 예제 */
#include <stdio.h>
#include <string.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <netinet/sctp.h>
int main(void)
{
int listen_fd, conn_fd;
struct sockaddr_in servaddr;
struct sctp_initmsg initmsg;
struct sctp_event_subscribe events;
char buf[1024];
/* 소켓 생성: one-to-one 스타일 */
listen_fd = socket(AF_INET, SOCK_STREAM, IPPROTO_SCTP);
/* 주소 바인딩 */
memset(&servaddr, 0, sizeof(servaddr));
servaddr.sin_family = AF_INET;
servaddr.sin_addr.s_addr = htonl(INADDR_ANY);
servaddr.sin_port = htons(9999);
bind(listen_fd, (struct sockaddr *)&servaddr,
sizeof(servaddr));
/* 스트림 수 설정 */
memset(&initmsg, 0, sizeof(initmsg));
initmsg.sinit_num_ostreams = 5;
initmsg.sinit_max_instreams = 5;
setsockopt(listen_fd, IPPROTO_SCTP, SCTP_INITMSG,
&initmsg, sizeof(initmsg));
/* 이벤트 구독 */
memset(&events, 0, sizeof(events));
events.sctp_data_io_event = 1;
setsockopt(listen_fd, IPPROTO_SCTP, SCTP_EVENTS,
&events, sizeof(events));
/* 리슨 시작 */
listen(listen_fd, 5);
printf("SCTP server listening on port 9999\\n");
/* 클라이언트 수락 */
conn_fd = accept(listen_fd, NULL, NULL);
/* 데이터 수신 루프 */
struct sctp_sndrcvinfo sinfo;
int flags = 0;
int n;
while ((n = sctp_recvmsg(conn_fd, buf, sizeof(buf),
NULL, 0, &sinfo, &flags)) > 0) {
printf("stream=%u: %.*s\\n", sinfo.sinfo_stream, n, buf);
/* 에코: 같은 스트림으로 응답 */
sctp_sendmsg(conn_fd, buf, n, NULL, 0,
sinfo.sinfo_ppid, 0,
sinfo.sinfo_stream, 0, 0);
}
close(conn_fd);
close(listen_fd);
return 0;
}
# SCTP 서버 예제 컴파일 및 실행
$ gcc -o sctp_server sctp_server.c -lsctp
$ ./sctp_server
# 클라이언트 테스트 (lksctp-tools)
$ sctp_test -H 127.0.0.1 -h 127.0.0.1 -P 9999 -p 9999 -s
운영 튜닝 포인트
SCTP의 동작은 sysctl 파라미터와 소켓 옵션으로 세밀하게 조정할 수 있습니다.
다음은 주요 sysctl 파라미터와 권장 설정입니다.
| sysctl 파라미터 | 기본값 | 설명 | 권장 (고가용성) |
|---|---|---|---|
net.sctp.rto_initial |
3000ms | 초기 RTO | 1000ms |
net.sctp.rto_min |
1000ms | RTO 최소값 | 200ms |
net.sctp.rto_max |
60000ms | RTO 최대값 | 10000ms |
net.sctp.hb_interval |
30000ms | Heartbeat 간격 | 10000ms |
net.sctp.path_max_retrans |
5 | 경로 최대 재전송 횟수 | 3 |
net.sctp.max_retrans_association |
10 | Association 최대 재전송 | 10 |
net.sctp.max_retrans_init |
8 | INIT 최대 재전송 | 4 |
net.sctp.valid_cookie_life |
60000ms | State Cookie 유효 기간 | 60000ms |
net.sctp.sack_timeout |
200ms | SACK 지연 전송 타임아웃 | 50ms (저지연) |
net.sctp.addip_enable |
0 | ASCONF (동적 주소 변경) | 1 (필요시) |
net.sctp.prsctp_enable |
1 | PR-SCTP 지원 | 1 |
net.sctp.auth_enable |
0 | AUTH 청크 지원 | 1 (보안 필요시) |
net.sctp.encap_port |
0 | UDP 캡슐화 포트 | 9899 (NAT 통과시) |
# 고가용성 환경 튜닝 예시
# Heartbeat 간격 단축 → 빠른 장애 감지
$ sysctl -w net.sctp.hb_interval=10000
# RTO 최소/최대 조정 → 빠른 재전송
$ sysctl -w net.sctp.rto_min=200
$ sysctl -w net.sctp.rto_max=10000
$ sysctl -w net.sctp.rto_initial=1000
# 경로 재전송 한계 축소 → 빠른 failover
$ sysctl -w net.sctp.path_max_retrans=3
# SACK 지연 축소 → 빠른 확인응답
$ sysctl -w net.sctp.sack_timeout=50
# 동적 주소 변경 허용
$ sysctl -w net.sctp.addip_enable=1
# PR-SCTP 확인
$ sysctl net.sctp.prsctp_enable
# 영구 적용: /etc/sysctl.d/sctp.conf
$ cat > /etc/sysctl.d/sctp.conf <<EOF
net.sctp.rto_min = 200
net.sctp.rto_max = 10000
net.sctp.rto_initial = 1000
net.sctp.hb_interval = 10000
net.sctp.path_max_retrans = 3
net.sctp.sack_timeout = 50
EOF
$ sysctl -p /etc/sysctl.d/sctp.conf
트러블슈팅
# 1. 커널 모듈 확인
$ lsmod | grep sctp
sctp 413696 0
# 모듈이 없으면 로드
$ modprobe sctp
# 2. SCTP 소켓 상태 확인
$ ss -n -A sctp
LISTEN 0 5 *:9999 *:*
ESTAB 0 0 10.0.0.1:9999 10.0.0.2:9999
# 3. sysctl 현재 값 확인
$ sysctl -a | grep sctp
# 4. 방화벽 규칙 확인 (SCTP 허용)
$ iptables -L -n | grep sctp
$ nft list ruleset | grep sctp
# 5. tcpdump로 SCTP 패킷 캡처
$ tcpdump -i eth0 -n sctp -vv
# 6. sctp_test 도구 (lksctp-tools)
# 서버
$ sctp_test -H 10.0.0.2 -P 9999 -l
# 클라이언트
$ sctp_test -H 10.0.0.1 -h 10.0.0.2 -P 9999 -p 9999 -s
# 7. /proc 인터페이스
$ cat /proc/net/sctp/snmp
$ cat /proc/net/sctp/eps # 엔드포인트 목록
$ cat /proc/net/sctp/assocs # Association 목록
$ cat /proc/net/sctp/remaddr # 원격 주소 목록
# 8. Wireshark SCTP 분석
# Wireshark에서 "sctp" 필터로 패킷 분석 가능
# Statistics → SCTP → Show All Associations 메뉴 활용
| 증상 | 원인 | 해결 방법 |
|---|---|---|
| INIT 후 응답 없음 | 방화벽이 SCTP(proto 132) 차단 | iptables -A INPUT -p sctp -j ACCEPT |
| INIT-ACK 수신 후 COOKIE-ECHO 실패 | NAT가 SCTP를 지원하지 않음 | UDP 캡슐화 사용 (net.sctp.encap_port) |
| 빈번한 경로 전환 | Heartbeat 간격이 너무 짧음 | hb_interval 증가 |
| Association 비정상 종료 (ABORT) | State Cookie 만료 | valid_cookie_life 증가 |
| 높은 재전송률 | RTO가 네트워크 지연에 비해 짧음 | rto_min 조정 |
| 모듈 로드 실패 | CONFIG_IP_SCTP가 비활성 | 커널 재빌드 또는 배포판 모듈 설치 |
| sctp_sendmsg 실패 (ENOMEM) | 수신 윈도우(a-rwnd) 소진 | 수신 측 처리 속도 개선, 버퍼 증가 |
/proc/net/sctp/assocs에서 각 Association의 상태,
스트림 수, 원격 주소 수를 확인할 수 있습니다. Wireshark의 SCTP 분석 기능은
Association별 TSN 그래프, 재전송 통계, 경로별 RTT를 시각화해줍니다.
SCTP 통계와 모니터링
커널은 /proc/net/sctp/snmp에서 SCTP MIB(RFC 3873) 기반 통계를 제공합니다.
이 통계를 모니터링하면 SCTP 스택의 건강 상태를 파악하고 성능 문제를 진단할 수 있습니다.
| MIB 카운터 | 설명 | 비정상 증가 시 의미 |
|---|---|---|
SctpCurrEstab |
현재 ESTABLISHED Association 수 | 예상보다 높으면 연결 누수 의심 |
SctpActiveEstabs |
클라이언트 측에서 성공한 Association 수 | 정상 활동 지표 |
SctpPassiveEstabs |
서버 측에서 수락한 Association 수 | 정상 활동 지표 |
SctpAborteds |
ABORT로 종료된 Association 수 | 높으면 비정상 종료 빈발 |
SctpShutdowns |
정상 종료(SHUTDOWN)된 Association 수 | 정상 활동 지표 |
SctpOutOfBlues |
미식별 Association의 패킷 수 | 높으면 스캔 공격 또는 설정 오류 |
SctpChecksumErrors |
체크섬 오류 패킷 수 | 높으면 네트워크 장비 문제 |
SctpT1InitExpireds |
T1-init 타이머 만료 횟수 | 높으면 서버 도달 불가 |
SctpT3RtxExpireds |
T3-rtx 타이머 만료 횟수 | 높으면 네트워크 손실 심함 |
SctpInSCTPPacks |
수신한 SCTP 패킷 총 수 | 처리량 지표 |
SctpOutSCTPPacks |
전송한 SCTP 패킷 총 수 | 처리량 지표 |
SctpFragUsrMsgs |
단편화된 사용자 메시지 수 | 높으면 메시지 크기 조정 권장 |
SctpReasmUsrMsgs |
재조립된 사용자 메시지 수 | 단편화 대응 지표 |
SctpInCtrlChunks |
수신한 제어 청크 수 | 프로토콜 오버헤드 지표 |
SctpOutCtrlChunks |
전송한 제어 청크 수 | 프로토콜 오버헤드 지표 |
# SCTP SNMP 통계 확인
$ cat /proc/net/sctp/snmp
SctpCurrEstab 3
SctpActiveEstabs 15
SctpPassiveEstabs 12
SctpAborteds 2
SctpShutdowns 10
SctpOutOfBlues 0
SctpChecksumErrors 0
SctpOutCtrlChunks 1247
SctpOutOrderChunks 5832
SctpOutUnorderChunks 0
SctpInCtrlChunks 1251
SctpInOrderChunks 5828
SctpInUnorderChunks 0
SctpFragUsrMsgs 47
SctpReasmUsrMsgs 47
SctpInSCTPPacks 7079
SctpOutSCTPPacks 7079
SctpT1InitExpireds 0
SctpT3RtxExpireds 3
# Association 목록 확인
$ cat /proc/net/sctp/assocs
ASSOC SOCK STY SST ST HBKT ASSOC-ID TX_QUEUE RX_QUEUE UID INODE LPORT RPORT
ffff... ffff... 2 1 3 0 1 0 0 1000 12345 9999 9999
# 엔드포인트 목록 확인
$ cat /proc/net/sctp/eps
ENDPT SOCK STY SST HBKT LPORT UID INODE LADDRS
ffff... ffff... 2 10 0 9999 1000 12345 10.0.0.1 10.0.1.1
# 원격 주소 목록 확인
$ cat /proc/net/sctp/remaddr
ADDR ASSOC_ID HB_ACT RTO MAX_PATH_RTX REM_ADDR_RTX START STATE
10.0.0.2 1 1 1000 5 0 1 ACTIVE
10.0.1.2 1 1 3000 5 0 1 ACTIVE
# nstat로 SCTP 통계 변화 모니터링 (실시간)
$ nstat -z | grep Sctp
SctpActiveEstabs 2 0.0
SctpPassiveEstabs 1 0.0
SctpOutSCTPPacks 147 0.0
SctpInSCTPPacks 152 0.0
# ss 명령어로 SCTP Association 상세 정보
$ ss -n -A sctp -i
State Recv-Q Send-Q Local Address:Port Peer Address:Port
ESTAB 0 0 10.0.0.1:9999 10.0.0.2:9999
skmem:(r0,rb212992,t0,tb212992,f0,w0,o0,bl0,d0)
# eBPF를 사용한 SCTP 추적 (bpftrace)
$ bpftrace -e 'kprobe:sctp_rcv { @pkts = count(); }'
$ bpftrace -e 'kprobe:sctp_do_sm { @events[arg1] = count(); }'
/proc/net/sctp/snmp의 카운터는 전역 통계입니다.
특정 Association의 통계가 필요하면 SCTP_GET_ASSOC_STATS 소켓 옵션을 사용하세요.
이를 통해 Association별 전송/수신 바이트, 재전송 횟수, 최대 RTO 등을 확인할 수 있습니다.
SCTP 스트림 스케줄러
RFC 8260은 SCTP 스트림 스케줄링과 사용자 메시지 인터리빙(User Message Interleaving)을 정의합니다. 기본적으로 SCTP는 FIFO(선입선출) 순서로 청크를 전송하지만, 여러 스트림에 동시에 큰 메시지가 있으면 특정 스트림이 다른 스트림을 차단할 수 있습니다. 스트림 스케줄러는 이 문제를 해결합니다.
| 스케줄러 | 커널 상수 | 동작 | 사용 사례 |
|---|---|---|---|
| FIFO (기본) | SCTP_SS_FCFS |
enqueue 순서대로 전송 | 단일 스트림 또는 메시지가 작을 때 |
| Round-Robin | SCTP_SS_RR |
활성 스트림 순환 | 균등 대역폭 분배 |
| Round-Robin per Packet | SCTP_SS_RR_PKT |
패킷 단위 라운드 로빈 | 더 세밀한 공정성 |
| Priority | SCTP_SS_PRIO |
스트림 우선순위에 따라 전송 | 제어 채널 우선 전송 |
| Fair Capacity | SCTP_SS_FC |
공정 용량 기반 (WFQ 유사) | 가중치 기반 대역폭 분배 |
| Weighted Fair Queue | SCTP_SS_WFQ |
가중 공정 큐잉 | QoS 차등 스트림 |
/* 스트림 스케줄러 설정 */
struct sctp_assoc_value av;
av.assoc_id = 0;
av.assoc_value = SCTP_SS_RR; /* Round-Robin */
setsockopt(fd, IPPROTO_SCTP, SCTP_STREAM_SCHEDULER,
&av, sizeof(av));
/* 스트림 우선순위 설정 (Priority 스케줄러용) */
struct sctp_stream_value sv;
sv.assoc_id = 0;
sv.stream_id = 0; /* 스트림 0 */
sv.stream_value = 0; /* 최고 우선순위 (0이 가장 높음) */
setsockopt(fd, IPPROTO_SCTP, SCTP_STREAM_SCHEDULER_VALUE,
&sv, sizeof(sv));
sv.stream_id = 1;
sv.stream_value = 10; /* 낮은 우선순위 */
setsockopt(fd, IPPROTO_SCTP, SCTP_STREAM_SCHEDULER_VALUE,
&sv, sizeof(sv));
/* I-DATA 인터리빙 활성화 (RFC 8260) */
int enable = 1;
setsockopt(fd, IPPROTO_SCTP, SCTP_INTERLEAVING_SUPPORTED,
&enable, sizeof(enable));
setsockopt(fd, IPPROTO_SCTP, SCTP_FRAGMENT_INTERLEAVE,
&enable, sizeof(enable));
/* 커널 스트림 스케줄러 인터페이스 — net/sctp/stream_sched.c */
struct sctp_sched_ops {
/* 스트림에 청크 enqueue */
int (*enqueue)(struct sctp_outq *q,
struct sctp_datamsg *msg);
/* 다음 전송할 청크 선택 */
struct sctp_chunk *(*dequeue)(
struct sctp_outq *q);
/* 스트림에서 청크 제거 */
void (*dequeue_done)(struct sctp_outq *q,
struct sctp_chunk *ch);
/* 스케줄러 초기화/해제 */
int (*init)(struct sctp_stream *stream);
void (*free)(struct sctp_stream *stream);
/* 스트림 초기화/해제 */
int (*init_sid)(struct sctp_stream *stream,
__u16 sid, gfp_t gfp);
void (*free_sid)(struct sctp_stream *stream,
__u16 sid);
};
/* I-DATA 청크 구조 — DATA 청크와의 차이 */
/*
* DATA 청크: Type(0x00) | Flags | Length | TSN | SID | SSN | PPID | Data
* I-DATA 청크: Type(0x40) | Flags | Length | TSN | SID | MID | PPID/FSID | Data
*
* 차이점:
* - SSN(Stream Sequence Number, 16비트) → MID(Message ID, 32비트)
* - 첫 조각: PPID 포함, 후속 조각: FSN(Fragment Sequence Number)
* - MID가 32비트이므로 순서 번호 고갈 문제 해결
* - 서로 다른 메시지의 조각을 자유롭게 인터리빙 가능
*/
Dynamic Address Reconfiguration (ASCONF)
ASCONF(Address Configuration Change)(RFC 5061)는 Association이 활성 상태인 동안 IP 주소를 동적으로 추가하거나 삭제할 수 있게 해주는 확장입니다. 모바일 환경에서 네트워크 인터페이스가 변경되거나, 서버 마이그레이션 시 중단 없이 주소를 전환하는 데 핵심적입니다.
| ASCONF 파라미터 | 코드 | 설명 | 조건 |
|---|---|---|---|
| Add IP Address | 0xC001 |
Association에 새 IP 주소 추가 | 양쪽 모두 ASCONF 지원 필수 |
| Delete IP Address | 0xC002 |
Association에서 IP 주소 제거 | 마지막 주소는 삭제 불가 |
| Set Primary Address | 0xC004 |
Primary 경로 변경 요청 | 상대가 해당 주소를 갖고 있어야 |
| Success Indication | 0xC005 |
ASCONF-ACK 성공 응답 | — |
| Error Cause | 0xC006 |
ASCONF-ACK 오류 응답 | 사유 코드 포함 |
/* ASCONF 활성화 및 동적 주소 변경 */
/* 1. ASCONF 기능 활성화 (커널 sysctl) */
/* sysctl -w net.sctp.addip_enable=1 */
/* sysctl -w net.sctp.addip_noauth_enable=1 (테스트용, AUTH 없이 허용) */
/* 2. sctp_bindx: 동적으로 주소 추가/삭제 */
struct sockaddr_in new_addr;
new_addr.sin_family = AF_INET;
new_addr.sin_port = htons(9999);
inet_pton(AF_INET, "10.0.1.1", &new_addr.sin_addr);
/* Association 활성 중 주소 추가 → ASCONF 자동 전송 */
sctp_bindx(fd, (struct sockaddr *)&new_addr, 1,
SCTP_BINDX_ADD_ADDR);
/* Association 활성 중 주소 제거 → ASCONF 자동 전송 */
struct sockaddr_in old_addr;
old_addr.sin_family = AF_INET;
old_addr.sin_port = htons(9999);
inet_pton(AF_INET, "10.0.0.1", &old_addr.sin_addr);
sctp_bindx(fd, (struct sockaddr *)&old_addr, 1,
SCTP_BINDX_REM_ADDR);
/* 3. Primary 경로 변경 */
struct sctp_setpeerprim prim;
prim.sspp_assoc_id = 0;
prim.sspp_addr = *(struct sockaddr_storage *)&new_addr;
setsockopt(fd, IPPROTO_SCTP, SCTP_SET_PEER_PRIMARY_ADDR,
&prim, sizeof(prim));
/* 커널 ASCONF 처리 — net/sctp/sm_make_chunk.c */
/*
* sctp_process_asconf(): ASCONF 청크 수신 처리
* 1. 각 ASCONF 파라미터 순회
* 2. Add IP → sctp_assoc_add_peer() 호출
* 3. Delete IP → sctp_assoc_del_peer() 호출
* 4. Set Primary → 상대의 primary 경로 변경 요청
* 5. 결과를 ASCONF-ACK로 응답
*
* 보안 주의: ASCONF는 AUTH 청크로 인증해야 안전
* (net.sctp.addip_noauth_enable=0 권장)
* 인증 없이 ASCONF를 허용하면 공격자가
* Association을 하이재킹할 수 있음
*/
/* T4-rto 타이머: ASCONF 재전송 */
/*
* ASCONF 전송 후 ASCONF-ACK를 받지 못하면
* T4-rto 타이머 만료 → ASCONF 재전송
* 최대 재전송 횟수: association_max_retrans
* 모두 실패 시 Association 중단(ABORT)
*/
net.sctp.addip_noauth_enable=1은 테스트 환경에서만 사용하세요.
운영 환경에서는 반드시 AUTH 청크(RFC 4895)와 함께 사용하여 ASCONF 인증을 활성화해야 합니다.
인증 없는 ASCONF는 공격자가 가짜 주소를 삽입하여 트래픽을 가로챌 수 있습니다.
Heartbeat와 PF(Potentially Failed) 상태
SCTP는 Heartbeat 메커니즘으로 각 경로의 활성 상태를 확인합니다. 비활성 경로(데이터 전송이 없는 경로)에 주기적으로 HEARTBEAT 청크를 보내고, HEARTBEAT-ACK를 받으면 경로가 살아 있음을 확인합니다. 리눅스 커널은 RFC 7829의 PF(Potentially Failed) 상태를 추가로 구현하여, 완전 실패 선언 전에 중간 상태를 두어 더 빠른 failover를 가능하게 합니다.
| 경로 상태 | 커널 상수 | 조건 | 동작 |
|---|---|---|---|
| ACTIVE | SCTP_ACTIVE |
error_count < pf_retrans | 정상 데이터 전송 가능 |
| PF | SCTP_PF |
pf_retrans ≤ error_count < pathmaxrxt | 데이터 전송 보류, Heartbeat만 전송 |
| INACTIVE | SCTP_INACTIVE |
error_count ≥ pathmaxrxt | 사용 불가, Heartbeat로 복구 시도 |
| UNCONFIRMED | SCTP_UNCONFIRMED |
ASCONF로 추가된 미확인 경로 | Heartbeat 확인 후 ACTIVE 전환 |
/* PF(Potentially Failed) 상태 설정 — 소켓 옵션 */
/* pf_retrans: PF 상태로 전환하는 오류 임계값 */
/* error_count가 이 값에 도달하면 경로를 PF로 표시 */
struct sctp_assoc_value pf;
pf.assoc_id = 0;
pf.assoc_value = 1; /* 1회 실패 시 PF 전환 (빠른 감지) */
setsockopt(fd, IPPROTO_SCTP, SCTP_PEER_ADDR_THLDS,
&pf, sizeof(pf));
/* 또는: 경로별 PF 임계값 설정 */
struct sctp_paddrthlds thlds;
thlds.spt_assoc_id = 0;
thlds.spt_address = *(struct sockaddr_storage *)&peer_addr;
thlds.spt_pathmaxrxt = 5; /* INACTIVE 임계값 */
thlds.spt_pathpfthld = 1; /* PF 임계값 */
setsockopt(fd, IPPROTO_SCTP, SCTP_PEER_ADDR_THLDS_V2,
&thlds, sizeof(thlds));
/* Heartbeat 커널 구현 — net/sctp/transport.c */
/* Heartbeat 타이머 콜백 */
static void sctp_transport_timeout(struct timer_list *t)
{
struct sctp_transport *transport =
from_timer(transport, t, hb_timer);
/*
* 1. HB 타이머 만료 → HEARTBEAT 청크 생성
* 2. 상태 머신에 SCTP_EVENT_TIMEOUT_HB 이벤트 전달
* 3. sctp_sf_sendbeat_8_3() → HEARTBEAT 청크 전송
* 4. 현재 시각을 HB에 포함 (RTT 측정용)
*/
}
/* Heartbeat 간격 계산 */
/*
* HB 간격 = hb_interval + RTO + 랜덤 지터
* 랜덤 지터: 0 ~ RTO 사이의 값 (동시 HB 방지)
*
* 경로에 활성 데이터 전송이 있으면 HB 생략
* (데이터 ACK가 경로 확인 역할을 대신함)
*/
/* PF 상태 전환 — net/sctp/transport.c */
static void sctp_transport_update_state(
struct sctp_transport *transport)
{
/* error_count 증가 시 호출 */
if (transport->error_count >= transport->pathmaxrxt) {
/* INACTIVE: 완전 실패 */
transport->state = SCTP_INACTIVE;
/* 이 경로가 primary면 다른 ACTIVE 경로로 전환 */
} else if (transport->error_count >= transport->pf_retrans) {
/* PF: 잠재적 실패 → 새 데이터는 다른 경로로 */
transport->state = SCTP_PF;
/* PF 경로에는 HB만 계속 전송 */
}
}
/* HB-ACK 수신 시 경로 복구 */
/*
* sctp_sf_backbeat_8_3():
* 1. error_count = 0 으로 리셋
* 2. 상태를 ACTIVE로 복원
* 3. HB에 포함된 시각으로 RTT 계산
* 4. RTO 갱신: RTO = SRTT + 4*RTTVAR
*/
pathmaxrxt만큼의 타임아웃을 모두 기다려야 합니다. PF 상태를 사용하면
1~2회 실패만으로 즉시 대안 경로를 사용하면서, 원래 경로의 복구를 Heartbeat로 계속 확인합니다.
텔레콤 환경에서 failover 시간을 수십 초에서 수백 밀리초로 단축할 수 있습니다.
SCTP over IPv6
SCTP는 IPv6를 완벽히 지원하며, IPv4/IPv6 듀얼 스택 환경에서 하나의 Association에 두 프로토콜 패밀리의 주소를 동시에 사용할 수 있습니다. 이는 TCP에서는 불가능한 SCTP 고유의 멀티호밍 기능입니다.
| 기능 | IPv4 전용 | IPv6 전용 | 듀얼 스택 |
|---|---|---|---|
| 소켓 생성 | AF_INET |
AF_INET6 |
AF_INET6 + IPV6_V6ONLY=0 |
| 멀티호밍 | IPv4 주소끼리만 | IPv6 주소끼리만 | IPv4 + IPv6 주소 혼합 가능 |
| INIT 주소 교환 | IPv4 Address 파라미터 | IPv6 Address 파라미터 | 두 종류 모두 포함 |
| 경로 failover | IPv4 ↔ IPv4 | IPv6 ↔ IPv6 | IPv4 ↔ IPv6 교차 가능 |
| 커널 구현 | net/sctp/protocol.c |
net/sctp/ipv6.c |
두 파일 모두 사용 |
/* SCTP 듀얼 스택 서버: IPv4 + IPv6 동시 수신 */
int fd = socket(AF_INET6, SOCK_SEQPACKET, IPPROTO_SCTP);
/* 듀얼 스택 활성화 (IPv4-mapped IPv6) */
int v6only = 0;
setsockopt(fd, IPPROTO_IPV6, IPV6_V6ONLY, &v6only, sizeof(v6only));
/* IPv6 + IPv4 주소 모두 바인딩 */
struct sockaddr_in6 addr6 = {
.sin6_family = AF_INET6,
.sin6_port = htons(9999),
.sin6_addr = in6addr_any, /* :: */
};
bind(fd, (struct sockaddr *)&addr6, sizeof(addr6));
/* 멀티호밍: 추가 IPv4 주소 바인딩 */
struct sockaddr_in addr4 = {
.sin_family = AF_INET,
.sin_port = htons(9999),
};
inet_pton(AF_INET, "10.0.0.1", &addr4.sin_addr);
sctp_bindx(fd, (struct sockaddr *)&addr4, 1,
SCTP_BINDX_ADD_ADDR);
/* 추가 IPv6 주소 바인딩 */
struct sockaddr_in6 addr6_2 = {
.sin6_family = AF_INET6,
.sin6_port = htons(9999),
};
inet_pton(AF_INET6, "2001:db8::1", &addr6_2.sin6_addr);
sctp_bindx(fd, (struct sockaddr *)&addr6_2, 1,
SCTP_BINDX_ADD_ADDR);
listen(fd, 5);
/* 이제 IPv4와 IPv6 클라이언트 모두 연결 가능 */
/* failover 시 IPv4 경로 ↔ IPv6 경로 교차 전환 가능 */
/* 커널 IPv6 SCTP 구현 — net/sctp/ipv6.c */
/* IPv6 전용 주소 비교 함수 */
static int sctp_v6_cmp_addr(
const union sctp_addr *addr1,
const union sctp_addr *addr2)
{
/* IPv6 주소 비교: 스코프 ID도 함께 비교 */
/* link-local 주소의 경우 인터페이스 인덱스가 다르면 다른 주소 */
}
/* IPv6 scope 기반 주소 선택 */
/*
* SCTP는 INIT에 로컬 주소를 포함할 때 scope를 고려:
* - link-local (fe80::) : 같은 링크에서만 사용
* - site-local (fec0::, deprecated) : 사이트 내부용
* - global (2000:: ~ 3fff::) : 전역 도달 가능
*
* 원격 주소가 global이면 link-local 주소는 INIT에 포함하지 않음
* (도달 불가능한 주소를 광고하면 failover 실패)
*/
/* 듀얼 스택 프로토콜 등록 */
static struct sctp_af sctp_af_inet6 = {
.sa_family = AF_INET6,
.sctp_xmit = sctp_v6_xmit,
.setsockopt = sctp_v6_setsockopt,
.getsockopt = sctp_v6_getsockopt,
.addr_valid = sctp_v6_addr_valid,
.cmp_addr = sctp_v6_cmp_addr,
.scope = sctp_v6_scope,
/* ... */
};
sctp_bindx로 글로벌 주소만 바인딩하는 것이 안전합니다.
SCTP 보안: 공격 방어 메커니즘
SCTP는 TCP의 보안 약점을 학습하여 설계 단계에서 여러 공격 방어 메커니즘을 내장했습니다. 4-way handshake의 State Cookie는 SYN Flood 방어, Verification Tag는 세션 하이재킹 방어, CRC32c 체크섬은 패킷 위조 감지에 각각 기여합니다.
| 공격 유형 | TCP 취약성 | SCTP 방어 | 추가 보호 |
|---|---|---|---|
| Flood 공격 (DoS) | SYN → 반열림 연결 자원 소진 | State Cookie → 서버 상태 0 | INIT에서 서버 자원 할당 없음 |
| 세션 하이재킹 | 시퀀스 번호 32비트 예측 | VTag 32비트 + TSN 32비트 = 64비트 | AUTH 청크로 HMAC 인증 추가 |
| 연결 리셋 공격 | RST 패킷 위조 → 연결 끊김 | ABORT에도 VTag 필수 | VTag 불일치 → 무시 |
| 패킷 위조 | 약한 16비트 체크섬 | CRC32c (32비트) | 위조 탐지율 ~99.99998% |
| Blind INIT Flood | 해당 없음 | INIT-ACK에 쿠키 포함 → 무상태 | Cookie HMAC 서명으로 위조 방지 |
| 반사 공격 (Reflection) | SYN-ACK 반사 | INIT에 VTag=0 필수 → 반사 무효 | — |
| ASCONF 하이재킹 | 해당 없음 | AUTH 청크로 ASCONF 인증 | 인증 없는 ASCONF 차단 가능 |
/* State Cookie 보안 메커니즘 상세 */
/* 1. State Cookie 생성 (서버 측, INIT-ACK 전송 시) */
/* net/sctp/sm_make_chunk.c — sctp_pack_cookie() */
/*
* Cookie 내용:
* - 초기화 파라미터 (스트림 수, a-rwnd 등)
* - 피어 주소 정보
* - 타임스탬프 (유효 기간 확인용)
* - HMAC 서명 (secret_key로 서명)
*
* 핵심: 서버가 어떤 상태도 저장하지 않음
* → INIT Flood에도 서버 메모리 소진 불가
*/
/* 2. State Cookie 검증 (서버 측, COOKIE-ECHO 수신 시) */
/* net/sctp/sm_make_chunk.c — sctp_unpack_cookie() */
struct sctp_signed_cookie {
__u8 signature[SCTP_SECRET_SIZE]; /* HMAC 서명 */
__u32 __pad;
struct sctp_cookie c; /* 쿠키 본체 */
};
/*
* 검증 순서:
* 1. HMAC 서명 확인 (위조 방지)
* 2. 타임스탬프 확인 (만료 방지, valid_cookie_life)
* 3. 파라미터 추출 → Association 생성
*
* HMAC 키 회전: 커널은 2개의 secret_key를 번갈아 사용
* → 키 전환 중에도 이전 키로 서명된 쿠키 유효
*/
/* 3. Verification Tag 검증 */
/* net/sctp/input.c — sctp_rcv() */
/*
* 모든 수신 패킷에서:
* 1. SCTP 공통 헤더의 VTag 추출
* 2. Association의 peer.i.init_tag과 비교
* 3. 불일치 → 패킷 즉시 폐기 (로그 없음, DoS 방지)
*
* 예외: INIT 청크는 VTag=0으로 전송
* (아직 Association이 없으므로)
*/
/* CRC32c 체크섬 계산 — net/sctp/output.c */
static __be32 sctp_compute_cksum(
const struct sk_buff *skb,
unsigned int offset)
{
/* CRC32c = Castagnoli CRC
* TCP의 1의 보수 합과 달리:
* - 32비트 다항식 기반 오류 탐지
* - 하드웨어 가속 지원 (SSE4.2 CRC32 명령)
* - 위조 난이도: ~2^32 (TCP는 ~2^16)
*
* 계산 방법:
* 1. 체크섬 필드를 0으로 설정
* 2. 전체 SCTP 패킷에 CRC32c 적용
* 3. 결과를 체크섬 필드에 저장
*/
struct sctphdr *sh = sctp_hdr(skb);
__le32 old = sh->checksum;
__le32 new;
sh->checksum = 0;
new = sctp_csum_update(~(__u32)0, skb);
sh->checksum = old;
return cpu_to_le32(~new);
}
modprobe -r sctp로 모듈을 언로드하여 공격 표면을 줄이세요.
SCTP 커널 패킷 경로 심화
SCTP 패킷이 커널에서 어떻게 처리되는지 수신(RX)과 송신(TX) 경로를 상세히 추적합니다. 각 단계의 함수 호출과 자료구조 변환을 이해하면 성능 병목과 버그를 효과적으로 분석할 수 있습니다.
| 단계 | 함수 | 소스 파일 | 역할 |
|---|---|---|---|
| RX 진입 | sctp_rcv() |
net/sctp/input.c |
IP 계층에서 SCTP 패킷 수신, 청크 파싱 시작 |
| Association 검색 | sctp_lookup_association() |
net/sctp/input.c |
VTag + 포트로 해시 테이블에서 Association 찾기 |
| 상태 머신 | sctp_do_sm() |
net/sctp/sm_sideeffect.c |
이벤트 + 현재 상태 → 전이 함수 실행 |
| 부작용 실행 | sctp_cmd_interpreter() |
net/sctp/sm_sideeffect.c |
타이머 시작/정지, 청크 전송, 상태 전이 |
| 사용자 전달 | sctp_ulpq_tail_data() |
net/sctp/ulpqueue.c |
재조립 완료된 메시지를 소켓 수신 큐에 삽입 |
| TX 시작 | sctp_sendmsg() |
net/sctp/socket.c |
사용자 데이터를 sctp_datamsg로 변환 |
| 출력 큐 | sctp_outq_flush() |
net/sctp/outqueue.c |
스케줄러로 전송할 청크 선택, 번들링 |
| 패킷 전송 | sctp_packet_transmit() |
net/sctp/output.c |
SCTP 헤더/CRC32c 설정 → IP 계층 전달 |
/* 수신 경로 상세 — 청크별 처리 */
/* sctp_inq_pop(): 입력 큐에서 청크 하나씩 추출 */
struct sctp_chunk *sctp_inq_pop(
struct sctp_inq *queue)
{
/*
* 1. sk_buff에서 다음 청크 헤더 파싱
* 2. 청크 길이만큼 데이터 포인터 전진
* 3. 패딩 건너뛰기 (4바이트 정렬)
* 4. sctp_chunk 구조체에 래핑하여 반환
*
* 하나의 sk_buff(패킷)에 여러 청크가 번들링되어 있으면
* 같은 sk_buff에서 여러 번 pop 가능
*/
}
/* 송신 경로 상세 — 데이터 청크 생성 */
/* sctp_datamsg_from_user(): 사용자 메시지 → 청크 분할 */
struct sctp_datamsg *sctp_datamsg_from_user(
struct sctp_association *asoc,
struct sctp_sndrcvinfo *sinfo,
struct iov_iter *from)
{
/*
* 1. 메시지 크기 확인
* 2. 경로 MTU 기준으로 분할 크기 결정
* max_data = PMTU - SCTP_HEADER - DATA_HEADER
* 3. 첫 청크: B(Begin) 플래그 설정
* 4. 중간 청크: B=0, E=0
* 5. 마지막 청크: E(End) 플래그 설정
* 6. TSN 할당 (글로벌 증가)
* 7. SSN/MID 할당 (스트림별 증가)
*
* PR-SCTP: sinfo의 timetolive, pr_policy 반영
*/
}
/* sctp_outq_flush(): 번들링 전략 */
/*
* 1. 제어 청크 우선 전송 (SACK, HEARTBEAT 등)
* 2. 재전송 청크 전송 (Fast Retransmit 대상)
* 3. 새 데이터 청크 전송 (스케줄러가 선택)
* 4. 하나의 패킷에 여러 청크 번들링 시도
* 조건: cwnd, a-rwnd, PMTU 여유 범위 내
* 5. 패킷 가득 차면 sctp_packet_transmit() 호출
*/
ftrace로 함수별 실행 시간을 측정하세요.
echo sctp_rcv sctp_do_sm sctp_outq_flush > /sys/kernel/debug/tracing/set_ftrace_filter로
핵심 함수만 필터링하면 오버헤드를 최소화하면서 전체 경로를 추적할 수 있습니다.
SCTP와 eBPF/XDP 통합
eBPF(extended Berkeley Packet Filter)는 커널 내부에서 안전하게 프로그램을 실행하여 SCTP의 관측성과 성능을 향상시킬 수 있습니다. kprobe/tracepoint를 통한 상세 추적부터 XDP를 통한 초기 패킷 필터링까지 다양한 활용이 가능합니다.
| eBPF 유형 | 연결 지점 | 용도 | 제약 |
|---|---|---|---|
| kprobe | 임의의 커널 함수 | SCTP 함수 추적, 인자 검사 | 함수 서명 변경 시 깨질 수 있음 |
| tracepoint | sctp:sctp_probe |
안정적인 SCTP 이벤트 추적 | 제공 tracepoint 수가 제한적 |
| cgroup/sock_ops | 소켓 연산 | SCTP 소켓 옵션 자동 설정 | SCTP 지원이 TCP만큼 풍부하지 않음 |
| XDP | NIC 수신 직후 | SCTP 패킷 초기 필터링/리다이렉트 | IP proto 132 수동 파싱 필요 |
| tc (Traffic Control) | ingress/egress | SCTP 패킷 분류, 마킹 | XDP보다 늦은 처리 단계 |
/* bpftrace를 사용한 SCTP 실시간 추적 */
/* 1. SCTP 패킷 수신 추적 */
/* $ bpftrace -e '
* kprobe:sctp_rcv {
* @rx_count = count();
* @rx_bytes = hist(((struct sk_buff *)arg0)->len);
* }
* interval:s:1 { print(@rx_count); print(@rx_bytes); }
* '
*/
/* 2. SCTP 상태 전이 추적 */
/* $ bpftrace -e '
* kprobe:sctp_do_sm {
* @events[arg0, arg1] = count();
* }
* '
* arg0 = event_type (CHUNK, TIMEOUT, PRIMITIVE)
* arg1 = subtype (INIT, DATA, SACK 등)
*/
/* 3. SCTP Association 생성/삭제 추적 */
/* $ bpftrace -e '
* kprobe:sctp_association_new {
* printf("New association: ep=%p\n", arg0);
* }
* kprobe:sctp_association_free {
* printf("Free association: asoc=%p\n", arg0);
* }
* '
*/
/* 4. SCTP 재전송 모니터링 */
/* $ bpftrace -e '
* kprobe:sctp_retransmit_mark {
* @retransmits = count();
* printf("Retransmit in outqueue %p\n", arg0);
* }
* '
*/
/* 5. SCTP Heartbeat 추적 */
/* $ bpftrace -e '
* kprobe:sctp_sf_sendbeat_8_3 {
* @hb_sent = count();
* }
* kprobe:sctp_sf_backbeat_8_3 {
* @hb_ack = count();
* }
* interval:s:10 {
* printf("HB sent: %d, HB-ACK: %d\n",
* @hb_sent, @hb_ack);
* }
* '
*/
/* XDP 프로그램: SCTP 패킷 필터링 */
#include <linux/bpf.h>
#include <bpf/bpf_helpers.h>
#include <linux/if_ether.h>
#include <linux/ip.h>
#define IPPROTO_SCTP 132
/* SCTP 공통 헤더 */
struct sctphdr {
__be16 source;
__be16 dest;
__be32 vtag;
__le32 checksum;
};
/* SCTP 청크 헤더 */
struct sctp_chunkhdr {
__u8 type;
__u8 flags;
__be16 length;
};
SEC("xdp")
int xdp_sctp_filter(struct xdp_md *ctx)
{
void *data = (void *)(long)ctx->data;
void *data_end = (void *)(long)ctx->data_end;
/* Ethernet 헤더 */
struct ethhdr *eth = data;
if ((void *)(eth + 1) > data_end)
return XDP_PASS;
if (eth->h_proto != __constant_htons(ETH_P_IP))
return XDP_PASS;
/* IP 헤더 */
struct iphdr *ip = (void *)(eth + 1);
if ((void *)(ip + 1) > data_end)
return XDP_PASS;
if (ip->protocol != IPPROTO_SCTP)
return XDP_PASS;
/* SCTP 헤더 */
struct sctphdr *sh = (void *)ip + (ip->ihl * 4);
if ((void *)(sh + 1) > data_end)
return XDP_PASS;
/* 특정 포트의 SCTP만 허용 */
__u16 dst_port = __constant_ntohs(sh->dest);
if (dst_port != 9999 && dst_port != 36412)
return XDP_DROP; /* 허용 포트 외 SCTP 차단 */
return XDP_PASS;
}
char _license[] SEC("license") = "GPL";
# XDP SCTP 필터 로드 및 테스트
$ clang -O2 -target bpf -c xdp_sctp_filter.c -o xdp_sctp_filter.o
$ ip link set dev eth0 xdpgeneric obj xdp_sctp_filter.o sec xdp
# 통계 확인
$ bpftool prog show
$ bpftool map dump name xdp_stats
# 제거
$ ip link set dev eth0 xdpgeneric off
sctp:sctp_probe tracepoint를 제공합니다.
perf trace -e sctp:*로 SCTP 이벤트를 추적할 수 있으며,
/sys/kernel/debug/tracing/events/sctp/에서 사용 가능한 tracepoint를 확인하세요.
kprobe와 달리 tracepoint는 커널 버전 간 안정적인 인터페이스를 제공합니다.
SCTP vs QUIC: 현대 전송 프로토콜 비교
SCTP와 QUIC는 모두 TCP의 한계를 극복하기 위해 설계된 전송 프로토콜이지만, 접근 방식이 근본적으로 다릅니다. SCTP는 커널 프로토콜 스택으로 구현되고, QUIC는 사용자 공간에서 UDP 위에 구축됩니다. 두 프로토콜의 설계 철학과 실무 배포 현황을 비교합니다.
| 비교 항목 | SCTP | QUIC |
|---|---|---|
| 표준 | RFC 4960 (2007, 원본 RFC 2960은 2000) | RFC 9000 (2021) |
| 구현 위치 | 커널 프로토콜 스택 | 사용자 공간 라이브러리 (UDP 위) |
| IP 프로토콜 | 132 (독자 프로토콜) | 17 (UDP) |
| NAT 통과 | UDP 캡슐화 필요 (RFC 6951) | UDP 기반 → 자연 통과 |
| 암호화 | DTLS 또는 별도 설정 필요 | TLS 1.3 내장 (필수) |
| 멀티스트리밍 | 지원 (INIT 시 스트림 수 협상) | 지원 (스트림 수 제한 없음) |
| 멀티호밍 | 네이티브 지원 (자동 failover) | Connection Migration (수동) |
| 메시지 경계 | 보존 (메시지 지향) | 스트림은 바이트 스트림 |
| 부분 신뢰성 | PR-SCTP (RFC 3758) | 없음 (모든 스트림 신뢰적) |
| 연결 설정 | 4-way handshake (1-RTT) | 0-RTT 또는 1-RTT |
| 업데이트 용이성 | 커널 업그레이드 필요 | 앱과 함께 배포 |
| 주요 사용처 | 텔레콤 (Diameter, S1AP, NGAP), WebRTC DC | 웹 (HTTP/3), 범용 |
| 리눅스 커널 지원 | net/sctp/ 모듈 | 커널 QUIC (개발 중, 6.x~) |
## SCTP가 여전히 QUIC보다 적합한 경우
1. 텔레콤 시그널링 (Diameter, SIGTRAN, S1AP/NGAP)
- 3GPP 표준이 SCTP를 명시적으로 요구
- 멀티호밍 failover가 99.999% 가용성 핵심
- 메시지 경계가 시그널링 프로토콜에 필수
2. 고가용성 클러스터 (Corosync, Pacemaker)
- 커널 레벨 멀티호밍으로 NIC 장애 자동 복구
- 사용자 공간 라이브러리 의존 없음
3. WebRTC DataChannel
- SCTP over DTLS over UDP (usrsctp)
- 부분 신뢰성(PR-SCTP)이 실시간 데이터에 필수
- 순서/비순서 전송 혼합 사용
## QUIC가 SCTP보다 적합한 경우
1. 웹 트래픽 (HTTP/3)
- 브라우저 기본 지원
- NAT/방화벽/CDN 완벽 호환
2. 인터넷 공개 서비스
- UDP 기반으로 미들박스 통과
- TLS 1.3 필수 암호화
3. 모바일 앱 통신
- Connection Migration으로 네트워크 전환 지원
- 사용자 공간 구현으로 앱에 번들 가능
관련 문서
SCTP와 관련된 다른 주제를 더 깊이 이해하고 싶다면 다음 문서를 참고하세요.
참고자료
- RFC 4960 - Stream Control Transmission Protocol
- RFC 6458 - Sockets API Extensions for SCTP
- RFC 3758 - SCTP Partial Reliability Extension (PR-SCTP)
- RFC 4895 - Authenticated Chunks for SCTP
- RFC 6951 - UDP Encapsulation of SCTP Packets
- RFC 8260 - SCTP Stream Schedulers and User Message Interleaving
- RFC 5061 - SCTP Dynamic Address Reconfiguration
- RFC 7829 - SCTP-PF: A Quick Failover Algorithm for SCTP
- RFC 9260 - Stream Control Transmission Protocol (SCTP 개정판, 2022)
- RFC 3873 - Management Information Base for SCTP
- Linux Kernel Documentation: SCTP
- Linux Kernel Source: net/sctp
- lksctp-tools - Linux Kernel SCTP Tools