Raspberry Pi で WireGuard + VPS
Raspberry Pi 4 model B で VPN アプリケーションである WireGuard を立て、ConoHa VPS を経由して VPN 環境を構築します。
自宅ルータのポートを開けずに 自宅外から自宅内のサーバやパソコンにアクセスしたい、フリー Wi-Fi でもある程度安全に通信できる環境を作りたいと思い構築しています。
期待する結果
最終的に、以下のように動作するよう構成します。
環境
Raspberry Pi と VPS 間のポートフォワーディングに fatedier/frp を利用します。
そのうえで、Docker Hub にあるもののうち ユーザー数がある程度いて、GitHub Actions などで fatedier/frp のアップデートに追従しているもの として snowdreamtech/frp を選定しています。
もちろん、frp や Pi-hole を Docker で構築せずホスト OS にインストールしてもかまいません。
WireGuard では Peer to Peer で双方がサーバにもクライアントにもなりうるので、ピア(Peer)と書くのが正しいのですがこの記事ではっきりとサーバ・クライアントが存在するので、「クライアント」と表記します。
- Raspberry Pi 4 model B
- Raspberry Pi OS 64bit (Bullseye)
- WireGuard 1.0.20210223-1
- snowdreamtech/frps 0.52.1
- pi-hole/pi-hole 2023.05.2
- Ethernet の NIC として
eth0
を利用 - 各クライアントに割り振る IP として
172.16.0.x
を使用 - WireGuard のサーバ IP として
172.16.0.254
を使用
- ConoHa VPS
- 512 MB プラン
- Ubuntu 20.04.6 LTS
- snowdreamtech/frpc 0.52.1
- iOS Client Device
- iPhone SE 第 3 世代: iOS 16.7.1
- iPad Pro 第 3 世代: iPad OS 16.7.1
- WireGuard 1.0.16 (27) Backend 1e2c3e5a
- Android Client Device
- Google Pixel 6a: Android 14
- WireGuard 1.0.20230707
- Tasker 6.1.32
- Windows Client Device
- Windows 10 22H2 Build 22621.2428
- WireGuard 0.5.3
作業
記事の趣旨から外れるので Pi-hole の構築を一番最後にしていますが、WireGuard に DNS を設定する関係上 1 と 2 の間に作業したほうが良いかと思われます。
- frps の構築
- WireGuard のインストール
- frpc の構築
- Pi-hole の構築
1. frps の構築
まず始めに、VPS 側の frp サーバソフトウェアである frps の構築作業をします。
VPS 上の任意の場所に以下の compose.yaml
を作成します。
services:
frps:
image: snowdreamtech/frps
container_name: frps
volumes:
- ./frps.toml:/etc/frp/frps.toml
ports:
- 7000:7000
- 51820:51820/udp
restart: always
その後、compose.yaml
を置いた同じディレクトリに frps.toml
を作成し以下を設定します。
[common]
bindAddr = "0.0.0.0"
bindPort = 7000
token = "任意の文字列"
token
はポートフォワーディング時の認証トークンとなるので、固有のある程度長い推測不能な文字列にしてください。
設定を終えたら、docker compose up --build -d
で立ち上げます。
7000
ポートの開放も忘れずに。
2. WireGuard のインストール
VPN サーバとなる WireGuard を Raspberry Pi 4 model B にインストールします。
WireGuard のインストールでは、以下の手順を踏んでいきます。
-
sysctl
の設定 - APT で WireGuard をインストール
- サーバサイド鍵ペアの作成
- WireGuard の設定ファイル作成
- クライアントに配布する接続設定ファイルの作成
- WireGuard のクライアント設定を更新
- WireGuard の起動
このうち、5, 6, 7 はクライアントを追加するごとに実施するのでシェルスクリプトで再利用可能にします。(クライアント追加用のシェルスクリプト作成
にセクションをまとめます)
sysctl の設定
追加設定をしない限り、異なる NIC 間でのパケットのやりとりができません。WireGuard は wg0
などの NIC を追加するので、これと eth0
間でパケット転送ができない場合 VPN をつないでも LAN ネットワークやインターネットと通信できません。
(という理解なのですが、間違っていたらすみません)
というわけで、IP 転送(フォワーディング)を有効にするため sysctl を編集します。
お好みのエディタで /etc/sysctl.conf
を開き、末尾に以下を追記し保存します。
net.ipv4.ip_forward=1
すでに net.ipv4.ip_forward
に関する記述がある場合は当該行をコメントアウトするなどしてください。
その後、編集した内容を反映するため以下のコマンドを実行します。
sudo sysctl -p
APT で WireGuard をインストール
以下のコマンドを実行し、WireGuard をインストールします。
sudo apt update
sudo apt install wireguard
クライアントに接続設定を追加する際、QR コードで読み込めるようにする場合は qrencode
もインストールしておきましょう。
sudo apt install qrencode
サーバサイド鍵ペアの作成
サーバに保管する鍵ペア(秘密鍵と公開鍵)を作成します。
以下のコマンドで作成します。作成した鍵は /etc/wireguard/server-*.key
に保存し、所有者のみ読み書き可にします。
sudo wg genkey | sudo tee /etc/wireguard/server-private.key
sudo cat /etc/wireguard/server-private.key | wg pubkey | sudo tee /etc/wireguard/server-public.key
sudo chmod -v 600 /etc/wireguard/server-*.key
WireGuard の設定ファイル作成
WireGuard の設定ファイルを /etc/wireguard/wg0.conf
に作成します。
cat << EOF | sudo tee /etc/wireguard/wg0.conf
[Interface]
Address = 172.16.0.254
ListenPort = 51820
PrivateKey = $(sudo cat /etc/wireguard/server-private.key)
PostUp = iptables -A FORWARD -i %i -j ACCEPT; iptables -A FORWARD -o %i -j ACCEPT; iptables -t nat -A POSTROUTING -o eth+ -j MASQUERADE
PostDown = iptables -D FORWARD -i %i -j ACCEPT; iptables -D FORWARD -o %i -j ACCEPT; iptables -t nat -D POSTROUTING -o eth+ -j MASQUERADE
EOF
%i
には WireGuard の NIC 名が自動で置き換わります。この場合は wg0
です。
クライアント追加用のシェルスクリプト作成
クライアントの鍵ペア(秘密鍵と公開鍵)を作成します。また、セキュリティ向上を目的に事前共有鍵(PSK, Pre-Shared Key)も作成します。
作成にあたり、この作業はクライアントを追加するごとに実施するのでシェルスクリプトとして繰り返し実行可能にします。
DNS の IP アドレスはとりあえずルータの IP アドレスを指定しておきましょう。Pi-hole を構築したあとで変更します。
ENDPOINT
と DNS
については適宜修正してください。
このスクリプトを実行すると、Please input client name:
と質問され、適当なクライアント名(端末名など)を入力し Enter を押すと各種キーが生成され接続設定ファイルが以下のように生成されます。
3. frpc の構築
frp クライアントソフトウェアである frpc の構築作業をします。
任意の場所に以下の compose.yaml
を作成します。
services:
frpc:
image: snowdreamtech/frpc
container_name: frpc
volumes:
- ./frpc.toml:/etc/frp/frpc.toml
restart: always
network_mode: host
その後、compose.yaml
を置いた同じディレクトリに frpc.toml
を作成し以下を設定します。
serverAddr = "サーバIPアドレス"
serverPort = 7000
auth.method = "token"
auth.token = "frpsで設定したトークン"
[[proxies]]
name = "wireguard"
type = "udp"
localIP = "127.0.0.1"
localPort = 51820
remotePort = 51820
token
には 1 で作成した frps.toml
にて設定したトークンを、server_addr
には VPS の IP アドレスを設定します。
設定を終えたら、docker compose up --build -d
で立ち上げます。
ここまで終えると、WireGuard が VPS 経由で使えるようになります。
4. Pi-hole の構築
広告ドメインやトラッカードメインを正引きできなくすることで拒否する DNS サーバ Pi-hole を構築します。
任意の場所に以下の compose.yaml
を作成します。
services:
pihole:
container_name: pihole
image: pihole/pihole:latest
ports:
- 53:53
- 53:53/udp
- 8080:80/tcp
environment:
TZ: 'Asia/Tokyo'
volumes:
- './etc-pihole:/etc/pihole'
- './etc-dnsmasq.d:/etc/dnsmasq.d'
cap_add:
- NET_ADMIN
restart: always
作成したら、docker compose up --build -d
で立ち上げます。
http://hostname:8080/admin/
で閲覧できる Web interface のパスワードは docker compose logs pihole | grep random
で確認できます。
クライアントの設定
どのクライアントで利用するにしても、WireGuard サーバ側にクライアントを追加して接続設定ファイルを生成する必要があります。
「クライアント追加用のシェルスクリプト作成」で作成したシェルスクリプトを実行し、クライアント名を入力の上作成しておきます。
作成された接続設定ファイルは /etc/wireguard/clients/${CLIENT_NAME}/${CLIENT_NAME}.conf
にあります。
qrencode
をインストールしている場合は /etc/wireguard/clients/${CLIENT_NAME}/${CLIENT_NAME}.png
に設定追加用の QR コードが生成されています。
接続設定の追加時に「トンネル名」を決める必要があります。わかりやすければなんでも構わないかと思います。ここでは Test
で登録しています。
iOS
iOS の場合、App Store から以下のアプリケーションをインストールします。
1. トンネルの追加 をタップ |
2. ファイル、アーカイブから作成 または QR コードから作成 をタップ |
特定の Wi-Fi やモバイル回線に切り替わった時に自動的に VPN を有効化する場合は、設定の編集画面に入ったあと以下の手順で有効化できます。
3. 下にスクロールしオンデマンド有効化 で適宜オンにする |
4. 状態 でtunnelStatusAddendumOnDemand をオンにする |
VPN に接続されると、上部に VPN
アイコンが表示されます。
Android
Android の場合、Google Play から以下のアプリケーションをインストールします。
1. 青い + をタップ |
2. ファイル、アーカイブからインポート または QRコードをスキャン をタップ |
Tasker でオンデマンド有効化
Android の WireGuard アプリケーションの場合、特定の Wi-Fi やモバイル回線に切り替わった時に自動的に VPN を有効化する機能がないので Tasker などで対応する必要があります。
まず、WireGuard アプリケーション側で Tasker からの操作を有効化する必要があります。
WireGuard アプリケーションを開き、縦の三点リーダー(︙)をタップします。設定画面が開くので、詳細設定
の中にある 外部アプリからの制御
をオンにしておきましょう。
その上で、Tasker にて以下 2 つのタスクを作成します。
Activate VPN Task | Deactivate VPN Task |
---|---|
Tasker Function
は Tasker
カテゴリの中にあり、Flash
は Alert
カテゴリの中にあります。
Tasker Function
には WireGuardSetTunnel
という関数があるので、これの第一引数に「有効にするか無効にするか」、第二引数に「トンネル名」を指定します。
Flash
は有効にしたか無効にしたかを確認できるようにするためのフラッシュ表示です。自身がわかるような表示であればなんでも良いかと思います。
このタスクを使って、以下のようにプロファイルを作成します。
特定の Wi-Fi 以外 に接続したら VPN 有効化 |
特定の Wi-Fi 以外 or モバイル回線 に接続したら VPN 有効化 |
---|---|
Wifi Connected
は State
の Net
カテゴリにあります。Not Wifi Connected
は Wifi Connected
を Invert
させたものです。
-
特定の Wi-Fi 以外に接続したら VPN 有効化
- 「特定の Wi-Fi に接続した(
Wifi Connected
)」をトリガーとしています。「特定の Wi-Fi」として自宅 Wi-Fi の SSID を指定しています。 - トリガーが有効になった場合のタスクとして、先ほど作成した
Activate VPN
を指定しています。 - トリガーが無効になった場合のタスクとして、先ほど作成した
Deactivate VPN
を指定しています。 - これにより、自宅以外の Wi-Fi を掴んだら VPN を有効化し、掴まなくなったら無効化するようになります。
- 「特定の Wi-Fi に接続した(
-
特定の Wi-Fi 以外 or モバイル回線に接続したら VPN 有効化
- 「特定の Wi-Fi に接続した(
Wifi Connected
)」を反転(Invert
)させたNot Wifi Connected
をトリガーとしています。「特定の Wi-Fi」として自宅 Wi-Fi の SSID を指定しています。 - トリガーが有効になった場合のタスクとして、先ほど作成した
Activate VPN
を指定しています。 - トリガーが無効になった場合のタスクとして、先ほど作成した
Deactivate VPN
を指定しています。 - これにより自宅 Wi-Fi を掴まなくなったら VPN を有効化するので、モバイル回線でも自宅以外の Wi-Fi でも VPN を通じてインターネット通信ができる ようになります。
- 「特定の Wi-Fi に接続した(
Windows
Windows の場合、WireGuard 公式の Installation から Windows Installer をインストールします。
インストールし起動すると、以下のような画面が表示されます。ファイルからトンネルをインポート
をクリックし、接続設定ファイルをインポートすることで追加できます。
Windows の場合 WireGuard アプリケーション自体に特定の Wi-Fi やモバイル回線に切り替わった時に自動的に VPN を有効化する機能がないので他の方法で対応するしかないのですが、ノートパソコンを自宅外で使う機会が今のところないので真面目に探していません。
GitHub でタスクスケジューラと Python を利用した自動化スクリプト を見つけましたが、使えるかどうか試していません。
以前、大学の Wi-Fi に自動ログインするために特定 SSID の Wi-Fi に接続したらログイン処理をする C# アプリケーションを作ったことがあるので、できそうではあるのですが…。
macOS
macOS の場合、App Store から以下のアプリケーションをインストールし利用できるようですが、今回は試していません。
トラブルシューティング
UDP 通信が到達しているかを確認
WireGuard は UDP で通信します。UDP は TCP のように通信が到達しているかを確認しにくいのでなかなか厄介なのですが、1 つの手法としてネットワークデバイスを流れる通信を tcpdump
で見る方法があります。
apt install tcpdump
などで tcpdump をインストールしたうえで、VPS -> frps コンテナ -> frpc コンテナ -> Raspberry Pi の順でクライアントから WireGuard への通信が来ているかを確認すると良いでしょう。
sudo tcpdump -tttni any "udp port 51820"
51820
はポート番号なので、VPS で公開するポート番号などを変えている場合は適宜変更してください。
きちんと疎通できていると、以下のように UDP 通信が行われていることが確認できます。ファイヤーウォールなどの影響で通信が到達していない場合はなにも表示されません。
00:00:00.000000 lo In IP 127.0.0.1.51820 > 127.0.0.1.37070: UDP, length 128
00:00:00.152274 lo In IP 127.0.0.1.37070 > 127.0.0.1.51820: UDP, length 148
00:00:00.000982 lo In IP 127.0.0.1.51820 > 127.0.0.1.37070: UDP, length 92
00:00:00.089711 lo In IP 127.0.0.1.37070 > 127.0.0.1.51820: UDP, length 32
00:00:00.009923 lo In IP 127.0.0.1.37070 > 127.0.0.1.51820: UDP, length 112
00:00:00.000079 lo In IP 127.0.0.1.37070 > 127.0.0.1.51820: UDP, length 96
00:00:00.000031 lo In IP 127.0.0.1.37070 > 127.0.0.1.51820: UDP, length 96
00:00:00.000330 lo In IP 127.0.0.1.51820 > 127.0.0.1.37070: UDP, length 80
lsof は使えない
ポートを Listen しているかを確認する際によく使う lsof
コマンドですが、WireGuard の場合には利用できません。
WireGuard はカーネルモジュールとして動作するので、プロセスが開いたファイルのみを表示する lsof コマンドでは確認できない のです。
wg show
コマンドを活用する
sudo wg show all
と実行することで、ピアの情報を確認できます。
interface: wg0
public key: xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
private key: (hidden)
listening port: 51820
peer: xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
preshared key: (hidden)
endpoint: 127.0.0.1:57165
allowed ips: 172.16.0.2/32
latest handshake: 32 seconds ago
transfer: 1.08 MiB received, 3.86 MiB sent
peer: xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
preshared key: (hidden)
allowed ips: 172.16.0.1/32
リモートデスクトップ(RDP)すると真っ黒になる
MTU サイズの設定がデフォルトのままだと発生するようです。
Tasker で VPN がつながるまでトライする
具体的にどのバージョンから問題になっているのかわかりませんが、Pixel 7a, Android 14, Tasker 6.2.22 環境で一部条件のもとでは VPN 接続が確立されない現象を確認しています。
いろいろ検証したところ、WireGuard アプリケーションがバックグラウンドで起動していないと Tasker からうまくオンオフ操作ができないようです。
回避策として、VPN 接続が確立するまで VPN 接続をオンにする処理に書き換えてとりあえずは動作しています。
また、バックグラウンドで動作できるよう、一度トライしてダメな場合は WireGuard を意図的に起動させるようにしています。
3 つのタスクと 1 つのプロファイルを用意します。
タスク1つめ: VPN 接続が確立したとき
以下のように、%isVPN
変数に対して true
を代入タスクを作ります。
(後述する切断時のタスクと名前の一貫性がないのは許してください…)
Tasker では変数名に大文字を入れることでタスクを超えたグローバル変数となるので、必ず大文字を含めます。
タスク2つめ: VPN 接続が切断したとき
以下のように、%isVPN
変数に対して false
を代入するタスクを作ります。
タスク3つめ: VPN 接続タスクにリトライ処理を追加
以下のように、%loop_count
ローカル変数でループ回数を計算しつつ、VPN が接続確立されるまで(%isVPN
変数が true
になるまで)接続を試行します。
2 回目以降のループでは、WireGuard がバックグラウンドにいないと Tasker からの操作を受け付けない関係で、起動して即座にもともと起動していたアプリケーションに戻す動作をさせています。
プロファイル: VPN 接続確立時・切断時
以下のように、VPN の接続確立時に %isVPN
変数を true
、切断時に %isVPN
変数を false
にするように設定します。
Discussion