WireGuard hoạt động thế nào: cryptokey routing
Hiểu vì sao AllowedIPs vừa là routing table vừa là access list, cách Noise handshake và key rotation hoạt động để đọc đúng mọi file wg0.conf.
WireGuard hoạt động như thế nào, gói gọn trong một ý
WireGuard gắn mỗi packet với một public key. Cơ chế này có tên là cryptokey routing, và đó là toàn bộ thiết kế: dòng AllowedIPs bên cạnh một peer vừa là bảng định tuyến cho các packet rời khỏi máy của bạn, vừa là access control list cho các packet đến từ peer đó. Một setting, hai nhiệm vụ. Hãy đọc AllowedIPs theo cách này, rồi mọi file cấu hình WireGuard sẽ trở nên dễ hiểu.
Không có session table được lập theo địa chỉ IP và cũng không có user database. Một peer gồm một public key cùng tập địa chỉ mà key đó được phép sử dụng. Handshake và các timer tồn tại để duy trì liên kết này trong khi network bên dưới thay đổi. Nếu muốn có một tunnel hoạt động trước khi tìm hiểu lý thuyết, hãy dựng một tunnel bằng WireGuard VPN tự host trên VPS của bạn, rồi quay lại đây khi một dòng cấu hình khiến bạn khó hiểu.
AllowedIPs là bảng định tuyến và danh sách kiểm soát truy cập
Trước tiên, xét chiều outbound. Kernel định tuyến một packet đến device wg0 theo cách thông thường, thông qua bảng định tuyến chính. Sau đó WireGuard đối chiếu địa chỉ đích của packet với một bảng chứa các prefix được phép của mọi peer, theo thứ tự prefix dài nhất trước. Kết quả khớp xác định một peer, từ đó xác định public key, session key và UDP endpoint. Packet được mã hóa cho peer đó rồi gửi đến endpoint tương ứng.
Nếu AllowedIPs của không peer nào bao phủ địa chỉ đích, không có gì được gửi vì không có key để mã hóa packet.
ping: sendmsg: Required key not availableLỗi đó chỉ có một nguyên nhân: địa chỉ bạn muốn truy cập không được liệt kê trong peer nào. Một lỗi khác, ping: sendmsg: Destination address required, có nghĩa là đã tìm thấy peer phù hợp nhưng WireGuard không có endpoint cho peer đó vì chưa được cấu hình và cũng chưa học được endpoint nào.
Tiếp theo, xét chiều inbound. Một UDP packet đến listen port. WireGuard tìm session từ receiver index trong header, kiểm tra counter với sliding replay window, sau đó giải mã và xác thực payload. Chỉ sau bước đó nó mới đọc packet bên trong. Địa chỉ nguồn của packet bên trong phải nằm trong AllowedIPs của peer gửi. Nếu không, packet sẽ bị drop. Khi bật dynamic debug, kernel sẽ in lý do, trong một dòng như sau:
wg0: Packet has unallowed src IP (10.8.0.9) from peer 2 (203.0.113.10:51820)Đây là lý do peer ở phía server nhận được /32. Một peer được cấu hình với AllowedIPs = 10.8.0.2/32 chỉ được phép gửi packet từ 10.8.0.2 và không được gửi từ địa chỉ nào khác. Nếu ghi 0.0.0.0/0 ở đó, client duy nhất này có thể inject packet nhận là có bất kỳ địa chỉ nguồn nào bên trong tunnel, kể cả địa chỉ của client khác.
Các prefix chồng lấn được xử lý theo độ cụ thể vì quá trình tra cứu dùng longest prefix match. Các prefix giống hệt nhau trên 2 peer hoạt động khác: entry sẽ được chuyển sang peer được cấu hình sau cùng, còn peer đầu tiên ngừng nhận traffic đó mà không có lỗi nào được in ra. wg show wg0 allowed-ips in ra bảng thực tế đang nằm trong kernel. Đây mới là trạng thái có hiệu lực khi file trên disk và trạng thái đang chạy không còn khớp nhau.
Đọc file cấu hình với định tuyến cryptokey
Phía server:
[Interface]
Address = 10.8.0.1/24
ListenPort = 51820
PrivateKey = <server private key>
[Peer]
PublicKey = <laptop public key>
AllowedIPs = 10.8.0.2/32Phía client:
[Interface]
Address = 10.8.0.2/32
PrivateKey = <laptop private key>
[Peer]
PublicKey = <server public key>
Endpoint = vpn.example.com:51820
AllowedIPs = 0.0.0.0/0, ::/0
PersistentKeepalive = 25Cùng một keyword có ý nghĩa ngược nhau ở hai phía. Ở client, nó có nghĩa là “gửi mọi đích đến peer này”. Ở server, nó có nghĩa là “chỉ chấp nhận địa chỉ này từ peer”. Tính bất đối xứng nằm trong các giá trị, không nằm trong vai trò nào.
Một nửa số key này không thuộc protocol. Address, DNS, MTU, PostUp và SaveConfig thuộc về wg-quick, shell script dùng để đưa interface lên. Kernel không bao giờ nhìn thấy chúng. wg-quick strip wg0 in ra config rút gọn mà tool wg thực sự load. Đây là cách nhanh nhất để thấy sự phân tách đó.
Handshake thực sự thực hiện gì
Handshake của WireGuard là Noise_IKpsk2, thuộc Noise Protocol Framework. Phần IK mới hữu ích với sysadmin: public key tĩnh của responder đã được initiator biết trước, vì đó là PublicKey trong block [Peer] của bạn, còn initiator gửi public key tĩnh của chính nó bên trong message đầu tiên và message này được mã hóa. Vì vậy không có bước trao đổi certificate và cũng không có lượt trao đổi danh tính. Observer thụ động không thể biết key nào đang kết nối nếu không có private key của responder.
Đổi lại, handshake chỉ cần một round trip. Message khởi tạo dài 148 byte, response dài 92 byte, sau đó data được truyền ngay. Mỗi phía tạo một cặp ephemeral key Curve25519 mới cho mỗi handshake. Session key được tạo từ một chuỗi kết quả Diffie-Hellman kết hợp key tĩnh và ephemeral key. Sau đó ephemeral private key bị hủy, nhờ vậy có forward secrecy: nếu ai đó ghi lại traffic của bạn hôm nay rồi lấy được private key của server vào năm sau, họ vẫn không thể đọc phần traffic đã ghi lại.
Message khởi tạo handshake chứa timestamp TAI64N. Mỗi peer ghi nhớ timestamp lớn nhất đã nhận từ peer bên kia, nên initiation bị replay sẽ bị từ chối. Data packet chứa một counter 64-bit dùng làm nonce. Receiver duy trì một sliding window gồm các counter vừa nhận, nên có thể xử lý replay và việc packet bị đảo thứ tự nhiều mà không cần connection state kiểu TCP.
Session key tồn tại trong thời gian ngắn. Các timer được compile sẵn và không thể cấu hình.
The data behind this chart
[
{
"label": "REKEY_TIMEOUT",
"seconds": 5,
"notes": "resend a handshake initiation that got no answer"
},
{
"label": "KEEPALIVE_TIMEOUT",
"seconds": 10,
"notes": "send a keepalive after receiving data and sending none back"
},
{
"label": "REKEY_ATTEMPT_TIME",
"seconds": 90,
"notes": "give up on the handshake and report the peer as down"
},
{
"label": "REKEY_AFTER_TIME",
"seconds": 120,
"notes": "sender begins a fresh handshake for a new session key"
},
{
"label": "REJECT_AFTER_TIME",
"seconds": 180,
"notes": "the old session key is refused and traffic stops"
}
]Đây là các hằng số trong protocol specification, không phải số đo thực tế. 5 hằng số này điều khiển toàn bộ vòng đời session. Sau 120 giây sử dụng, sender bắt đầu một handshake mới. Sau 180 giây, key cũ bị từ chối hoàn toàn, nên traffic dừng cho đến khi handshake mới hoàn tất. Initiation không nhận được response sẽ được gửi lại sau mỗi 5 giây và bị bỏ sau 90 giây. Đây là lý do wg show hiển thị latest handshake dưới dạng thời gian tương đối, và cũng là lý do một tunnel đang hoạt động bình thường thường có thời gian này nhỏ. Nếu thời gian này tăng trong khi bạn đang chủ động gửi traffic, nghĩa là handshake đang fail, không phải tunnel đang idle.
Vì sao một peer không có vai trò client hoặc server
Hai đầu chạy cùng một đoạn code và cùng định dạng cấu hình. Không có server mode. Sự bất đối xứng mà bạn thấy đến từ Endpoint, còn Endpoint là tùy chọn.
Peer đã cấu hình endpoint có thể bắt đầu handshake. Peer không có endpoint sẽ chờ, sau đó lấy địa chỉ và port của đầu bên kia từ packet đầu tiên xác thực thành công. Endpoint đã học này được lưu lại và cập nhật mỗi khi có packet hợp lệ đến từ một địa chỉ mới. Đây là cách roaming hoạt động: laptop chuyển từ Wi-Fi sang mạng di động vẫn giữ cùng tunnel, vì session được xác định bằng key và index thay vì địa chỉ IP. Không có gì reconnect, vì theo nghĩa TCP thì chưa từng có kết nối nào.
Cơ chế này cũng tạo ra một điểm cần biết: peer có địa chỉ public luôn giữ public IP được biết gần đây nhất của đầu bên kia, và wg show sẽ hiển thị địa chỉ đó.
Các primitive cố định, không cần thương lượng
WireGuard không có danh sách ciphersuite. ChaCha20-Poly1305 dùng cho mã hóa có xác thực, Curve25519 dùng để thỏa thuận key, BLAKE2s dùng để băm và HKDF dùng để dẫn xuất key. Mọi deployment đều sử dụng các primitive này. Vì vậy không có giai đoạn thương lượng để phân tích và cũng không có đường hạ cấp xuống một tùy chọn yếu hơn. Đây là một đánh đổi thực tế: nếu một trong các primitive này bị phá vỡ, cách khắc phục là phát hành version mới của toàn bộ protocol và cập nhật ở cả hai đầu, không phải sửa config. Một quyết định duy nhất này loại bỏ phần lớn code và phần lớn dạng lỗi mà một tunnel dựa trên TLS thường phải xử lý. Đây cũng là điểm chính của phần so sánh trong WireGuard so với OpenVPN.
Vì sao cổng không phản hồi scanner
Mỗi thông báo handshake đều chứa một trường có tên mac1. Đây là MAC (mã xác thực thông báo) được tính trên thông báo bằng một key có nguồn gốc từ public key tĩnh của responder. Sender không biết public key đó sẽ không thể tạo mac1 hợp lệ, nên receiver sẽ drop packet đó mà không phản hồi. Không có lỗi, không có reset và không có thông báo ICMP.
Kết quả nhìn thấy là UDP scan không nhận được phản hồi nào.
sudo nmap -sU -p 51820 vpn.example.comnmap báo open|filtered. Đây cũng là kết quả nmap trả về khi firewall âm thầm drop packet đến một cổng. Cổng này có biểu hiện giống nhau dù WireGuard có đang listen hay không, ít nhất với những ai chưa có public key của bạn.
Một trường thứ hai, mac2, xử lý áp lực denial of service. Khi receiver đang chịu tải, nó trả lời một initiation hợp lệ bằng cookie reply dài 64-byte, gắn với source address của sender, rồi từ chối thực hiện các phép tính public key tốn tài nguyên cho đến khi sender gửi lại cookie đó. Cơ chế này xác nhận source address là thật trước khi tiêu tốn CPU và chỉ được kích hoạt khi hệ thống chịu tải.
Vì sao 0.0.0.0/0 biến peer thành default route của bạn
Vì AllowedIPs là routing table, AllowedIPs = 0.0.0.0/0, ::/0 yêu cầu peer đó nhận mọi destination. Đây chính là cấu hình full tunnel.
Cách routing làm cho cấu hình này hoạt động thú vị hơn chính dòng cấu hình. Một default route thông thường qua wg0 sẽ tạo vòng lặp, vì encrypted UDP packet chứa traffic của bạn cũng phải rời khỏi máy và sẽ khớp với chính default route đó. wg-quick tránh việc này bằng policy routing. Nó đánh dấu các packet đi ra của WireGuard bằng fwmark, đặt default route của tunnel vào một routing table riêng, rồi thêm các rule để chỉ traffic không có mark mới đi vào table đó. Chạy ip rule show để xem kết quả:
32764: from all lookup main suppress_prefixlength 0
32765: not from all fwmark 0xca6c lookup 51820
32766: from all lookup main0xca6c là 51820 ở dạng hex, và 51820 cũng là số của table. Rule suppress_prefixlength 0 khiến main table bỏ qua default route của chính nó, nên các route cụ thể như subnet nội bộ vẫn được ưu tiên, còn mọi traffic khác sẽ chuyển sang tunnel table. Split tunnel không cần các cấu hình này: danh sách hẹp hơn như AllowedIPs = 10.8.0.0/24, 10.20.0.0/16 sẽ trở thành các route thông thường trong main table.
Full tunnel không tự giải quyết việc phân giải tên, vì resolver mà client nhận từ mạng nội bộ thường vẫn được giữ nguyên và route đến resolver đó cụ thể hơn. Đây là một việc riêng, được đề cập trong DNS bị rò rỉ ra ngoài WireGuard tunnel.
PersistentKeepalive thực sự dùng để làm gì
WireGuard không gửi gì khi không có traffic. Không heartbeat, không refresh session, không có gói tin nào đi qua wire. Sự im lặng này giúp tiết kiệm pin, hỗ trợ trường hợp scanner nêu trên và làm hỏng một cấu hình cụ thể.
Một peer nằm sau NAT (network address translation) hoặc firewall stateful chỉ có thể được truy cập từ bên ngoài khi thiết bị đó còn mapping. Mapping này được tạo bởi một packet đi ra. Thời gian tồn tại phổ biến của UDP mapping bắt đầu từ khoảng 30 giây. Khi mapping hết hạn, middlebox sẽ drop các packet từ phía public. Tunnel sẽ trông như đã chết cho đến khi peer phía sau NAT gửi dữ liệu. PersistentKeepalive = 25 gửi một packet rỗng đã xác thực mỗi 25 giây. Khoảng thời gian này ngắn hơn thời gian tồn tại phổ biến tối thiểu, nên mapping vẫn được duy trì.
Đặt tùy chọn này trên peer nằm sau NAT. Server có địa chỉ public và UDP port đang mở không cần tùy chọn này. Đặt nó trên server chỉ làm phát sinh thêm traffic. Không nhầm tùy chọn này với keepalive tự động. Keepalive tự động được gửi 10 giây sau khi một peer nhận dữ liệu nhưng không có dữ liệu riêng để gửi lại. Cơ chế này luôn bật và không thể cấu hình.
Định tuyến LAN của bạn qua tunnel không phải là tính năng của WireGuard
Giả sử peer B nằm trên mạng gia đình 192.168.50.0/24 và peer A cần truy cập mạng đó. Hai hệ thống riêng biệt phải được cấu hình phù hợp với nhau, và chỉ một trong hai hệ thống là WireGuard.
Phần việc của WireGuard: thêm 192.168.50.0/24 vào AllowedIPs của B trên A. Việc này khiến A định tuyến prefix đó đến B và chấp nhận từ B các packet có source address tương ứng. Nếu thiếu cấu hình này, cryptokey routing không có key cho destination và không có quyền cho source.
Phần việc của kernel: trên B, net.ipv4.ip_forward phải là 1, nếu không kernel sẽ drop mọi packet đã giải mã nhưng không được gửi đến chính B. Forward chain của firewall trên B phải cho phép traffic này. Các host trong LAN cần có route quay về 10.8.0.0/24, hoặc B phải áp dụng source NAT để reply quay lại qua B.
WireGuard kết thúc phần việc khi chuyển packet đã giải mã cho kernel. Mọi bước sau đó đều là định tuyến và filtering thông thường của Linux. Vì vậy, lỗi này xuất hiện trong các counter của nft list ruleset hoặc trong ip -s link show wg0, chứ không phải trong wg show. Nếu muốn quản lý peer qua web interface, chạy wg-easy trong Docker sẽ tự tạo các peer entry cho bạn, nhưng forwarding rule vẫn thuộc về host.
Vì sao WireGuard chạy trong kernel
wg0 là một network device driver. Packet đi qua nó bằng routing stack thông thường, được mã hóa trong ngữ cảnh softirq, rồi đi ra qua một UDP socket mà không bao giờ chuyển sang userspace. Đây là lý do WireGuard đạt throughput cao. Đây cũng là lý do module này chỉ có khoảng bốn nghìn dòng code, đủ nhỏ để review và được merge vào mainline Linux 5.6 vào tháng 3 năm 2020. Ubuntu 24.04 và Debian 13 đã tích hợp sẵn, nên chỉ còn thiếu package wireguard-tools.
Việc hoạt động như một interface thông thường có những hệ quả thực tế. tcpdump -ni wg0 hiển thị các packet bên trong ở dạng plaintext, còn tcpdump -ni eth0 udp port 51820 hiển thị các packet bên ngoài đã mã hóa. So sánh hai bên cho biết ngay hướng nào đang bị lỗi. netfilter và traffic shaping xử lý wg0 như mọi link khác. Khi không có kernel module, chẳng hạn trên nền tảng container virtualisation dùng chung kernel của host, wireguard-go triển khai cùng protocol trong userspace thông qua một TUN device. Cách này làm throughput giảm đáng kể vì mỗi packet phải đi qua ranh giới kernel hai lần.
WireGuard không bảo vệ bạn khỏi những gì
Mô hình mối đe dọa được cố ý giới hạn, và một protocol kín đáo như vậy dễ khiến người dùng suy diễn quá mức. Hãy xác định rõ.
- WireGuard không che giấu việc bạn đang sử dụng WireGuard. Các handshake message có kích thước cố định, byte đầu tiên cho biết loại message, và transport sử dụng UDP. Deep packet inspection dễ dàng nhận diện lưu lượng này, và mạng không cho phép VPN có thể chặn nó. Obfuscation bị loại bỏ có chủ đích.
- WireGuard không che giấu lưu lượng hoặc thời điểm truyền. Payload chỉ được padding đến boundary 16-byte, nên người quan sát vẫn biết bạn gửi dữ liệu lúc nào và ước lượng được khối lượng dữ liệu.
- WireGuard lưu endpoint được biết gần đây nhất. Peer có địa chỉ public sẽ lưu public IP hiện tại của phía bên kia, và
wg showhiển thị địa chỉ đó. Kết hợp với tunnel address cố định trong config, thông tin này trở thành một định danh ổn định theo người dùng giữa các network. Trên VPS của bạn, điều này không có vấn đề. Đây cũng là lý do các dịch vụ thương mại thêm một layer bên trên protocol. - WireGuard xác thực một key, không xác thực một người. Ai giữ file private key thì là peer. Đặt
/etc/wireguardở mode 700 và các file key ở mode 600. - WireGuard không có revocation list và không có thời hạn hết hạn. Quyền truy cập kết thúc khi bạn xóa entry của peer khỏi mọi server đang lưu entry đó, còn static key vẫn có hiệu lực cho đến khi bạn xóa chúng.
Không điều nào trong số này khiến WireGuard yếu. Nó chỉ khiến WireGuard nhỏ gọn, và đó chính là mục đích: WireGuard xác thực và mã hóa, còn việc quản lý identity và phân bổ address được giao cho hệ thống bạn xây dựng bên trên. Một coordination layer như mô tả trong WireGuard so với Tailscale tồn tại để lấp đúng khoảng trống đó, bằng cách sử dụng cùng data plane mà bạn vừa đọc.
FAQ
Cryptokey routing trong WireGuard là gì?
Cryptokey routing là quy tắc liên kết mỗi packet với một public key. Mỗi peer entry chứa một danh sách prefix trong AllowedIPs. Ở chiều outbound, WireGuard chọn peer bằng cách so khớp destination của packet với danh sách của từng peer, ưu tiên prefix dài nhất, nên danh sách này hoạt động như một routing table. Ở chiều inbound, sau khi packet được giải mã và xác thực, source address bên trong packet phải nằm trong chính danh sách của peer đó; nếu không, packet sẽ bị drop. Vì vậy, danh sách này đồng thời hoạt động như một access control list. WireGuard không có cấu hình routing riêng và cũng không có internal firewall riêng, vì một danh sách này đảm nhiệm cả hai chức năng.
Tôi có cần đặt PersistentKeepalive trên cả hai peer không?
Không. Hãy đặt nó ở phía nằm sau NAT (network address translation) hoặc stateful firewall; thông thường đó là client. WireGuard không gửi gì khi idle, nên mapping cho phép phía bên kia kết nối đến peer đó sẽ hết hạn, thường trong vòng một phút, rồi tunnel trông như bị chết theo một chiều. PersistentKeepalive = 25 gửi một empty authenticated packet mỗi 25 giây và giữ mapping luôn mở. Peer có public address và UDP port đang mở thì không cần tùy chọn này.
Tại sao ping qua tunnel lại báo "Required key not available"?
Vì destination address không nằm trong AllowedIPs của bất kỳ peer nào. Cryptokey routing không tìm thấy key để mã hóa packet, nên kernel từ chối gửi packet đó. Chạy wg show wg0 allowed-ips rồi so sánh output với address bạn đang ping. Lỗi tương tự Destination address required là một vấn đề khác: một peer đã được khớp, nhưng WireGuard không có endpoint cho peer đó vì chưa có endpoint nào được cấu hình và cũng chưa nhận packet đã xác thực nào từ peer đó.
Firewall có thể phát hiện và chặn WireGuard không?
Có. WireGuard xác thực và mã hóa traffic của bạn, đồng thời không cố che giấu chính nó. Handshake message có kích thước cố định là 148 và 92 byte, byte đầu tiên của mỗi message cho biết loại message, còn transport sử dụng UDP. Vì vậy, deep packet inspection có thể dễ dàng nhận diện protocol này. Các network chặn UDP hoặc fingerprint protocol sẽ chặn được WireGuard. Muốn ẩn tunnel, bạn phải bọc nó trong một giao thức khác. Đây là một tool riêng, không phải setting của WireGuard.