Skip to content
LIVE FEED

Blog07 — Lập Trình Hệ Thống & Mạng

07 — Lập Trình Hệ Thống & Mạng

Mục tiêu: Đưa code C/C++ lên mạng thật. Đây là cầu nối trực tiếp tới công việc của bạn: socket TCP/UDP/SCTP (nền của Diameter/M3UA), raw socket (craft packet để fuzz/test), event loop epoll (kiến trúc server hiệu năng cao như Kamailio), thread và signal. Module này lý giải ở mức code những thứ bạn vẫn thấy trên Wireshark — kể cả tại sao “SCTP bắt tay xong nhưng không có CER”.


1. Mô hình socket Berkeley — bản đồ tinh thần

Socket là “file descriptor cho mạng”. Mọi giao tiếp mạng trong Linux đều qua các syscall sau:

        SERVER                              CLIENT
        ------                              ------
        socket()                            socket()
        bind()                              
        listen()      (TCP/SCTP)            
        accept() ◄───────── kết nối ──────► connect()
           │                                   │
        recv()/send() ◄──── dữ liệu ─────► send()/recv()
           │                                   │
        close()                             close()
#include <sys/socket.h>
int s = socket(domain, type, protocol);
// domain:   AF_INET (IPv4), AF_INET6 (IPv6)
// type:     SOCK_STREAM (TCP), SOCK_DGRAM (UDP), SOCK_SEQPACKET/SOCK_STREAM (SCTP)
// protocol: 0 (mặc định) hoặc IPPROTO_SCTP, IPPROTO_RAW...

socket() trả về một int (file descriptor) hoặc -1 nếu lỗi (kiểm tra errno).


2. Địa chỉ & endianness ở tầng socket

#include <netinet/in.h>
#include <arpa/inet.h>

struct sockaddr_in addr;
memset(&addr, 0, sizeof(addr));
addr.sin_family = AF_INET;
addr.sin_port   = htons(3868);                 // PORT phải HOST→NETWORK (big-endian)!
inet_pton(AF_INET, "192.0.2.10", &addr.sin_addr);  // chuỗi IP → nhị phân

Nhắc lại module 03: sin_port dùng htons. Đây là lỗi kinh điển của người mới làm socket — quên htons → kết nối sai port một cách “im lặng”.


3. UDP — cho SIP, RTP, GTP, DNS

UDP không kết nối: gửi/nhận datagram độc lập. Phù hợp với SIP (thường UDP/5060), GTP (2123/2152), RTP.

Server UDP (nhận gói SIP/GTP)

#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <string.h>
#include <stdio.h>
#include <unistd.h>

int main(void) {
    int s = socket(AF_INET, SOCK_DGRAM, 0);
    if (s < 0) { perror("socket"); return 1; }

    struct sockaddr_in addr = {0};
    addr.sin_family = AF_INET;
    addr.sin_port   = htons(5060);             // SIP port
    addr.sin_addr.s_addr = htonl(INADDR_ANY);  // mọi interface
    if (bind(s, (struct sockaddr*)&addr, sizeof(addr)) < 0) { perror("bind"); return 1; }

    uint8_t buf[2048];
    struct sockaddr_in peer; socklen_t plen = sizeof(peer);
    for (;;) {
        ssize_t n = recvfrom(s, buf, sizeof(buf), 0,
                             (struct sockaddr*)&peer, &plen);
        if (n < 0) { perror("recvfrom"); break; }
        printf("nhận %zd byte từ %s\n", n, inet_ntoa(peer.sin_addr));
        // → đây là chỗ gọi parser SIP/GTP của bạn với (buf, n)
    }
    close(s);
    return 0;
}

Client UDP (gửi)

int s = socket(AF_INET, SOCK_DGRAM, 0);
struct sockaddr_in dst = {0};
dst.sin_family = AF_INET; dst.sin_port = htons(5060);
inet_pton(AF_INET, "192.0.2.10", &dst.sin_addr);
const char *msg = "OPTIONS sip:probe SIP/2.0\r\n...";
sendto(s, msg, strlen(msg), 0, (struct sockaddr*)&dst, sizeof(dst));

Liên hệ tooling: đây là khung của một SIP scanner tối giản (như svmap của SIPVicious). Gửi OPTIONS, đọc phản hồi để fingerprint. Bạn đã hiểu nó ở mức packet — giờ hiểu ở mức code.


4. TCP — cho SIP/TLS, HTTP/2 (5G SBI), Diameter-over-TCP

TCP có kết nối, có thứ tự, có độ tin cậy. Giống UDP nhưng thêm listen/accept (server) và connect (client), dùng send/recv.

// SERVER (rút gọn)
int s = socket(AF_INET, SOCK_STREAM, 0);
bind(s, ...); listen(s, 16);
int c = accept(s, NULL, NULL);     // chặn tới khi có client → trả fd kết nối
ssize_t n = recv(c, buf, sizeof(buf), 0);
send(c, resp, resp_len, 0);

// CLIENT
int s = socket(AF_INET, SOCK_STREAM, 0);
connect(s, (struct sockaddr*)&dst, sizeof(dst));
send(s, req, req_len, 0);
recv(s, buf, sizeof(buf), 0);

Bẫy TCP quan trọng cho parser: TCP là luồng byte, không bảo toàn ranh giới message. Một lần recv có thể trả một phần message hoặc nhiều message dính nhau. Parser của bạn phải đệm và tái khung (reframe) theo length field của protocol (Diameter header có Message Length → đọc đủ rồi mới parse). Quên điều này gây bug “đôi khi parse lỗi” rất khó tìm. (UDP/SCTP-SEQPACKET bảo toàn ranh giới nên không bị.)


5. SCTP — TRỌNG TÂM cho Diameter, M3UA/SIGTRAN, S6a

SCTP (RFC 4960) là tầng giao vận của Diameter và SIGTRAN. Bạn đang xử lý case S6a DEA↔HSS, nên đây là phần đắt giá nhất module.

5.1. Vì sao telecom dùng SCTP thay TCP

Tính năng SCTPLợi ích viễn thông
Multi-streamingnhiều luồng độc lập trên 1 association → tránh head-of-line blocking
Multi-homingmột association qua nhiều IP → dự phòng đường mạng (redundancy)
Message-orientedbảo toàn ranh giới message (khác TCP byte stream)
4-way handshakechống SYN-flood tốt hơn (INIT/INIT-ACK/COOKIE-ECHO/COOKIE-ACK)

5.2. Bắt tay 4 bước SCTP (đối chiếu trực tiếp với case của bạn)

   CLIENT (DEA?)                         SERVER (HSS?)
        │                                     │
        │ ──────────── INIT ────────────────► │
        │ ◄──────── INIT-ACK (cookie) ─────── │
        │ ──────── COOKIE-ECHO ─────────────► │
        │ ◄──────── COOKIE-ACK ─────────────► │   ← TẦNG GIAO VẬN sẵn sàng tại đây
        │                                     │
        │  (giờ tới lượt TẦNG ỨNG DỤNG)        │
        │ ─────────── CER ──────────────────► │   ← ai gửi CER? = vai trò Diameter
        │ ◄────────── CEA ─────────────────── │

Đây chính là gốc rễ case của bạn: COOKIE-ACK xong nghĩa là SCTP association đã mở — nhưng đó chỉ là tầng giao vận. Việc ai gửi CER do vai trò Diameter (initiator/responder) quyết định, là tầng ứng dụng hoàn toàn độc lập. Nếu DEA được cấu hình là responder (đợi CER) trong khi HSS cũng đợi CER, không bên nào gửi → HSS hết timer → gửi SHUTDOWN. Code dưới đây cho bạn thấy hai tầng này là hai đoạn code tách biệt.

5.3. Client SCTP gửi CER thô (one-to-one, SOCK_STREAM)

#include <sys/socket.h>
#include <netinet/in.h>
#include <netinet/sctp.h>     // cần libsctp-dev
#include <arpa/inet.h>
#include <string.h>
#include <stdio.h>
#include <unistd.h>
#include <stdint.h>

int main(void) {
    // ===== TẦNG GIAO VẬN: mở SCTP association =====
    int s = socket(AF_INET, SOCK_STREAM, IPPROTO_SCTP);   // SCTP one-to-one
    if (s < 0) { perror("socket"); return 1; }

    struct sockaddr_in hss = {0};
    hss.sin_family = AF_INET;
    hss.sin_port   = htons(3868);                 // Diameter port
    inet_pton(AF_INET, "192.0.2.20", &hss.sin_addr);

    if (connect(s, (struct sockaddr*)&hss, sizeof(hss)) < 0) {
        perror("connect (SCTP INIT/COOKIE thất bại)"); return 1;
    }
    printf("[+] SCTP association MỞ (qua COOKIE-ACK)\n");

    // ===== TẦNG ỨNG DỤNG: chủ động gửi CER (vai trò Diameter = initiator) =====
    // Diameter header (20 byte) + vài AVP tối thiểu. Đây là byte THÔ minh hoạ.
    uint8_t cer[] = {
        0x01,                   // Version = 1
        0x00, 0x00, 0x00,       // Message Length (điền đúng ở bài tập)
        0x80,                   // Command Flags: R=1 (Request)
        0x00, 0x01, 0x01,       // Command Code = 257 (CER)
        0x00, 0x00, 0x00, 0x00, // Application-Id = 0 (base)
        0x00, 0x00, 0x00, 0x01, // Hop-by-Hop ID
        0x00, 0x00, 0x00, 0x01, // End-to-End ID
        // ... + Origin-Host, Origin-Realm, Host-IP-Address, Vendor-Id,
        //       Product-Name, Auth-Application-Id AVP ...
    };

    // sctp_sendmsg cho phép chọn stream, ppid... (ppid Diameter thường = 46/47)
    ssize_t n = sctp_sendmsg(s, cer, sizeof(cer),
                             NULL, 0,        // địa chỉ (đã connect nên NULL)
                             htonl(0),       // ppid
                             0,              // flags
                             0,              // stream id 0
                             0, 0);          // ttl, context
    if (n < 0) { perror("sctp_sendmsg"); return 1; }
    printf("[+] Đã gửi CER (%zd byte) — ta là Diameter INITIATOR\n", n);

    uint8_t buf[2048];
    struct sctp_sndrcvinfo info; int flags = 0;
    ssize_t r = sctp_recvmsg(s, buf, sizeof(buf), NULL, NULL, &info, &flags);
    if (r > 0) printf("[+] Nhận %zd byte (CEA?). Command = 0x%02x%02x%02x\n",
                      r, buf[5], buf[6], buf[7]);

    close(s);
    return 0;
}

Biên dịch: gcc -std=c17 -Wall -Wextra cer_client.c -o cer_client -lsctp

5.4. Bắt sự kiện SCTP (phát hiện SHUTDOWN — chính triệu chứng của bạn)

// Bật nhận notification để thấy sự kiện association (kể cả SHUTDOWN từ HSS)
struct sctp_event_subscribe ev = {0};
ev.sctp_association_event = 1;
ev.sctp_shutdown_event    = 1;
setsockopt(s, IPPROTO_SCTP, SCTP_EVENTS, &ev, sizeof(ev));
// Khi recvmsg trả về với flags & MSG_NOTIFICATION → đọc union sctp_notification,
// loại SCTP_SHUTDOWN_EVENT cho bạn biết peer đã đóng (đúng triệu chứng "HSS gửi SHUTDOWN").

Một harness như thế này giúp bạn tái hiện và quan sát vấn đề S6a ở mức byte: thấy COOKIE-ACK, thấy ta (không) gửi CER, thấy HSS gửi SHUTDOWN — xác nhận chẩn đoán “lỗi cấu hình vai trò Diameter FSM” mà không phụ thuộc tài liệu nền tảng DEA.

5.5. Tùy chọn SCTP hữu ích

struct sctp_initmsg im = {0};
im.sinit_num_ostreams  = 16;     // số stream gửi
im.sinit_max_instreams = 16;     // số stream nhận
setsockopt(s, IPPROTO_SCTP, SCTP_INITMSG, &im, sizeof(im));
// Multi-homing: dùng sctp_bindx()/sctp_connectx() với nhiều địa chỉ

6. Raw socket — craft & soi packet (cho fuzzing/test tầng thấp)

Raw socket cho phép tự dựng header (IP/UDP/…) — nền tảng để craft gói dị dạng test stack (như Scapy nhưng bằng C, nhanh hơn).

#include <sys/socket.h>
#include <netinet/ip.h>
#include <netinet/udp.h>

// Cần CAP_NET_RAW (chạy bằng sudo trong lab)
int s = socket(AF_INET, SOCK_RAW, IPPROTO_RAW);

int one = 1;
setsockopt(s, IPPROTO_IP, IP_HDRINCL, &one, sizeof(one));  // "tôi tự dựng IP header"

// Tự điền struct iphdr + udphdr + payload vào buffer, tự tính checksum (module 04),
// rồi sendto() tới đích. Đây là cách craft GTP/Diameter packet dị dạng để fuzz.

Bối cảnh white-hat: raw socket + checksum thủ công + bố cục byte chính xác = bạn craft được gói GTP/SCTP/SIP bất kỳ để kiểm thử khả năng chống chịu của stack trong lab được cấp phép. Đây là phiên bản “mức C” của tư duy fuzzing protocol.


7. epoll — event loop hiệu năng cao (kiến trúc Kamailio/OpenSIPS)

Server thật xử lý hàng nghìn kết nối đồng thời. Thay vì một thread/kết nối, dùng I/O đa hợp (multiplexing): một thread theo dõi nhiều fd qua epoll.

#include <sys/epoll.h>
#include <unistd.h>

int ep = epoll_create1(0);
struct epoll_event ev = {0};
ev.events  = EPOLLIN;            // quan tâm sự kiện "đọc được"
ev.data.fd = listen_fd;
epoll_ctl(ep, EPOLL_CTL_ADD, listen_fd, &ev);

struct epoll_event events[64];
for (;;) {
    int n = epoll_wait(ep, events, 64, -1);    // chặn tới khi có fd sẵn sàng
    for (int i = 0; i < n; i++) {
        int fd = events[i].data.fd;
        if (fd == listen_fd) {
            int c = accept(listen_fd, NULL, NULL);
            // thêm c vào epoll...
        } else {
            // recv() trên fd này, parse, xử lý...
        }
    }
}
Cơ chếĐộ mở rộngGhi chú
selectkém (giới hạn 1024 fd)cũ, di động
polltrung bìnhkhông giới hạn cứng
epoll (Linux)cao (vạn fd)chuẩn cho server Linux hiệu năng cao

Đây là lý do một SIP proxy hay Diameter node xử lý được lượng lớn kết nối. Hiểu epoll giúp bạn đọc kiến trúc lõi của các stack này và suy luận về điểm nghẽn/DoS.


8. Đa luồng (pthread) & đồng bộ

#include <pthread.h>

void *worker(void *arg) {
    int id = *(int*)arg;
    /* xử lý gói trên một core */
    return NULL;
}

pthread_t t;
int id = 1;
pthread_create(&t, NULL, worker, &id);
pthread_join(t, NULL);                  // đợi thread xong

// Bảo vệ dữ liệu chia sẻ
pthread_mutex_t lock = PTHREAD_MUTEX_INITIALIZER;
pthread_mutex_lock(&lock);   /* vùng tới hạn */   pthread_mutex_unlock(&lock);

Bẫy an ninh: race condition trên dữ liệu chia sẻ (vd. bảng session) gây UAF/double-free khó tái hiện và có thể khai thác (TOCTOU). Phát hiện bằng ThreadSanitizer: gcc -fsanitize=thread. Trong code mạng đa luồng, đây là lớp lỗi cần soi kỹ.


9. Signal & vệ sinh tài nguyên

#include <signal.h>
volatile sig_atomic_t running = 1;
void on_sigint(int sig) { (void)sig; running = 0; }   // chỉ làm việc tối thiểu trong handler

signal(SIGINT, on_sigint);
while (running) { /* vòng lặp server; thoát sạch khi Ctrl-C */ }

Trong signal handler chỉ được gọi hàm “async-signal-safe”. Đặt cờ rồi xử lý ở vòng lặp chính là pattern an toàn (graceful shutdown cho probe/LI service).


10. Bài tập / Lab module 07

  1. UDP SIP responder: hoàn thiện server UDP mục 3, nhận một SIP OPTIONS (gõ tay qua nc -u) và trả “SIP/2.0 200 OK”. Bắt bằng Wireshark để xác nhận.
  2. TCP reframing: viết client/server TCP gửi nhiều “message” có 2-byte length prefix; ở server, chứng minh một recv có thể nhận dính/cụt, rồi viết logic đệm để tái khung đúng từng message. Hiểu bẫy byte-stream của TCP.
  3. SCTP echo: viết server SCTP (one-to-one) echo lại mọi message, và client gửi vài message trên các stream khác nhau. Bắt bằng Wireshark, quan sát đủ 4 bước INIT/INIT-ACK/COOKIE-ECHO/COOKIE-ACK.
  4. CER client (capstone nhỏ): hoàn thiện client mục 5.3 — điền đúng Message Length và thêm các AVP bắt buộc của CER (Origin-Host, Origin-Realm, Host-IP-Address, Vendor-Id, Product-Name, Auth-Application-Id). Gửi tới một HSS/DEA lab (vd. FreeDiameter freeDiameterd hoặc Open5GS HSS) và nhận CEA. Đối chiếu byte với Wireshark Diameter dissector.
  5. Tái hiện triệu chứng S6a: dùng harness SCTP + bật SCTP_EVENTS (mục 5.4). Kịch bản A: client gửi CER → nhận CEA (thành công). Kịch bản B: client không gửi CER (đóng vai responder) → quan sát HSS gửi SHUTDOWN sau timeout. Viết ghi chú giải thích bằng lời vì sao đây là minh hoạ “tầng giao vận OK, vai trò Diameter sai”.
  6. epoll mini-server: chuyển server UDP/TCP sang dùng epoll, phục vụ nhiều client đồng thời. Đo bằng cách mở nhiều nc.
  7. (Lab nâng cao) raw UDP: với raw socket + IP_HDRINCL, tự dựng một gói UDP (IP+UDP header + payload), tính checksum, gửi tới một listener của bạn. Xác nhận listener nhận đúng. Tiền đề cho fuzzing tầng mạng.

Cột mốc module 07: Bạn lập trình được UDP/TCP/SCTP ở mức syscall, hiểu bắt tay 4 bước SCTP và sự tách biệt tầng giao vận ↔ tầng ứng dụng (giải thích được case S6a ở mức code), craft được packet thô, và dựng server bằng epoll. Bạn có thể viết công cụ tương tác trực tiếp với telecom stack thật.

➡️ Tiếp theo: 08-Phan-Tich-Giao-Thuc-Telecom.md — parse Diameter/GTP/SIP/ASN.1 và craft packet trong C/C++.

Lộ trình học tập

Lập Trình C/C++

  1. 1 01 — C Cơ Bản
  2. 2 02 — Con Trỏ & Bộ Nhớ
  3. 3 03 — Struct, Union, Bitfield & Bố Cục Bộ Nhớ
  4. 4 04 — C Nâng Cao
  5. 5 05 — C++ Cốt Lõi
  6. 6 06 — C++ Hiện Đại
  7. 7 07 — Lập Trình Hệ Thống & Mạng
  8. 8 08 — Phân Tích Giao Thức Telecom Bằng C/C++
  9. 9 09 — An Ninh Bộ Nhớ & Khai Thác Cơ Bản
  10. 10 11 — Lập Trình Nhúng & Real-time (SDR-first)
  11. 11 Bài tập & Pet Project — Lộ trình C/C++ chuyên sâu
  12. 12 Tổng hợp các dạng printf để in giá trị trong C
  13. 13 Bitwise & Mask, Octet, Endian: Hướng dẫn parse protocol byte cho người mới
  14. 14 Tổng hợp: Con trỏ, kích thước kiểu dữ liệu và byte trong bộ nhớ
Contact
Facebook
TOP