sanstun: Thư viện WireGuard viết bằng Rust với kiến trúc sans-IO và hỗ trợ xử lý song song

Phần mềm30 tháng 10, 2026·8 phút đọc

Firezone vừa giới thiệu sanstun, một triển khai WireGuard bằng Rust theo mô hình sans-IO, tách biệt hoàn toàn logic trạng thái khỏi các tác vụ I/O. Thiết kế mới cho phép mã hóa song song trên nhiều luồng, đơn giản hóa API và thay thế cho fork boringtun đã được duy trì suốt hơn hai năm.

sanstun: Thư viện WireGuard viết bằng Rust với kiến trúc sans-IO và hỗ trợ xử lý song song

Firezone vừa công bố sanstun, một triển khai máy trạng thái WireGuard bằng Rust theo mô hình sans-IO, hướng tới sự tiện dụng, bảo mật và hiệu năng thông qua một API thân thiện với xử lý song song. Đây là kết quả của hơn hai năm kinh nghiệm vận hành và vá lỗi một fork của thư viện boringtun.

sans-IO là gì?

sans-IO là triết lý thiết kế hệ thống sao cho phần I/O và các tác dụng phụ (side-effect) được tách bạch hoàn toàn khỏi trạng thái và "logic nghiệp vụ".

Điều đáng chú ý là "I/O" ở đây không chỉ nói đến I/O tệp hay mạng, mà còn bao gồm cả thời gian và tính ngẫu nhiên. Nhờ không có tác dụng phụ, các thành phần sans-IO kết hợp với nhau rất tốt và tương đối dễ kiểm thử. Cách thiết kế này cũng tránh được những vấn đề như "function coloring" và rất hợp với quy tắc sở hữu (ownership) của Rust.

Dù các ví dụ trong bài viết gốc đều dùng Rust, sans-IO không hề gắn bó riêng với ngôn ngữ này — các mẫu thiết kế của nó có thể áp dụng cho bất kỳ ngôn ngữ nào.

Từ boringtun đến sanstun

Khi mới xây dựng Firezone, nhóm phát triển chọn boringtun làm thư viện WireGuard. Ban đầu nó đáp ứng được nhu cầu, nhưng theo thời gian, họ liên tục phải vá fork của mình bằng các bản sửa lỗi và bổ sung API. Vì luôn muốn các bản vá có thể được đưa ngược lên upstream, họ cố giữ nguyên API cũ — và điều này ngày càng khó khi yêu cầu sản phẩm thay đổi.

Một trong những thay đổi lớn đầu tiên là chuyển sang xử lý thời gian hoàn toàn theo sans-IO, thêm các API có hậu tố _at để nhận vào một giá trị Instant. Điều này rất quan trọng nếu bạn muốn mô phỏng chính xác toàn bộ hệ thống.

Một thay đổi khác là đệm nội bộ (internal buffering). Thoạt nhìn, việc boringtun tự động thiết lập phiên khi bạn gửi gói tin mà chưa có phiên có vẻ tiện lợi, nhưng nó kéo theo nhiều vấn đề:

  • Đến một lúc nào đó buffer phải được xả ra — boringtun giải quyết bằng cách yêu cầu gọi decapsulate với datagram độ dài bằng 0, khá phản trực giác.
  • Phát sinh thêm cấp phát bộ nhớ.
  • Kích thước buffer cần được cấu hình và tinh chỉnh.

Tất cả đều dẫn đến cùng một giải pháp: không đệm trong thư viện. Điều này đơn giản hóa giao diện và cả phần triển khai ở hai phía.

Tổng cộng, nhóm đã tạo hơn 70 PR, không tính các lần nâng cấp phụ thuộc. Sau hơn hai năm, họ đã có hình dung khá rõ về API mà một thư viện WireGuard chạy trong không gian người dùng nên có — và sanstun ra đời từ đó.

API sans-IO tốt hơn

Các khối xây dựng cốt lõi của API sans-IO bao gồm:

  • Xử lý gói tin đến: Tunnel::handle_handshake_initiation, Tunnel::handle_handshake_response, Tunnel::handle_cookie_reply, Tunnel::begin_decapsulate và Tunnel::commit_decapsulate
  • Xử lý gói tin đi: Tunnel::begin_encapsulate
  • Cập nhật bộ định thời: Tunnel::handle_timeout và Tunnel::poll_timeout
  • Xử lý sự kiện: Tunnel::poll_event

Không có đệm nội bộ nghĩa là không còn ràng buộc nào buộc bạn phải gọi các API theo vòng lặp — cách làm này ăn khớp hơn nhiều với mô hình sans-IO. Muốn gửi gói tin, bạn gọi Tunnel::begin_encapsulate; việc đệm gói tin (nếu chưa có phiên) là do ứng dụng tự quyết định.

Nhắc lại giao thức WireGuard

Định dạng thông điệp của WireGuard hỗ trợ bốn loại: handshake init, handshake response, cookie reply và encrypted data.

Mỗi phiên WireGuard bắt đầu bằng trao đổi khóa Diffie-Hellman — một phép toán có chi phí vừa phải. Thiết kế của WireGuard bảo đảm việc lộ endpoint không tạo ra cơ hội tấn công từ chối dịch vụ (DoS). Bất kỳ ai biết khóa công khai của endpoint đều có thể tạo các handshake init giả và làm giả địa chỉ IP nguồn của gói UDP. Phía nhận buộc phải thực hiện xong phần DH của mình mới biết dữ liệu là giả — nhưng lúc đó tài nguyên CPU đã bị tiêu tốn.

Để ngăn chặn, WireGuard quy định một cơ chế cookie buộc phía đối diện phải chứng minh quyền sở hữu địa chỉ IP nguồn, từ đó cho phép giới hạn tần suất handshake theo từng địa chỉ IP. Các giới hạn này áp dụng cho toàn bộ interface chứ không riêng một tunnel.

Ánh xạ bất biến giao thức vào thiết kế API

Với một ngôn ngữ như Rust, dùng kiểu dữ liệu tốt nghĩa là cưỡng chế các bất biến ngay ở thời điểm biên dịch, để một số lỗi không thể xuất hiện ngay từ đầu. Ví dụ, quên áp dụng bộ giới hạn tần suất là lỗi rất khó phát hiện trong vận hành bình thường; định tuyến gói tin đến sai tunnel cũng vậy.

Trong sanstun, khái niệm interface và tunnel được ánh xạ trực tiếp thành hai kiểu Interface và Tunnel. Mọi gói tin đến đều đi qua Interface::receive — API công khai duy nhất phân tích datagram thô. Nó phân tích gói tin, áp giới hạn tần suất và trả về một ReceivedMessage với mỗi biến thể enum ứng với một loại thông điệp, hoặc trả về cookie reply nếu interface đang quá tải.

Điểm mấu chốt là các hàm khởi tạo cho những kiểu như HandshakeInitiation đều ở mức crate-private. Dù Tunnel có hàm công khai handle_handshake_initiation, cách duy nhất để gọi nó là đi qua Interface::receive trước để có được kiểu dữ liệu tương ứng — và Interface::receive tự động áp giới hạn tần suất mà bạn không cần bận tâm.

Mã hóa song song

Một trong những điểm mạnh nhất của sanstun là API mã hóa và giải mã thân thiện với xử lý song song. Ở tầng giao thức, WireGuard không chia sẻ trạng thái giữa các tunnel, nên việc song song hóa trên nhiều peer rất dễ dàng. Nhưng song song hóa trong cùng một tunnel thì sao?

WireGuard hoạt động ở tầng 3, nên một phiên với peer khác chứa lưu lượng của rất nhiều luồng mạng riêng lẻ. Mọi luồng mạng về cùng một máy chủ hay subnet đều bị xử lý tuần tự, bị giới hạn bởi tốc độ ChaCha20-Poly1305 của CPU. Để chống phát lại bởi kẻ quan sát trên đường truyền, gói tin dữ liệu WireGuard có bộ đếm gói tin tăng đơn điệu — bộ đếm này gắn với từng phiên và sẽ cần đồng bộ nếu truy cập từ nhiều luồng.

Để tránh điều đó, sanstun tách phần ghi sổ khỏi phần mã hóa thực tế. Gọi Tunnel::begin_encapsulate thực chất không mã hóa gì cả — nó kiểm tra phiên hiện tại, giành một bộ đếm gói tin, cập nhật trạng thái bộ định thời rồi trả cho bạn một PendingEncapsulate. Đối tượng này triển khai Send, nên có thể chuyển sang luồng khác để làm phần việc nặng.

Kết quả đo với cargo bench data_path cho thấy rõ:

  • Với gói 1280 byte, phần ghi sổ của việc mã hóa chỉ chiếm khoảng 2% tổng thời gian.
  • begin_encapsulate mất khoảng 21 ns, trong khi encrypt mất khoảng 934 ns.
  • begin_decapsulate khoảng 38 ns, decrypt khoảng 915 ns, commit_decapsulate khoảng 18 ns.

Nếu chuyển phần mã hóa thực tế sang luồng khác, tiềm năng tăng tốc có thể lên tới hơn 45 lần. Trên thực tế khó đạt được con số này vì còn chi phí chuyển PendingEncapsulate giữa các luồng và các hao phí khác, nhưng việc đẩy phần mã hóa sang luồng công nhân vẫn mang lại cải thiện thông lượng đáng kể.

Vì là thư viện sans-IO, sanstun không quan tâm bạn song song hóa theo cách nào. Nếu muốn đơn giản, bạn có thể gọi encrypt và decrypt ngay tại chỗ. Mục tiêu chính của thiết kế API là cho phép mã hóa song song trong khi vẫn giữ mọi thứ quanh Interface đơn luồng — nhờ đó bộ giới hạn tần suất bên trong hoạt động không cần khóa hay bộ đếm nguyên tử.

Kết luận

Với sanstun, fork boringtun tại firezone/boringtun chính thức bị ngừng khuyến nghị sử dụng. Nếu bạn đang xây dựng sản phẩm trên nền WireGuard — VPN, mạng mesh hay bất cứ thứ gì khác — Firezone hy vọng sanstun sẽ giúp công việc của bạn dễ dàng hơn.

Thư viện đã có mặt trên crates.io, là mã nguồn mở theo giấy phép Apache 2.0 và sẵn sàng đón nhận issue cùng pull request trên GitHub.

Đối với cộng đồng lập trình tại Việt Nam đang xây dựng các giải pháp VPN, mạng riêng ảo hay hệ thống kết nối biên, sanstun là một lựa chọn đáng cân nhắc: vừa tận dụng được hiệu năng của Rust, vừa dễ kiểm thử nhờ kiến trúc sans-IO, lại có thể mở rộng theo chiều ngang khi lưu lượng tăng cao.

Chia sẻ:FacebookX
Nội dung tổng hợp bằng AI, mang tính tham khảo. Xem bài gốc ↗