Các máy chủ tập trung VPN, proxy kết thúc TLS và trạm định tuyến biên doanh nghiệp thường xuyên đối mặt với sự thay đổi liên tục của các luồng kết nối mạng. Dưới khối lượng công việc liên tục hàng chục ngàn yêu cầu kết nối mỗi giây, thông lượng thực tế của một trạm chuyển tiếp biên không chỉ phụ thuộc vào hiệu suất giải mã hay băng thông phần cứng. Tính ổn định của hệ thống thường suy giảm nghiêm trọng trước hàng loạt phiên bắt tay TCP rỗng, các đợt quét cổng tự động và nỗ lực làm cạn kiệt tài nguyên theo phương thức Slowloris.

Trong quy trình xử lý mạng tiêu chuẩn của Linux, việc hoàn thành quá trình bắt tay ba bước (SYN -> SYN-ACK -> ACK) sẽ lập tức chuyển đổi một yêu cầu kết nối tạm thời thành một socket đầy đủ. Nhân hệ thống xếp socket vào hàng đợi chấp nhận (accept queue) và thông báo cho không gian người dùng thông qua epoll. Tiến trình proxy thức dậy, gọi hàm accept(), phân bổ tài nguyên theo dõi phiên và đăng ký bộ mô tả tệp vào vòng lặp sự kiện—nhưng sau đó chỉ có thể chờ đợi gói dữ liệu từ máy trạm, vốn có thể đến rất muộn hoặc không bao giờ đến.

Để xóa bỏ các lần chuyển đổi ngữ cảnh CPU lãng phí và bảo vệ không gian người dùng trước làn sóng kết nối rỗng, Linux cung cấp tùy chọn socket TCP_DEFER_ACCEPT. Bằng cách chỉ đạo nhân trì hoãn việc đưa kết nối vào hàng đợi chấp nhận cho đến khi máy trạm gửi byte dữ liệu đầu tiên, TCP_DEFER_ACCEPT chuyển toàn bộ gánh nặng lọc kết nối xuống tầng bộ nhớ nhẹ của nhân, tạo nên lớp phòng thủ thiết yếu cho các gateway VPN mật độ cao.


Khủng hoảng bắt tay rỗng: Lãng phí đánh thức Epoll và cạn kiệt trạng thái

Trong hoạt động TCP thông thường, việc thiết lập kết nối tuân theo mô hình hàng đợi hai cấp bên trong ngăn xếp mạng Linux:

  1. Hàng đợi SYN Backlog: Khi máy trạm gửi gói TCP SYN, nhân hệ thống tạo một cấu trúc request_sock tinh gọn (chỉ chiếm khoảng 256 byte) và đưa vào hàng đợi SYN trong lúc truyền lại gói SYN-ACK.
  2. Hàng đợi Accept Queue: Khi máy trạm phản hồi bằng gói ACK hoàn tất bắt tay ba bước, nhân hệ thống cấp phát một cấu trúc struct sock đầy đủ (chiếm hơn 2 KB bộ nhớ RAM vật lý bị ghim, cùng các bộ đệm phát và nhận), đưa kết nối vào hàng đợi chấp nhận và đánh dấu socket lắng nghe ở trạng thái sẵn sàng đọc (EPOLLIN).
Điểm nghẽn của hàng đợi chấp nhận thông thường:

Máy trạm (Client)                 Ngăn xếp mạng nhân Linux            Proxy không gian người dùng
  │                                      │                                  │
  │─── SYN ─────────────────────────────►│                                  │
  │◄── SYN-ACK ──────────────────────────│ (Tạo request_sock ~256 byte)     │
  │                                      │                                  │
  │─── ACK (Bắt tay không dữ liệu) ─────►│                                  │
  │                                      │──► Cấp phát struct sock đầy đủ   │
  │                                      │──► Đưa vào hàng đợi Accept       │
  │                                      │──► Đánh thức EPOLLIN ───────────►│
  │                                      │                                  │ (Thức dậy luồng xử lý)
  │                                      │◄── accept4() ────────────────────│ (Cấp phát 4-16 KB)
  │                                      │                                  │ (Ngồi chờ dữ liệu...)
  │                                      │                                  │
  │... (Máy trạm im lặng hoặc quét) .....│..................................│ (Luồng bị đình trệ!)

Hành vi mặc định này tạo ra các lỗ hổng nghiêm trọng trong kiến trúc proxy biên:

  • Lãng phí chu kỳ CPU: Các luồng công việc liên tục thức dậy, thực hiện các lời gọi hệ thống tốn kém và phân bổ bộ đệm cho các kết nối không truyền tải bất kỳ dữ liệu hữu ích nào.
  • Phình to bảng kết nối: Các bot quét mạng tự động hoàn tất bắt tay rồi để kết nối ở trạng thái ngủ, đẩy số lượng bộ mô tả tệp của proxy chạm ngưỡng giới hạn RLIMIT_NOFILE.
  • Tràn hàng đợi lắng nghe: Dưới các đợt bùng nổ lưu lượng, các phiên bắt tay rỗng chiếm dụng toàn bộ hàng đợi backlog, khiến các kết nối hợp lệ bị loại bỏ trước khi tiến trình người dùng kịp tiếp nhận.

Phân tích chuyên sâu: Cách TCP_DEFER_ACCEPT thay đổi máy trạng thái TCP nhân

Được cấu hình thông qua hàm setsockopt(), TCP_DEFER_ACCEPT tái cấu trúc căn bản sự tương tác giữa cơ chế bắt tay của nhân và vòng lặp sự kiện không gian người dùng:

int timeout_secs = 30;
int listen_fd = socket(AF_INET, SOCK_STREAM, 0);

/* Yêu cầu nhân hoãn thông báo accept cho đến khi có dữ liệu đến */
int ret = setsockopt(listen_fd, IPPROTO_TCP, TCP_DEFER_ACCEPT, &timeout_secs, sizeof(timeout_secs));

1. Kéo dài trạng thái SYN_RECV và hoãn nâng cấp cấu trúc Socket

Khi kích hoạt TCP_DEFER_ACCEPT trên socket lắng nghe, nhân hệ điều hành vẫn nhận gói ACK cuối cùng từ máy trạm nhưng chủ động hoãn việc nâng cấp socket. Nếu gói ACK không chứa dữ liệu tải trọng (skb->len == 0), nhân sẽ giữ kết nối ở trạng thái SYN_RECV gọn nhẹ bên trong bảng request socket.

Kết nối không bị chuyển thành struct sock đầy đủ và không có sự kiện EPOLLIN nào được gửi tới socket lắng nghe. Đối với máy trạm, kết nối dường như đã được thiết lập thành công. Tuy nhiên, trên máy chủ, phiên kết nối này chỉ tiêu tốn lượng RAM tối thiểu và hoàn toàn không làm tiêu hao chu kỳ CPU ở không gian người dùng.

2. Dữ liệu sẵn sàng tức thì khi gọi lệnh accept() ở không gian người dùng

Ngay khi máy trạm gửi gói dữ liệu đầu tiên—chẳng hạn như gói TLS ClientHello, yêu cầu HTTP CONNECT hoặc gói khởi tạo đường hầm WireGuard qua TCP—nhân hệ thống sẽ khớp gói tin với cấu trúc request_sock đang chờ.

Luồng máy trạng thái khi dùng TCP_DEFER_ACCEPT:

Máy trạm (Client)                 Ngăn xếp mạng nhân Linux            Proxy không gian người dùng
  │                                      │                                  │
  │─── SYN ─────────────────────────────►│ (Tạo request_sock)               │
  │◄── SYN-ACK ──────────────────────────│                                  │
  │─── ACK (Không dữ liệu) ─────────────►│ (Giữ ở SYN_RECV; KHÔNG đánh thức)│ (Nghỉ ngơi / Rảnh tay)
  │                                      │                                  │
  │... (Độ trễ: 500 ms) .................│ (Chỉ tiêu tốn ~256 byte RAM)     │ (0% chu kỳ CPU bị tốn)
  │                                      │                                  │
  │─── PSH, ACK (Gói TLS ClientHello) ──►│──► Nâng cấp lên socket hoàn chỉnh│
  │                                      │──► Đưa vào hàng đợi Accept       │
  │                                      │──► Đánh thức EPOLLIN ───────────►│ (Đánh thức luồng xử lý)
  │                                      │◄── accept4() ────────────────────│
  │                                      │◄── recv() (Nhận dữ liệu ngay)    │

Khi tiếp nhận dữ liệu, nhân nâng cấp kết nối sang trạng thái TCP_ESTABLISHED, khởi tạo các cấu trúc socket hoàn chỉnh và đưa vào hàng đợi chấp nhận. Khi tiến trình người dùng thức dậy và gọi lệnh accept(), gói dữ liệu đã nằm sẵn trong bộ đệm nhận của socket. Lệnh gọi recv() tiếp theo sẽ trả về dữ liệu ngay lập tức mà không phải chờ đợi.


Phòng ngừa tấn công: Triệt tiêu Slowloris và các đợt quét cổng thụ động

Trên hạ tầng mạng mở, TCP_DEFER_ACCEPT đóng vai trò như một lá chắn tầng nhân giúp ngăn ngừa các cuộc tấn công từ chối dịch vụ tầng ứng dụng và các hoạt động do thám mạng.

1. Hấp thụ nguy cơ cạn kiệt bảng kết nối bên trong Request Socket tinh gọn

Các đợt tấn công Slowloris và cạn kiệt tài nguyên mở hàng ngàn phiên TCP đồng thời nhưng không gửi tiêu đề giao thức. Nếu không có TCP_DEFER_ACCEPT, kẻ tấn công có thể làm cạn kiệt bộ mô tả tệp của trạm proxy chỉ với băng thông tối thiểu.

Khi bật TCP_DEFER_ACCEPT, các phiên nhàn rỗi của kẻ tấn công bị giữ lại trong bảng request_sock của nhân và không bao giờ chạm tới không gian người dùng. Daemon proxy hoàn toàn không bị ảnh hưởng, dành toàn bộ tài nguyên bộ nhớ và CPU phục vụ các máy trạm hợp lệ gửi dữ liệu thực thụ.

2. Cơ chế tự động hủy quá hạn và truyền lại gói tin SYN-ACK tầng nhân

Tham số thời gian truyền vào TCP_DEFER_ACCEPT xác định khoảng thời gian chờ tính bằng giây. Nếu máy trạm hoàn thành bắt tay nhưng không gửi dữ liệu trước khi hết thời gian này, nhân hệ điều hành sẽ truyền lại gói SYN-ACK để nhắc nhở máy trạm.

Nếu máy trạm vẫn tiếp tục im lặng sau thời gian quy định, nhân sẽ âm thầm giải phóng cấu trúc request_sock. Kết nối chết bị loại bỏ trực tiếp khỏi bộ nhớ mà không cần kích hoạt mã ghi nhật ký lỗi hay xử lý dọn dẹp phức tạp ở không gian người dùng.


Bản thiết kế vận hành: Cấu hình TCP_DEFER_ACCEPT trong proxy môi trường thực tế

Việc triển khai TCP_DEFER_ACCEPT trong các gateway VPN môi trường sản xuất đòi hỏi thiết lập cửa sổ thời gian chờ hợp lý kết hợp với vòng lặp sự kiện bất đồng bộ:

/* Khởi tạo socket lắng nghe bảo mật cao */
int setup_hardened_listener(int port, int defer_timeout) {
    int fd = socket(AF_INET, SOCK_STREAM | SOCK_NONBLOCK | SOCK_CLOEXEC, 0);
    
    int optval = 1;
    setsockopt(fd, SOL_SOCKET, SO_REUSEPORT, &optval, sizeof(optval));
    
    /* Bật hoãn accept với thời gian chờ (ví dụ: 15-30 giây) */
    setsockopt(fd, IPPROTO_TCP, TCP_DEFER_ACCEPT, &defer_timeout, sizeof(defer_timeout));
    
    struct sockaddr_in addr;
    memset(&addr, 0, sizeof(addr));
    addr.sin_family = AF_INET;
    addr.sin_addr.s_addr = htonl(INADDR_ANY);
    addr.sin_port = htons(port);
    
    bind(fd, (struct sockaddr *)&addr, sizeof(addr));
    listen(fd, 4096);
    return fd;
}

Các lưu ý kỹ thuật trọng tâm:

  • Hiệu chuẩn thời gian chờ: Thiết lập defer_timeout từ 15 đến 30 giây cho các trạm chuyển tiếp công cộng. Thời gian chờ quá ngắn (dưới 5 giây) có thể ngắt nhầm các kết nối hợp lệ trên mạng di động có độ trễ cao.
  • Tính tương thích của giao thức: Đảm bảo các giao thức ứng dụng gửi dữ liệu ngay sau bắt tay. Các tiêu chuẩn bảo mật như TLS, HTTP, SSH và WireGuard-over-TCP đều tuân thủ nguyên tắc này và hoạt động hoàn hảo.

Tương tác kiến trúc: TCP Fast Open, SYN Cookies và định tuyến biên

TCP_DEFER_ACCEPT kết hợp hài hòa với các công nghệ tăng tốc và bảo mật khác của nhân Linux:

  • TCP SYN Cookies: Trong các đợt tấn công SYN flood quy mô lớn, nhân chuyển sang cơ chế SYN cookie phi trạng thái. Khi máy trạm phản hồi bằng gói ACK hợp lệ, TCP_DEFER_ACCEPT vẫn tiếp tục hoãn khởi tạo socket cho đến khi nhận được dữ liệu tải trọng, bảo vệ bộ nhớ máy chủ ngay cả sau khi xác minh cookie.
  • So sánh với TCP Fast Open (TFO): Trong khi TFO gửi dữ liệu ngay trong gói SYN đầu tiên (RFC 7413), TCP_DEFER_ACCEPT tối ưu hóa các phiên bắt tay ba bước chuẩn khi dữ liệu đến ở gói thứ ba hoặc các gói tiếp theo. Kết hợp cả hai mang lại sự tối ưu toàn diện cho cả máy trạm hiện đại lẫn truyền thống.

Đường dẫn dữ liệu RiceGuard: Lọc kết nối không tốn chi phí tại vùng biên

VPNRice tích hợp TCP_DEFER_ACCEPT như một lớp phòng thủ cửa ngõ cốt lõi trong các trạm chuyển tiếp RiceGuard. Xử lý lưu lượng toàn cầu phục vụ các cổng truy cập doanh nghiệp đòi hỏi khả năng phục hồi vững chắc trước tiếng ồn mạng và các đợt cạn kiệt tài nguyên.

Bằng cách kích hoạt cơ chế hoãn bắt tay trên toàn bộ các cổng TCP tiếp nhận, trạm chuyển tiếp RiceGuard bảo đảm rằng luồng công việc người dùng chỉ vận hành cho các phiên có dữ liệu thực tế. Các kết nối nhàn rỗi, hoạt động quét cổng và bắt tay dở dang đều bị hấp thụ và loại bỏ tại tầng nhân mà không làm tiêu tốn tài nguyên tính toán. Lớp phòng vệ này giúp hệ thống duy trì thông lượng ổn định ở tốc độ đường truyền, tối ưu hóa tài nguyên và bảo vệ quyền riêng tư số an toàn cho mọi người dùng.