🔐

Raspberry Pi で WireGuard + VPS

2023/03/14に公開

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
  • 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 の間に作業したほうが良いかと思われます。

  1. frps の構築
  2. WireGuard のインストール
  3. frpc の構築
  4. Pi-hole の構築

1. frps の構築

まず始めに、VPS 側の frp サーバソフトウェアである frps の構築作業をします。

VPS 上の任意の場所に以下の compose.yaml を作成します。

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 を作成し以下を設定します。

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 のインストールでは、以下の手順を踏んでいきます。

  1. sysctl の設定
  2. APT で WireGuard をインストール
  3. サーバサイド鍵ペアの作成
  4. WireGuard の設定ファイル作成
  5. クライアントに配布する接続設定ファイルの作成
  6. WireGuard のクライアント設定を更新
  7. WireGuard の起動

このうち、5, 6, 7 はクライアントを追加するごとに実施するのでシェルスクリプトで再利用可能にします。(クライアント追加用のシェルスクリプト作成 にセクションをまとめます)

sysctl の設定

追加設定をしない限り、異なる NIC 間でのパケットのやりとりができません。WireGuard は wg0 などの NIC を追加するので、これと eth0 間でパケット転送ができない場合 VPN をつないでも LAN ネットワークやインターネットと通信できません。
(という理解なのですが、間違っていたらすみません)

というわけで、IP 転送(フォワーディング)を有効にするため sysctl を編集します。
お好みのエディタで /etc/sysctl.conf を開き、末尾に以下を追記し保存します。

/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 を構築したあとで変更します。

ENDPOINTDNS については適宜修正してください。

このスクリプトを実行すると、Please input client name: と質問され、適当なクライアント名(端末名など)を入力し Enter を押すと各種キーが生成され接続設定ファイルが以下のように生成されます。

3. frpc の構築

frp クライアントソフトウェアである frpc の構築作業をします。

任意の場所に以下の compose.yaml を作成します。

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 を作成し以下を設定します。

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 を作成します。

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 から以下のアプリケーションをインストールします。

https://apps.apple.com/jp/app/id1441195209

1. トンネルの追加 をタップ 2. ファイル、アーカイブから作成
または QR コードから作成 をタップ

特定の Wi-Fi やモバイル回線に切り替わった時に自動的に VPN を有効化する場合は、設定の編集画面に入ったあと以下の手順で有効化できます。

3. 下にスクロールし
オンデマンド有効化 で適宜オンにする
4. 状態
tunnelStatusAddendumOnDemand をオンにする

VPN に接続されると、上部に VPN アイコンが表示されます。

Android

Android の場合、Google Play から以下のアプリケーションをインストールします。

https://play.google.com/store/apps/details?id=com.wireguard.android

1. 青い + をタップ 2. ファイル、アーカイブからインポート
または QRコードをスキャン をタップ

Tasker でオンデマンド有効化

Android の WireGuard アプリケーションの場合、特定の Wi-Fi やモバイル回線に切り替わった時に自動的に VPN を有効化する機能がないので Tasker などで対応する必要があります。

https://play.google.com/store/apps/details?id=net.dinglisch.android.taskerm

まず、WireGuard アプリケーション側で Tasker からの操作を有効化する必要があります。

WireGuard アプリケーションを開き、縦の三点リーダー(︙)をタップします。設定画面が開くので、詳細設定 の中にある 外部アプリからの制御 をオンにしておきましょう。

その上で、Tasker にて以下 2 つのタスクを作成します。

Activate VPN Task Deactivate VPN Task

Tasker FunctionTasker カテゴリの中にあり、FlashAlert カテゴリの中にあります。

Tasker Function には WireGuardSetTunnel という関数があるので、これの第一引数に「有効にするか無効にするか」、第二引数に「トンネル名」を指定します。
Flash は有効にしたか無効にしたかを確認できるようにするためのフラッシュ表示です。自身がわかるような表示であればなんでも良いかと思います。

このタスクを使って、以下のようにプロファイルを作成します。

特定の Wi-Fi 以外
に接続したら VPN 有効化
特定の Wi-Fi 以外 or モバイル回線
に接続したら VPN 有効化

Wifi ConnectedStateNet カテゴリにあります。Not Wifi ConnectedWifi ConnectedInvert させたものです。

  • 特定の Wi-Fi 以外に接続したら VPN 有効化
    • 「特定の Wi-Fi に接続した(Wifi Connected)」をトリガーとしています。「特定の Wi-Fi」として自宅 Wi-Fi の SSID を指定しています。
    • トリガーが有効になった場合のタスクとして、先ほど作成した Activate VPN を指定しています。
    • トリガーが無効になった場合のタスクとして、先ほど作成した Deactivate VPN を指定しています。
    • これにより、自宅以外の Wi-Fi を掴んだら VPN を有効化し、掴まなくなったら無効化するようになります。
  • 特定の 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 を通じてインターネット通信ができる ようになります。

Windows

Windows の場合、WireGuard 公式の Installation から Windows Installer をインストールします。

https://www.wireguard.com/install/

インストールし起動すると、以下のような画面が表示されます。ファイルからトンネルをインポート をクリックし、接続設定ファイルをインポートすることで追加できます。

Windows の場合 WireGuard アプリケーション自体に特定の Wi-Fi やモバイル回線に切り替わった時に自動的に VPN を有効化する機能がないので他の方法で対応するしかないのですが、ノートパソコンを自宅外で使う機会が今のところないので真面目に探していません。
GitHub でタスクスケジューラと Python を利用した自動化スクリプト を見つけましたが、使えるかどうか試していません。

以前、大学の Wi-Fi に自動ログインするために特定 SSID の Wi-Fi に接続したらログイン処理をする C# アプリケーションを作ったことがあるので、できそうではあるのですが…。

macOS

macOS の場合、App Store から以下のアプリケーションをインストールし利用できるようですが、今回は試していません。

https://apps.apple.com/us/app/wireguard/id1451685025

トラブルシューティング

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 コマンドでは確認できない のです。

https://serverfault.com/questions/1015322/the-wireguard-not-listening-on-port-after-started

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 サイズの設定がデフォルトのままだと発生するようです。

https://qiita.com/tabimoba/items/a99fb34d504b02437b9e

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 にするように設定します。

参考サイト

GitHubで編集を提案

Discussion