Post

WireGuard VPN - MTU Black Hole Explained

WireGuard VPN - MTU Black Hole Explained

WireGuard VPN: SSH/SCP Fail Nhưng Ping Vẫn OK – MTU Black Hole Explained.

Mô tả vấn đề

Kết nối WireGuard VPN từ Kubuntu PC về MikroTik Router tại nhà hoạt động bình thường: tunnel up, handshake OK, ping các host nội bộ thành công. Tuy nhiên, SSH và mount SMB vào các thiết bị trong homelab liên tục fail – cụ thể SSH báo Connection closed by <IP> port <port> sau vài giây chờ đợi.

Điều khó hiểu là một máy Windows khác kết nối cùng WireGuard server thì SSH hoạt động bình thường.

Môi trường:

  • WireGuard Client: Kubuntu PC, WG subnet 172.16.254.0/24
  • WireGuard Server: MikroTik RouterOS, interface wireguard_homelab
  • Homelab LAN: 192.168.1.0/24
  • SSH target: Synology NAS (192.168.1.230), port 9922
  • WireGuard MTU mặc định: 1420

Topology:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
                          ┌──── Internet ────┐
                          │                  │
                   ISP (PPPoE/CGNAT)    ISP (PPPoE/CGNAT)
                          │                  │
                          │           ┌──────┴──────┐
                          │           │  Kubuntu PC │
                          │           │  WG Client  │
                          │           │  wg0: .2    │
                          │           └──────┬──────┘
                          │                  │
                          │    WireGuard Tunnel (UDP 13231)
                          │    MTU mặc định: 1420
                          │    Overhead: ~80-90 bytes
                          │                  │
                   ┌──────┴──────────────────┴──────┐
                   │        MikroTik Router          │
                   │     WireGuard Server (wg: .1)   │
                   │   WG: 172.16.254.1/24           │
                   │   LAN: 192.168.1.1/24           │
                   └──────────────┬──────────────────┘
                                  │
                      Homelab LAN 192.168.1.0/24
                                  │
              ┌───────────────────┼───────────────────┐
              │                   │                   │
       ┌──────┴──────┐    ┌──────┴──────┐    ┌───────┴─────┐
       │ Synology NAS │    │   Proxmox   │    │ Raspberry Pi │
       │ .230         │    │   .200      │    │ .50          │
       │ SSH :9922    │    │             │    │              │
       └─────────────┘    └─────────────┘    └──────────────┘

Vấn đề: Packet lớn (SSH KEX ~1568 bytes) sau khi WG encapsulate
         vượt MTU đường truyền ISP → bị drop im lặng (black hole)
         Packet nhỏ (ping 64 bytes, SMB keepalive 72 bytes) → OK

Phân tích bằng tcpdump

Bắt packet trên interface wg0 (WireGuard) tại Kubuntu PC:

1
sudo tcpdump -i any host 192.168.1.230 and port 9922

Giai đoạn 1: TCP 3-way handshake – THÀNH CÔNG

1
2
3
15:27:24.516  Out  Kubuntu → synology:9922  [SYN]       mss 1380
15:27:24.520  In   synology:9922 → Kubuntu  [SYN,ACK]   mss 1460
15:27:24.520  Out  Kubuntu → synology:9922  [ACK]

Handshake hoàn tất. Cả 2 bên đã đồng ý thiết lập kết nối TCP. SYN và SYN-ACK là packet nhỏ (~60 bytes), dễ dàng đi qua tunnel.

Lưu ý MSS (Maximum Segment Size): Kubuntu đề xuất MSS 1380 (= MTU 1420 – 40 bytes TCP/IP header). Synology đề xuất MSS 1460 (= MTU 1500 – 40). Theo RFC, 2 bên sẽ dùng giá trị nhỏ hơn → MSS = 1380.

Giai đoạn 2: SSH banner exchange – THÀNH CÔNG

1
2
3
4
15:27:24.520  Out  Kubuntu → synology  [PSH] seq 1:43     (42 bytes – SSH client banner)
15:27:24.524  In   synology → Kubuntu  [ACK]
15:27:24.554  In   synology → Kubuntu  [PSH] seq 1:22     (21 bytes – SSH server banner)
15:27:24.554  Out  Kubuntu → synology  [ACK]

SSH client gửi banner (thường là SSH-2.0-OpenSSH_9.x), server trả lời. Packet nhỏ (< 50 bytes), đi qua tunnel bình thường.

Giai đoạn 3: SSH Key Exchange – FAIL

1
2
3
15:27:24.554  Out  Kubuntu → synology  [PSH] seq 43:1611  (1568 bytes – SSH KEX init)
15:27:24.557  In   synology → Kubuntu  [ACK] ack 43, sack {1411:1611}
                                        ↑ CHỈ ACK TỚI BYTE 43, SACK 1411-1611

Đây là điểm bùng phát. Kubuntu gửi SSH KEX init dài 1568 bytes – đây là packet lớn đầu tiên trong session, chứa danh sách thuật toán mã hóa, key exchange methods, MACs, v.v.

Synology trả lời ACK chỉ tới byte 43 (banner cũ), nhưng SACK {1411:1611} – nghĩa là nó nhận được phần cuối (byte 1411-1611, 200 bytes) nhưng mất phần giữa (byte 43-1411, 1368 bytes).

Giai đoạn 4: Retransmission loop – THẤT BẠI HOÀN TOÀN

1
2
3
4
5
6
7
8
9
10
11
15:27:24.561  Out  seq 43:1411  (1368 bytes) ← retransmit lần 1
15:27:24.768  Out  seq 43:1411               ← retransmit lần 2
15:27:25.184  Out  seq 43:1411               ← retransmit lần 3
15:27:26.032  Out  seq 43:1411               ← lần 4
15:27:27.696  Out  seq 43:1411               ← lần 5
15:27:31.024  Out  seq 43:1411               ← lần 6
15:27:37.936  Out  seq 43:1411               ← lần 7
15:27:51.248  Out  seq 43:1411               ← lần 8
15:28:17.872  Out  seq 43:1411               ← lần 9
15:29:13.168  Out  seq 43:1411               ← lần 10
15:29:24.555  In   synology → Kubuntu  [FIN] ← server timeout, đóng connection

10 lần retransmit cùng segment seq 43:1411 (1368 bytes), không bao giờ được ACK. Server timeout sau ~2 phút, gửi FIN, SSH client nhận được Connection closed.

Trong khi đó, SMB keepalive trên cùng tunnel (72 bytes mỗi packet) vẫn hoạt động bình thường suốt quá trình SSH fail:

1
2
15:27:30.768  Out  Kubuntu → synology:445  [PSH] 72 bytes → ACK OK ✅
15:28:32.208  Out  Kubuntu → synology:445  [PSH] 72 bytes → ACK OK ✅

Chẩn đoán: MTU Black Hole

Tại sao packet nhỏ OK nhưng packet lớn fail?

WireGuard encapsulate mỗi packet bằng cách thêm header (khoảng 60 bytes) và đóng gói trong UDP/IP outer (28 bytes). Tổng overhead ~80-90 bytes.

Bảng so sánh packet size thực tế trên đường truyền vật lý (sau khi WireGuard encapsulate):

Loại trafficInner size+ WG overheadTrên wireKết quả
Ping mặc định (56 byte payload)84 bytes+88 bytes~172 bytes✅ OK
SSH banner (42 bytes)94 bytes+88 bytes~182 bytes✅ OK
SMB keepalive (72 bytes)124 bytes+88 bytes~212 bytes✅ OK
SSH KEX segment (1368 bytes)1420 bytes+88 bytes~1508 bytes❌ DROP

Đường truyền giữa client và server có MTU thực tế thấp hơn 1508 bytes (do PPPoE, VLAN tag, CGNAT, hoặc các lớp encapsulation khác của ISP). Packet vượt quá MTU đường truyền bị drop im lặng.

Tại sao gọi là “Black Hole”?

Theo lý thuyết, khi router trung gian gặp packet quá lớn và có Don’t Fragment (DF) bit set, nó phải gửi ICMP Fragmentation Needed (Type 3, Code 4) về source. Source sẽ giảm packet size – cơ chế này gọi là Path MTU Discovery (PMTUD).

Nhưng PMTUD thường fail trong thực tế vì:

  1. WireGuard outer packet có DF bit set → router trung gian không thể fragment
  2. Nhiều ISP (đặc biệt tại Việt Nam) và firewall drop hoặc rate-limit ICMP → source không nhận được thông báo “packet quá lớn”
  3. Kết quả: packet “biến mất” không dấu vết – black hole. Source chỉ biết “không có ACK” và retransmit vô ích cho tới khi timeout.

Xác minh bằng ping với payload lớn

1
2
3
4
5
6
7
8
9
10
11
12
13
14
# Ping nhỏ - OK
$ ping -M do -s 56 192.168.1.230
64 bytes from 192.168.1.230: icmp_seq=1 ttl=63 time=4.48 ms  ✅

# Tăng dần - tìm ngưỡng
$ ping -M do -s 1200 192.168.1.230
1208 bytes from 192.168.1.230: icmp_seq=1 ttl=63 time=3.94 ms  ✅

$ ping -M do -s 1350 192.168.1.230
1358 bytes from 192.168.1.230: icmp_seq=1 ttl=63 time=4.15 ms  ✅

# Vượt ngưỡng - FAIL
$ ping -M do -s 1372 192.168.1.230
8 packets transmitted, 0 received, 100% packet loss  ❌

Option -M do set DF bit (Don’t Fragment), buộc packet bị drop nếu quá MTU thay vì bị fragment. Kết quả cho thấy ngưỡng nằm giữa 1350-1372 bytes payload.

Tại sao ping mặc định vẫn OK?

Ping mặc định gửi 56 bytes payload (tổng 84 bytes trên IP), sau WG encapsulate chỉ ~172 bytes – nằm rất xa dưới ngưỡng MTU. Đây là lý do nhiều người kết luận sai “tunnel hoạt động bình thường” chỉ vì ping thông. Ping nhỏ không chứng minh tunnel hoạt động cho mọi loại traffic.

Tại sao Windows laptop SSH OK nhưng Kubuntu fail?

Có nhiều khả năng:

  • Windows WireGuard client có thể đang chạy MTU thấp hơn (mặc định hoặc cấu hình)
  • Windows SSH client có thể đàm phán MSS nhỏ hơn
  • Windows đi qua đường ISP khác, có MTU đường truyền lớn hơn

Giải pháp: Giảm MTU trên WireGuard Interface

Cách tính MTU phù hợp

Từ kết quả ping: max payload OK = 1350, vậy:

1
2
3
Max inner packet = 1350 + 28 (IP + ICMP header) = 1378 bytes
→ WG interface MTU nên ≤ 1378
→ Để an toàn (dự phòng biến động đường truyền): MTU = 1340

Với MTU = 1340, TCP sẽ tự tính MSS = 1340 – 40 = 1300 bytes, đảm bảo packet sau WG encapsulate = 1340 + 88 ≈ 1428 bytes – nằm trong giới hạn an toàn.

Áp dụng trên WireGuard client (Linux)

Chỉnh file /etc/wireguard/wg0.conf:

1
2
3
4
5
6
7
8
9
10
[Interface]
PrivateKey = <key>
Address = 172.16.254.2/24
MTU = 1340          # ← Thêm dòng này

[Peer]
PublicKey = <server_public_key>
AllowedIPs = 172.16.254.0/24, 192.168.1.0/24
Endpoint = <WAN_IP>:13231
PersistentKeepalive = 25

Restart tunnel:

1
2
3
4
5
6
sudo wg-quick down wg0
sudo wg-quick up wg0

# Verify MTU
ip link show wg0 | grep mtu
# Expected: mtu 1340

Áp dụng trên WireGuard client (Windows)

Mở WireGuard GUI → Edit tunnel → thêm MTU = 1340 vào section [Interface] → Save → Deactivate + Activate.

Áp dụng trên WireGuard server (MikroTik RouterOS)

/interface wireguard set wireguard_homelab mtu=1340

Set trên server giúp đảm bảo packet từ LAN host gửi ngược về client cũng được giới hạn. Nên set cả client lẫn server để cover traffic 2 chiều.

Xác minh sau khi fix

tcpdump cho thấy MSS đã giảm

1
17:55:56.078  Out  [SYN]  mss 1300   ← Giảm từ 1380 xuống 1300 ✅

SSH KEX init được ACK đầy đủ

1
2
17:55:56.115  Out  [PSH] seq 43:1611  (1568 bytes – SSH KEX init)
17:55:56.118  In   [ACK] ack 1611     ← ACK TOÀN BỘ 1611 bytes ✅

So với trước khi fix:

1
15:27:24.557  In   [ACK] ack 43, sack {1411:1611}  ← Chỉ ACK 43 bytes ❌

Không còn retransmission

Toàn bộ 62 packet clean, không có retransmit nào. SSH session hoàn tất bình thường, bao gồm key exchange, authentication, channel open, interactive data, và graceful FIN/ACK close.

Bổ sung: Firewall Mangle MSS Clamp (Tùy chọn, defense-in-depth)

Một số hướng dẫn đề xuất dùng firewall mangle change-mss clamp-to-pmtu trên MikroTik thay vì set MTU:

/ip firewall mangle
add chain=forward protocol=tcp tcp-flags=syn in-interface=wireguard_homelab \
    action=change-mss new-mss=clamp-to-pmtu
add chain=forward protocol=tcp tcp-flags=syn out-interface=wireguard_homelab \
    action=change-mss new-mss=clamp-to-pmtu

Tuy nhiên, trong case này mangle clamp-to-pmtu không đủ. Lý do:

  • MSS clamp chỉ rewrite MSS field trong SYN/SYN-ACK – tức là chỉ giới hạn TCP segment size
  • Nhưng nếu MTU trên WG interface vẫn là 1420, TCP stack trên client vẫn có thể tạo segment lên tới 1380 bytes (1420 - 40)
  • clamp-to-pmtu dựa trên MTU của outgoing interface trên MikroTik, không phải MTU thực tế của đường truyền phía ngoài

Kết luận: Set MTU trực tiếp trên WG interface là giải pháp triệt để và đáng tin cậy hơn. MSS clamp có thể dùng bổ sung như defense-in-depth, nhưng không thay thế được việc set MTU đúng.

Quick Reference: Quy trình debug MTU issue trên WireGuard

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
1. Triệu chứng:
   - Ping OK (packet nhỏ)
   - SSH, SCP, HTTP, SMB mount fail hoặc cực chậm (packet lớn)
   - tcpdump thấy retransmission loop trên segment > 1300 bytes

2. Xác minh:
   - ping -M do -s <size> <target>
   - Tăng dần size từ 56 → 1400, tìm ngưỡng fail
   - tcpdump kiểm tra SACK gap trong SSH handshake

3. Tính MTU:
   - max_payload = ping size lớn nhất mà 100% packet OK
   - inner_max = max_payload + 28  (IP + ICMP header)
   - safe_mtu = inner_max - 20 đến 40 bytes  (dự phòng)
   - Giá trị phổ biến: 1340 (an toàn cho hầu hết ISP Việt Nam)

4. Fix:
   - Client: MTU = <safe_mtu> trong [Interface] section wg config
   - Server: set mtu trên WG interface
   - Verify: tcpdump xem MSS trong SYN đã giảm

5. Giá trị MTU gợi ý:
   - 1420: WG mặc định, thường quá lớn cho PPPoE hoặc CGNAT
   - 1380: Phù hợp nếu đường truyền có MTU 1500 thuần (Ethernet không PPPoE)
   - 1340: An toàn cho đa số ISP Việt Nam (PPPoE + CGNAT)
   - 1280: Giá trị tối thiểu theo IPv6, hoạt động mọi nơi nhưng giảm throughput

Bài học rút ra

  1. Ping không đủ để kết luận tunnel hoạt động. Luôn test bằng service thực tế (SSH, SCP, curl) hoặc ping với payload lớn (ping -M do -s 1400).

  2. MTU black hole là invisible. Không có error message, không có ICMP feedback. Packet lớn âm thầm biến mất. Chỉ có tcpdump mới cho thấy pattern retransmission.

  3. WireGuard MTU mặc định (1420) thường quá lớn cho đường truyền có PPPoE, CGNAT, hoặc các lớp encapsulation khác – rất phổ biến với ISP tại Việt Nam.

  4. Luôn set MTU khi deploy WireGuard. Đừng dùng giá trị mặc định. Dành 2 phút test bằng ping -M do để tìm ngưỡng chính xác, hoặc set giá trị an toàn 1340 nếu không muốn test.

  5. MSS clamp không thay thế được MTU tuning. Clamp-to-pmtu là defense-in-depth tốt nhưng không giải quyết gốc rễ khi MTU của WG interface đã sai.


Bài viết dựa trên case thực tế xảy ra trên hệ thống homelab cá nhân. Hy vọng giúp ích cho anh em network/system engineer khi gặp triệu chứng tương tự.

This post is licensed under CC BY 4.0 by the author.