Dựng mạng VPN với WireGuard

Tiếp nối bài trước, Câu chuyện sử dụng VPN, tôi xin mô tả cách dựng một mạng VPN cho mục đích cá nhân bằng phần mềm WireGuard.

Mục đích sử dụng mạng VPN của tôi chỉ là để truy cập từ xa vào các thiết bị IoT của tôi nên mô hình VPN của tôi chỉ phù hợp với nhu cầu đó. Nếu bạn cần dùng VPN cho mục đích khác, ví dụ để vượt tường lửa, truy cập vào website bị chặn, thì bạn không nên trông đợi gì vào bài viết này.

Trước khi bắt tay vào việc, ta cần mường tượng sơ đồ mạng sẽ như thế nào:

Sơ đồ

Phải luôn có một máy có địa chỉ IP tĩnh làm server. Nó cũng đóng vai trò router trong mạng ảo (VPN) được tạo ra. Các gói tin từ máy con A sẽ được mã hóa, gửi lên server, server sẽ chuyển tiếp tới máy con B rồi gói tin được giải mã tại đó.

Ta sẽ chọn dải IP cho mạng ảo này. Ví dụ tôi chọn dải 192.168.2.0/24. Trong thực tế, ta cần chọn dải nào sao cho không đụng chạm đến bất cứ mạng LAN thật nào của từng máy con. Vì server đóng vai trò router nên ta sẽ dành địa chỉ 192.168.2.1 cho nó.

Cài đặt WireGuard

Việc cài đặt WireGuard thì khá đơn giản, đã có trên tài liệu. Trước khi cài WireGuard thì bạn phải cài trước header source code của Linux [^1]. Chuyện này khá đơn giản trên desktop và server, vì câu lệnh để cài WireGuard, sudo apt install wireguard sẽ tự kéo gói header của Linux về, tự cài. Nhưng trên máy tính nhúng như BeagleBone, Raspberry Pi, bạn phải tự chỉ định và cài gói header đó.

Trên Raspberry Pi, tên gói là raspberrypi-kernel-headers, nên bạn cài bằng:


sudo apt install raspberrypi-kernel-headers

Trên BeagleBone thì phức tạp hơn. Đầu tiên bạn phải xác định xem BeagleBone của bạn đang chạy Linux phiên bản nào:


$ uname -a

Linux beaglebone 4.4.88-ti-r125 #1 SMP Thu Sep 21 19:23:24 UTC 2017 armv7l GNU/Linux

Tìm kiếm gói header theo tên linux-headers-[version]:


$ apt search linux-headers-4.4.88-ti-r125

Sorting... Done

Full Text Search... Done

linux-headers-4.4.88-ti-r125/unknown,now 1stretch armhf

  Linux kernel headers for 4.4.88-ti-r125 on armhf

Ok, vậy là gói linux-headers-4.4.88-ti-r125 có tồn tại trong repo, hãy cài đặt nó:


$ sudo apt install linux-headers-4.4.88-ti-r125

Trong trường hợp bạn cấu hình APT repo cho Raspberry Pi, bạn có thể gặp lỗi này:


$ sudo apt update

Get:1 http://raspbian.raspberrypi.org/raspbian stretch InRelease [15.0 kB]

Get:2 http://deb.debian.org/debian unstable InRelease [233 kB]                                     

Hit:3 http://archive.raspberrypi.org/debian stretch InRelease   

Get:4 http://raspbian.raspberrypi.org/raspbian stretch/main armhf Packages [11.7 MB]

Err:2 http://deb.debian.org/debian unstable InRelease          

  The following signatures couldn't be verified because the public key is not available: NO_PUBKEY 8B48AD6246925553 NO_PUBKEY 7638D0442B90D010 NO_PUBKEY 04EE7237B7D453EC

Reading package lists... Done                                                                                                                  

W: GPG error: http://deb.debian.org/debian unstable InRelease: The following signatures couldn't be verified because the public key is not available: NO_PUBKEY 8B48AD6246925553 NO_PUBKEY 7638D0442B90D010 NO_PUBKEY 04EE7237B7D453EC

E: The repository 'http://deb.debian.org/debian unstable InRelease' is not signed.

N: Updating from such a repository can't be done securely, and is therefore disabled by default.

N: See apt-secure(8) manpage for repository creation and user configuration details.

Có thể khắc phục bằng cách chạy lệnh sau:


$ sudo apt install dirmngr

$ sudo apt-key adv --keyserver keyserver.ubuntu.com --recv-keys 04EE7237B7D453EC

Cấu hình cho WireGuard

Vì server sẽ đóng vai trò router, ta cần cấu hình kernel của server để cho phép "IPv4 forwarding":


$ echo 'net.ipv4.ip_forward=1' | sudo tee /etc/sysctl.d/99-ipv4_forwarding.conf

$ sudo sysctl -p /etc/sysctl.d/99-ipv4_forwarding.conf

Trên từng máy, ta làm theo hướng dẫn trên trang chủ để tạo key. Các file sinh ra privatekey, publickey nằm đâu cũng được, vì sau khi cấu hình xong ta có thể xóa nó đi.

Trên máy con A, ta tạo file /etc/wireguard/wg0.conf với nội dung như sau:

[Interface]
PrivateKey = private_key_cua_may_A
Address = 192.168.2.2/24
[Peer]
PublicKey = public_key_cua_server
Endpoint = ip_cua_server:51820
AllowedIPs = 192.168.2.0/24

Trên máy con B, ta tạo file tương tự, với nội dung như sau:

[Interface]
PrivateKey = private_key_cua_may_B
Address = 192.168.2.3/24

[Peer]
PublicKey = public_key_cua_server
Endpoint = ip_cua_server:51820
AllowedIPs = 192.168.2.0/24

Trên server, ta tạo file ở đường dẫn giống vậy, với nội dung như sau:


[Interface]
Address = 192.168.2.1/24
PostUp = iptables -A FORWARD -i wg0 -j ACCEPT; iptables -t nat -A POSTROUTING -o ens18 -j MASQUERADE; ip6tables -A FORWARD -i wg0 -j ACCEPT; ip6tables -t nat -A POSTROUTING -o ens18 -j MASQUERADE
PostDown = iptables -D FORWARD -i wg0 -j ACCEPT; iptables -t nat -D POSTROUTING -o ens18 -j MASQUERADE; ip6tables -D FORWARD -i wg0 -j ACCEPT; ip6tables -t nat -D POSTROUTING -o ens18 -j MASQUERADE
ListenPort = 51820
PrivateKey = private_key_cua_server
[Peer]
# May A
PublicKey = public_key_cua_may_A
AllowedIPs = 192.168.2.2/32
[Peer]
# May B
PublicKey = public_key_cua_may_B
AllowedIPs = 192.168.2.3/32

Lưu ý sự khác nhau giữa các file trên:

  • File cấu hình trên máy con thì chỉ cần 1 mục [Peer] để điền thông tin của server.

  • File cấu hình trên server thì có nhiều mục [Peer], mỗi cái tương ứng với một máy con.

  • Vì server đóng vai trò router nên ta có một chùm cấu hình iptables kèm theo. Trong các dòng iptables này, bạn cần thay ens18 bằng tên card mạng trên server của bạn (xem danh sách card mạng bằng lệnh ip link hay ngắn gọn hơn, ip l).

Một vài lưu ý khác:

  • Đừng bao giờ xài AllowedIPs = 0.0.0.0/0 trên file cấu hình của client (máy con), như một số bài hướng dẫn khác trên mạng, nếu không chức năng DNS sẽ tê liệt.

Kiểm tra

Sau khi ghi các file cấu hình kể trên, thử chạy


$ sudo wg-quick up wg0

$ ip a

để xem đã có network interface nào tên wg0 được tạo ra và gán địa chỉ chưa.

Ví dụ, đây là kết quả trên máy tôi:


$ ip a

1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN group default qlen 1000

    link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00

    inet 127.0.0.1/8 scope host lo

       valid_lft forever preferred_lft forever

    inet6 ::1/128 scope host 

       valid_lft forever preferred_lft forever

2: eno1: <NO-CARRIER,BROADCAST,MULTICAST,UP> mtu 1500 qdisc fq_codel state DOWN group default qlen 1000

    link/ether a0:2b:b8:24:0b:55 brd ff:ff:ff:ff:ff:ff

3: wlo1: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc mq state UP group default qlen 1000

    link/ether 9c:d2:1e:7d:f7:e7 brd ff:ff:ff:ff:ff:ff

    inet 192.168.1.88/24 brd 192.168.1.255 scope global dynamic noprefixroute wlo1

       valid_lft 79700sec preferred_lft 79700sec

    inet6 2001:ee0:5511:860:a8c1:937b:b26:8095/64 scope global temporary dynamic 

       valid_lft 1198sec preferred_lft 1198sec

    inet6 2001:ee0:5511:860:64ca:7ee4:ac78:5940/64 scope global dynamic mngtmpaddr noprefixroute 

       valid_lft 1198sec preferred_lft 1198sec

    inet6 fe80::236c:9478:b305:1c05/64 scope link noprefixroute 

       valid_lft forever preferred_lft forever

4: wg0: <POINTOPOINT,NOARP,UP,LOWER_UP> mtu 1420 qdisc noqueue state UNKNOWN group default qlen 1000

    link/none 

    inet 192.168.2.2/24 scope global wg0

       valid_lft forever preferred_lft forever

Bảng route:


$ ip r                                                                                                                            

default via 192.168.1.1 dev wlo1 proto dhcp metric 600 

169.254.0.0/16 dev wlo1 scope link metric 1000 

192.168.1.0/24 dev wlo1 proto kernel scope link src 192.168.1.88 metric 600 

192.168.2.0/24 dev wg0 proto kernel scope link src 192.168.2.2

Trên các máy con, thử ping về server bằng địa chỉ của mạng VPN:

ping 192.168.2.1

Trên máy con A, thử ping đến máy con B bằng địa chỉ của mạng VPN:

ping 192.168.2.3

Nếu việc ping thành công thì chúc mừng, bạn đã hoàn tất 95%.

Duy trì mạng VPN

Vì mạng VPN chỉ được tạo ra bằng lệnh wg-quick up, nên sau khi bạn khởi động lại máy, mạng sẽ biến mất. WireGuard có cung cấp file systemd service để máy tự chạy wq-quick up mỗi lần khởi động. Kích hoạt service này trên server bằng lệnh:

$ sudo systemctl enable wg-quick@wg0

Trong trường hợp của tôi, tôi cần truy cập từ laptop (máy A) vào board Raspberry Pi (máy B) nên tôi cũng cần kích hoạt service kia trên máy B. Riêng laptop thì không cần.

Làm được tới đây, nếu bạn khởi động lại máy B, bạn sẽ không ping được từ A vào B, cho dù wg0 vẫn đang hoạt động trên máy B và trên server. Tại sao? Đó là vì khi gói tin từ A lên server, server không biết máy B ở đâu để mà chuyển tiếp đến (nên nhớ là máy B đang nằm khuất sau 1 router nào đó nên nó không có địa chỉ IP public). Muốn server biết máy B ở đâu thì phải có 1 sự tương tác giữa máy B và server. Ta phải duy trì sự tương tác này để vị trí của máy B luôn được cập nhật. Để làm việc này, ta thêm tùy chọn PersistentKeepalive vào file của máy B như sau:

[Interface]
PrivateKey = private_key_cua_may_B
Address = 192.168.2.3/24

[Peer]
PublicKey = public_key_cua_server
Endpoint = ip_cua_server:51820
AllowedIPs = 192.168.2.0/24
PersistentKeepalive = 60

Thông số trên có nghĩa là cứ 60s, WireGuard trên máy B sẽ gửi một gói tin đặc biệt đến server để duy trì kết nối VPN.

Phụ chú

Trước đây, khi tôi chưa biết đến tham số PersistentKeepalive thì giải pháp của tôi là làm cho máy B cứ tự động ping đến server sau mỗi khoảng thời gian nhất định. Tôi sẽ ứng dụng systemd timer để làm việc này.

Tạo 2 file như sau trên máy B:

ping-wireguard-server.service:

[Unit]
Description=Ping WireGuard server

[Service]
Type=oneshot
ExecStart=/bin/ping -c 2 192.168.2.1

ping-wireguard-server.timer:


[Unit]
Description=Ping WireGuard server every minute

[Timer]
OnBootSec=2
OnUnitActiveSec=60

[Install]
WantedBy=timers.target

Copy 2 file trên vào /usr/local/lib/system/system/ rồi chạy lệnh sau để kích hoạt timer:

$ sudo systemctl enable ping-wireguard-server.timer
$ sudo systemctl start ping-wireguard-server.timer

Bạn cũng có thể làm công cụ ping tương tự với cron, nhưng tôi không thích cron lắm.

Truy cập SSH mà không cần bật WireGuard

Thật ra, nếu chỉ để SSH thì ta chỉ cần duy trì mạng VPN trên máy đích chứ không cần bật WireGuard trên máy của ta. Khi đó ta sẽ sử dụng server làm "đá bước dặm" để "nhảy cóc" đến máy bên kia, và kết nối từ máy ta đến server vẫn là kết nối trực tiếp, không qua VPN. Ví dụ ta sẽ dùng tính năng ProxyCommand của SSH.

Đầu tiên, mở file ~/.ssh/config và đặt lối tắt cho server:

Host my-server
    User user_tren_server
    Hostname ip_cua_server

Trong đó ip_cua_server là IP trực tiếp, không phải IP trong mạng VPN của server. Tiếp theo, tạo lối tắt cho máy đích

Host may-b
    User user_tren_may_b
    Hostname ip_vpn_may_b
    ProxyJump my-server

trong đó, ip_vpn_may_b là IP trong mạng VPN của máy đích.

Như vậy, chỉ cần gõ:


$ ssh may-b

ta đã có thể SSH vào máy B.

Xem tiếp bài viết liên quan: Khắc phục tình trạng VPN WireGuard bị treo

Một số góp ý của độc giả

(Góp ý của @VuNhanUit)

1. Có thể cài linux-headers theo version của kernel ngắn hơn bằng lệnh sau:

apt install linux-headers-$(uname -r)

2. Gần đây wireguard-tools trên repo unstable của debian bị outdated, gây ra conflict nếu cài thẳng wireguard bằng apt. Cách giải quyết: tải gói wireguard-tools.deb về cài tay, rồi mới cài wireguard được.

Chú thích:

[^1] Khi viết bài này, năm 2018, thì WireGuard chưa được kèm sẵn trong nhân Linux trong các distro, nên quá trình cài phải có bước biên dịch module động, nên cần đến source header của nhân. Các phiên bản mới hơn đã kèm sẵn WireGuard trong nhân Linux nên ta không cần "cài".