nftablesとwireguardでVPN

# 背景
Ubuntu 22.04.4  LTSにWireGuardを入れてVPNを構築してみました．Ubuntu 22.04のfirewallはiptablesの後継としてnftablesが採用されています．

多くの資料ではWireGuardに合わせてiptablesを設定しているため，nftablesの設定を紹介したいと思います．および備忘録．

WireGuardではServer-Clientという考え方ではないらしいですが，ここでは便宜上，Server-Client型の構成で話を進めます．

**WireGuardめっちゃ簡単でよかった．**

# 目標

+ UbuntuをサーバーとしてVPNを作る
+ クライアント同士が相互通信できるようにする

# 環境

OSとVirtual Private Network上でのIPアドレス，グローバルIPは次を想定します．

+ Server
    + WebArena Indigo Ubuntu Server 22.04.4
        + WAN上でのアドレス：200.0.0.1
        + VPN上でのアドレス：10.0.0.1
+ Client
    + Client1: Ubuntu 22.04.4
        + VPN上でのアドレス：10.0.0.2
    + Client2: Android 12
        + VPN上でのアドレス：10.0.0.3

あと必要なのはWireGuardとnftables．WireGuardはaptでインストールできます．

```
sudo apt install wireguard
```

# 構築

## 鍵の作成

WireGuardではVPNに接続するそれぞれが秘密鍵を持ち，公開鍵暗号を用いて通信を行います．今回はServer１台とClient２台のため，予め計３つの秘密鍵を作っておきます．

WireGuardでは，秘密鍵の生成に「wg genkey」を使います．ssh-keygenと違い公開鍵は同時に作られません．そのため「wg pubkey」を用いて秘密鍵から公開鍵を作ります．

```terminal
$ wg genkey > server.key # UH87bg632a/mziGJV1RC02N2jvVzsavVTP0Z1MHHOXM=
$ cat server.key | wg pubkey > server.pubkey # IGtAbGCQqQosm/WYMUMDe6DCmCYxuymd3LiF2f25VCI=
```

これを必要な回数やって秘密鍵と公開鍵を準備します．便宜上，ここでは次のような鍵になったとします．

|デバイス|秘密鍵|公開鍵|
|-|-|-|
|Server|AAAAA=|aaaaa=|
|Client1|BBBBB=|bbbbb=|
|Client2|CCCCC=|ccccc=|

## Server側のWireGuardの設定

WireGuardでは`/etc/wireguard/`フォルダ内に設定ファイルを置きます．設定ファイルは`VPNのインターフェイス名.conf`という名前にしなければいけません．

例えば`/etc/wireguard/server.conf`というふうに作成したとしましょう．この場合，Virtual Private Networkにはserverというインターフェイスを通してアクセスします．そのため設定後にipコマンドを使ったりするとWireGuardがいい感じにインターフェイスを自動で作ってくれるのを確認できます．

```terminal
$ ip l
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN mode DEFAULT group default qlen 1000
    link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
2: ens1: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc fq_codel state UP mode DEFAULT group default qlen 1000
    link/ether 6c:4b:90:c8:b4:ad brd ff:ff:ff:ff:ff:ff
~~~
42: server: <POINTOPOINT,NOARP,UP,LOWER_UP> mtu 1420 qdisc noqueue state UNKNOWN mode DEFAULT group default qlen 1000
    link/none 
```

このインターフェイス名は何でも良いのですが，ここではServerの設定ファイル名を`server.conf`とします．公式や他の資料では`wg0.conf`を用いてることが多いです．

設定は次のようにします．

```bash:/etc/wireguard/wg0.conf
[Interface]
PrivateKey = AAAAA=
Address = 10.0.0.1/32
ListenPort = 51000

[Peer]
# Client1
PublicKey = bbbbb=
PresharedKey = aFOnB=
AllowedIPs = 10.0.0.2/32

[Peer]
# Client2
PublicKey = ccccc=
PresharedKey = +NICu=
AllowedIPs = 10.0.0.3/32
```

説明がいる部分は「PresharedKey」と「AllowdIPs」だと思います．一応ですが「＃」はコメント．

PresharedKeyは共通鍵暗号で用いる鍵を指定します．これを用いることで公開鍵暗号と一緒に共通鍵暗号も使うようになって量子コンピュータが実用化されたときの対策になるんだとか？．．．ちょっとよくしらないから詳しいこと言えない．セキュリティレベルが上がるという認識では間違いない．

AllowdIPsは「どの宛先のパケットをここに流しますか？」という設定項目です．Clinet2の場合だと「10.0.0.3宛のパケットはClient2に流します」という意味です．

AllowedIPsは「0.0.0.0/0」を指定することで全ての宛先を対象にしたり，「192.168.1.0/24」とすることで192.168.1.1〜192.168.1.254までの宛先を対象にしたり，カンマ区切りで複数指定したりもできます．

これが基本の設定ですがファイアウォールの都合上，後でもう少し手を加えます．

## Server側のnftablesの設定

WANに直接つながっているServer機であるならファイアウォールの設定は欠かせません．設定は様々ありますが，ここでは次のような設定がなされていたとします．

```bash:/etc/nftables.conf
#!/usr/sbin/nft -f

flush ruleset

table inet filter {
        chain INPUT {
                type filter hook input priority filter; policy drop;

                iifname "lo" accept
                ct state invalid drop
                ct state {established, related} accept

                tcp dport 22 drop comment "Default SSH port is not used"
                tcp dport 60000 accept comment "SSH port"
                
                ip protocol icmp accept
        }

        chain OUTPUT {
                type filter hook output priority filter; policy accept;
        }

        chain FORWARD {
                type filter hook forward priority raw; policy drop;

                ct state established, related accept
        }
}
```

この状態ではserverインターフェイスに関わるフォワーディングがなされません．フォワーディングの処理は基本的にドロップさせてますからね．この状態では目標のClientは相互通信ができません．

そのためWireGuardが動き始めたら次のように変化してほしいです．

```bash:/etc/nftables.conf
~~略~~
        chain FORWARD {
                type filter hook forward priority raw; policy drop;

                ct state established, related accept
                iifname "server" accept # NEW!
                oifname "server" accept # NEW!
        }
}
```

さらに，WireGuardが止まったら元に戻ってもほしいです．nftablesではルールの消去にhandle番号を用いることが悩みどころで，例示したものを実現するのは難しそうです．

対してチェインの消去であればhandle番号が不要のため，チェインの追加/消去をWireGuardの立ち上げ/終了時に行わせる方針で解決します．

それに伴ってファイアウォールの設定を次のようにします．

```bash:/etc/nftables.conf
#!/usr/sbin/nft -f

flush ruleset

table inet filter {
        # NEW!
        chain ACCEPTING {
                mark set 1
        }

        chain INPUT {
                type filter hook input priority raw; policy drop;

                iifname "lo" accept
                ct state invalid drop
                ct state {established, related} accept

                tcp dport 22 drop comment "Default SSH port is not used"
                tcp dport 60000 accept comment "SSH port"
                
                ip protocol icmp accept
                
                udp dport 51000 accept comment "VPN port"
        }
        
        chain OUTPUT {
                type filter hook output priority filter; policy accept;
        }

        # NEW!
        chain FORWARD-RAW {
                type filter hook forward priority raw; policy accept;

                mark set 0

                ct state established, related goto ACCEPTING
        }
        
        # NEW!
        chain FORWARD-SEC {
                type filter hook forward priority security; policy drop;
                
                mark 1 accept
        }
}
```

FORWARDの部分を２つに分け，もともとの設定はpriorityがrawの方に，dropする場所をpriority securityのところに置いています．またパケットに対してacceptしたいときは，単にacceptを書くのではなく「goto ACCEPTING」と書いています．

このようにするのは「途中のチェイン内でacceptされたパケットは次のチェインに進む」というのを考慮してです．

悪い例として，serverインターフェイスから来た/出ていくパケットを許可するルールを設定したく，次のようなルールを定めたとします．

```bash
chain FORWARD-RAW { # handle 1
        type filter hook forward priority raw; policy accept;
        iifname server accept # handle 2
}

chain FORWARD-SEC { # handle 3
        type filter hook forward priority security; policy drop;
        oifname server accept # handle 4
}
```

ここにserverインターフェイスからens1に向かうパケットがきました．すると，そのパケットはhandle1に入り，handle2でacceptされます．ですが，handle2でacceptされても，handle3に入っていきそこのpolicy dropで捨てられてしまいます．

こんなふうに上流でacceptされたとしても下流のルールでdropされます．そのため「このパケットはもう絶対にacceptしてね」というマークをつける等の工夫が必要となるのです．

マークする工夫を加えたら上記の例は次のようになるでしょう．

```bash
chain FORWARD-RAW {
        type filter hook forward priority raw; policy accept;
        mark set 0
        iifname server mark set 1
}

chain FORWARD-SEC { # handle 3
        type filter hook forward priority security; policy drop;
        mark 1 accept
        oifname server accept
}
```

これで基本のnftablesの設定は終わりです．`nft -f /etc/nftables.conf`を実行して設定を読み込ませましょう．

最後にserver.confにチェインを追加する設定を書き加えます．

```bash:/etc/wireguard/server.conf
[Interface]
PrivateKey = AAAAA=
Address = 10.0.0.1/32
ListenPort = 51000

PostUp = nft add chain inet filter %i-FORWARD "{ type filter hook forward priority filter; policy accept ; }"
PostDown = nft delete chain inet filter %i-FORWARD

PostUp = nft add rule inet filter %i-FORWARD mark 1 accept
PostUp = nft add rule inet filter %i-FORWARD iifname "%i" goto ACCEPTING
PostUp = nft add rule inet filter %i-FORWARD oifname "%i" goto ACCEPTING

[Peer]
# Client1
PublicKey = bbbbb=
PresharedKey = aFOnB=
AllowedIPs = 10.0.0.2/32

[Peer]
# Client2
PublicKey = ccccc=
PresharedKey = +NICu=
AllowedIPs = 10.0.0.3/32
```

PostUpは立ち上げ時に実行するコマンド，PostDownは終了時に実行するコマンドです．%iはインターフェイス名を表します．ここではserverですね．

これにてServer側の設定は終了です．

# Client 1 (Ubuntu PC)

WireGuardの良いところとして，ServerとClient側で設定の書き方が大きく変わらないという点があります．もう，さくっと終わります．

Client 1のPCに次のファイルを作成します．もちろんなければWireGuardをインストールしてからです．ここではインターフェイス名はclient1にしました．

```bash:/etc/wireguard/client1.conf
[Interface]
PrivateKey = BBBBB=
Address = 10.0.0.2/32
ListenPort = 51000

[Peer]
PublicKey = aaaaa=
PresharedKey = aFOnB=
EndPoint = 200.0.0.1:51000
AllowedIPs = 10.0.0.0/24
```

EndPointは接続先のServerのグローバルIPアドレスとポート番号を指定しています．

またAllowedIPsには「10.0.0.0/24」が設定されています．これにより，このインターフェイスには「10.0.0.1〜10.0.0.254」が宛先のパケットが流されます．これでClient2（10.0.0.3）へ向けたパケットをVPN上に流せるようになります．

# Client 2 (Android Smart Phone)

Androidではアプリが出されています．

[WireGuard by WireGuard Development Team](https://play.google.com/store/apps/details?id=com.wireguard.android&hl=ja&gl=US)

おそらく使い方は悩まないはず．スクショ取って説明書こうと思っていたけど，セキュリティのためかスクショを取れなかったため諦めました．

ただし，設定項目にDNSとあって，これについては悩むかもしれない．これはAllowedIPsに「0.0.0.0/0」を設定したときに使う項目です．本当に全てのパケットをVPNに流してしまうとドメイン名の解決をするときに困っちゃうため，「このDNSに向けたパケットはVPNに流さないでね」と伝えるための項目です．

# 終わりに

WireGuard自体は簡単に設定できるのでよかったのです．

しかし，自分はファイアウォールの設定に詳しくなく，その周りでつまずいて４日くらい悩みました．今，振り返るとWireGuardを信じファイアウォールを集中的に疑っていればよかったかなーという気がします．


この記事が誰かの参考になれば幸いです．

# 補足

+ nftablesのルールで「goto ACCEPTING」の代わりに「mark set 1」を使う方法もありますが，ひと手間のgotoを加えることで素早くチェインから脱出できます．gotoではジャンプ先で評価が終わった後に呼び出し元に戻らないという仕組みを利用しています

+ 「wg-quick up server」や「wg-quick up client1」で起動できます．停止は「wg-quick down server」です
+ スタートアップさせる場合は「systemctl enable wg-quick@server」のようにします