Qiita Teams that are logged in
You are not logged in to any team

Log in to Qiita Team
Community
OrganizationEventAdvent CalendarQiitadon (β)
Service
Qiita JobsQiita ZineQiita Blog
6
Help us understand the problem. What are the problem?
@tinoue@github

tailscale VPNとRaspberryPiを使って、拠点間での同一セグメントネットワークを構築する

はじめに

最近家族がアパートを借りて別に住むようになったのですが、実家においてあるNASやMacのTimemachine Backupサーバにアクセスできないということで、VPNを使用して実家とアパートの2拠点間で、同一セグメントのネットワークを構築しようと思いました。

最初はWireGuradを使ってみようと思ったんですが、問題になるのはインターネットからのアクセスをどうするかです。ルータのポート空けたり、固定IPとかDynamic DNSとかも面倒。セキュリティも気になります。

tailscale VPN

検索して出てきたのがtailscale VPN。アプリケーションをインストールしてGoogleアカウントでログインするだけでP2PベースのVPN網を構築できます。Win/Mac/スマホ/Linux(RaspberryPi)に対応で、今のところ無料で100台まで利用可能。これはすばらしいということで、tailscaleを使うことにしました。

参考:Internet Watch: 100台まで無料のVPNサービス「tailscale」、リンクだけでマシンのシェアも可能!?

GRE(GRETAP) : Ethernet over IP

拠点間同一セグメントネットワーク構築には、IP(L3)のトンネリングではなくEthernet(L2)のトンネリングが必要となります。調べてみると、GREプロトコルがL2/L3の両方のトンネリングに対応しており、Linuxでも使いやすそうなのでこれにしました。

ここにあるRedHatの資料「IPv4 でイーサネットフレームを転送するための GRETAP トンネルの設定」がまさにそれです:

RaspberryPiを仮想スイッチングハブ(仮想ブリッジ)とし、両拠点に用意したその仮想スイッチングハブを、同じく仮想のLANケーブル(実態はVPN + GREのL2トンネリング)でお互いに接続するイメージとなります。

下準備

RaspberryPiを2台用意

お互いの拠点に設置し、tailscaleをインストールしておきます。
セットアップはこちらを参照: Raspberry Pi 3/4 (RaspberryPi OS 64bit) 一括セットアップ手順

tailscaleの認証を無期限にしておくために、tailscaleのWeb管理画面でraspberry piの設定を選択し(右端の":"アイコン)、"disable key expiry"を選択してください。

Setting up Tailscale on Linux

なお、拠点間で動画などサイズの大きいデータの受け渡しをしたい場合、RaspberryPi 4と64bit OSの使用をお勧めします。当初片側を有線100MbpsのRaspberryPi3にしていたのですが、拠点間で動画データを流すのは厳しかったです。

拠点間のブロードバンドルータ設定

ブロードバンドルータをたとえば以下のように設定します。同じネットワークアドレスですが、お互い重ならないIPアドレスを使用します。

拠点A: ルータアドレス192.168.1.1/24, DHCPアドレス配布範囲: 192.168.1.2 - 192.168.1.99
拠点B: ルータアドレス192.168.1.100/24, DHCPアドレス配布範囲: 192.168.1.101 - 192.168.1.199

Raspberry Piの設定

必要なカーネルモジュールの追加

  • GREプロトコルのサポート
sudo modprobe ip_gre
lsmod | grep gre

上記で、ip_greが表示されることを確認。なお、GRE over IPv6をする場合はip6_greが必要になりますが、tailscaleのMTUが1280とIPv6で許容される最小のMTUサイズになっている関係で、IPv6版は使いませんでした。

  • 仮想ブリッジのサポート
sudo modprobe bridge
lsmod | grep bridge

上記で、bridgeが表示されることを確認。

ブリッジ経由の通信内容をebtablesでフィルタリングするため、br_netfilterも追加。

$ sudo modprobe br_netfilter
$ lsmod | grep netfilter

念のため、ブリッジのフィルタリングが有効になっていることを確認。

$ sysctl net.bridge.bridge-nf-call-iptables
net.bridge.bridge-nf-call-iptables = 1
$ sysctl net.bridge.bridge-nf-call-ip6tables
net.bridge.bridge-nf-call-ip6tables = 1

再起動後も有効になるように設定

/etc/modules
ip_gre
bridge
br_netfilter

ebtables, iproute2のインストール

iproute2はインストール済みかと思いますが、念のためインストール。なお、ブリッジ関連の操作は旧来のbridge-utils(brctlコマンド)がiproute2(ipコマンド)で置き換えられているのでipコマンドだけで十分なのですが、ネットではbrctlを使った例も多く、参考にするために入れました。
ebtablesはコマンド自体は最初から存在するのですが、明示的にインストールしないと/etc/ethertypesがない旨のエラーが出ました。

sudo apt-get -y install ebtables iproute2 bridge-utils

eth0のdhcpを無効化

ちょっと戸惑ったところですが、仮想ブリッジ(br_lan)を作成しそこにeth0を接続した場合、LANに接続されるのはeth0ではなくbr_lanという扱いになります。そのため、eth0にはIPアドレスを付与せず、br_lanがDHCPでアドレスを取得するように設定します。

/etc/dhcpcd.conf
denyinterfaces eth0

/etc/network/interfaces の設定

現行のRaspberryPi OSでは/etc/network/interfacesは空となっていますが、ここに記述した設定も普通に有効になるようです。

関係するインタフェースは以下の4つ:

  • eth0: 物理インタフェース
  • gretap1: GRETAP L2トンネルインタフェース。gretap0は予約済みなのでgretap1の名前にしています。
  • br_lan: 仮想ブリッジインタフェース。ここにeth0とgretap1を接続します。デフォルトではSTP(ネットのループ検出)が有効になってますが、不要なので無効化。
  • tailscale0: tailscale VPNインタフェース(これはtailscaledが自動で作成するので、ここでは記述不要)

MTUについて

トンネリングインタフェースのMTUは、"実際のMTU - トンネリングに必要なヘッダ"とするのが一般的な模様です。今回の場合、gretap1のMTUはtailscale0のMTU値1280から38(GREヘッダ: 4, Ether: 14, IPヘッダ: 20)を引いた値、1242が適切です。

と最初は思ったのですが、今回はLayer2のトンネリングのため、経路途中のMTUに影響されることなく、入ってきたEthernetフレームをそのまま相手側に渡す必要があります。そのため、ブリッジ接続するbr_lan, eth0, gretap1のMTUはすべて1500に固定。その上で、gretap1から tailscale0(MTU:1280)を経由するパケットはフラグメント化が必須となります。
gretap1インタフェース作成コマンド pre-up ip link add gretap1 type gretap remote ... ignore-df nopmtudiscの中のignore-df nopmtudiscオプションがそれです。

ちなみにこの辺、10年以上の長い間バグがあったようなのですが、タイミングがいいことに5.10.10と割と最近のカーネルで修正されています -
https://phabricator.vyos.net/T3555

MTUについてはまだ怪しい点があるため、後述します。

追記:gretap1のMTUは1500以上でないとパケットがドロップしました。実験的には1514でよさそうですが、1542を設定します。

DHCPパケットのフィルタリング設定

お互いの拠点にブロードバンドルータが存在するので、拠点間をDHCPリクエストなどが通らないようにブロックします。ブリッジレベルの通信のフィルタリングとなるので、いつものiptablesではなくebtablesでの設定が必要です(これを知らずにiptablesでパケットを遮断しようとして結構はまりました)。interfacesファイルでの該当行は以下。IPv4/IPv6のDHCP、およびIPv6のRouter Solicitation/Advertisementがgretap1から送信されないようにブロックします。

     # Block DHCP
     post-up ebtables -I FORWARD -o gretap1 -p IPv4 --ip-proto udp --ip-dport 67:68 -j DROP
     post-up ebtables -I FORWARD -o gretap1 -p IPv6 --ip6-proto udp --ip6-dport 67:68 -j DROP
     # Block Router Solicitation/Advertisement
     post-up ebtables -I FORWARD -o gretap1 -p IPv6 --ip6-proto ipv6-icmp --ip6-icmp-type 133:134 -j DROP 

なお余談ですが、tailscaleは実際に通信が確立した段階(systemdでの一連のサービスの起動の後)でiptablesの設定を追加します。その設定はルール先頭に挿入されるため、iptablesのINPUT, FORWARDにDROP系の独自ルールを入れている場合(tailscaleのACCEPTルールが先に適用されて)無効化されるので注意が必要です。

main側のRaspberry piの/etc/network/interfaces

設定ファイル中remote以降のIPアドレスは適宜置き換えてください。

/etc/network/interfaces

auto lo
iface lo inet loopback

auto eth0
iface eth0 inet static
     address 0.0.0.0
     pre-up ip link set dev eth0 promisc on
     post-down ip link set eth0 promisc off

auto gretap1
iface gretap1 inet static
     address 0.0.0.0
     pre-up ip link add gretap1 type gretap remote 100.86.118.83 ignore-df nopmtudisc # main
     #pre-up ip link add gretap1 type gretap remote 100.92.41.28 ignore-df nopmtudisc # sub
     pre-up ip link set dev gretap1 promisc on # arp off # multicast off
     up ip link set dev gretap1 up mtu 1542  # 1500 + 42(GRE:4 + Ether:18 + IP:20)
     # Block DHCP
     post-up ebtables -I FORWARD -o gretap1 -p IPv4 --ip-proto udp --ip-dport 67 -j DROP
     post-up ebtables -I FORWARD -o gretap1 -p IPv4 --ip-proto udp --ip-dport 68 -j DROP
     post-up ebtables -I FORWARD -o gretap1 -p IPv6 --ip6-proto udp --ip6-dport 67 -j DROP
     post-up ebtables -I FORWARD -o gretap1 -p IPv6 --ip6-proto udp --ip6-dport 68 -j DROP
     # Block Router Solicitation/Advertisement
     post-up ebtables -I FORWARD -o gretap1 -p IPv6 --ip6-proto ipv6-icmp --ip6-icmp-type 133:134 -j DROP
     # Clamp IPv4 TCP MSS to avoid fragmentation.
     # MSS = 1280(tailscale MTU) - 42(GRE:4 + Ether:18 + IP:20) - 40(IP/TCP) = 1198
     post-up iptables -t mangle -I POSTROUTING -m physdev --physdev-out gretap1 -p tcp --tcp-flags SYN,RST SYN -j TCPMSS --set-mss 1198

auto br_lan
iface br_lan inet dhcp
     pre-up ip link add name br_lan type bridge
     bridge_stp off
     #bridge_waitport 0
     #bridge_fd 0
     pre-up ip link set dev br_lan promisc on
     pre-up ip link set dev eth0 master br_lan
     pre-up ip link set dev gretap1 master br_lan
     pre-up ip link set dev br_lan up mtu 1500

     post-down ip link set dev eth0 nomaster
     post-down ip link set dev gretap1 nomaster
     post-down ip link del gretap1
     post-down ip link del br_lan

sub側のRaspberry Piの/etc/network/interfaces

mainとほぼ同一。gretapインタフェースのリモートアドレスが違うだけです。

/etc/network/interfaces

auto lo
iface lo inet loopback

auto eth0
iface eth0 inet static
     address 0.0.0.0
     pre-up ip link set dev eth0 promisc on
     post-down ip link set eth0 promisc off

auto gretap1
iface gretap1 inet static
     address 0.0.0.0
     #pre-up ip link add gretap1 type gretap remote 100.86.118.83 ignore-df nopmtudisc # main
     pre-up ip link add gretap1 type gretap remote 100.92.41.28 ignore-df nopmtudisc # sub
     pre-up ip link set dev gretap1 promisc on # arp off # multicast off
     up ip link set dev gretap1 up mtu 1542  # 1500 + 42(GRE:4 + Ether:18 + IP:20)
     # Block DHCP
     post-up ebtables -I FORWARD -o gretap1 -p IPv4 --ip-proto udp --ip-dport 67:68 -j DROP
     post-up ebtables -I FORWARD -o gretap1 -p IPv6 --ip6-proto udp --ip6-dport 67:68 -j DROP
     # Block Router Solicitation/Advertisement
     post-up ebtables -I FORWARD -o gretap1 -p IPv6 --ip6-proto ipv6-icmp --ip6-icmp-type 133:134 -j DROP
     # Clamp IPv4 TCP MSS to avoid fragmentation.
     # MSS = 1280(tailscale MTU) - 42(GRE:4 + Ether:18 + IP:20) - 40(IP/TCP) = 1198
     post-up iptables -t mangle -I POSTROUTING -m physdev --physdev-out gretap1 -p tcp --tcp-flags SYN,RST SYN -j TCPMSS --set-mss 1198

auto br_lan
iface br_lan inet dhcp
     pre-up ip link add name br_lan type bridge
     bridge_stp off
     #bridge_waitport 0
     #bridge_fd 0
     pre-up ip link set dev br_lan promisc on
     pre-up ip link set dev eth0 master br_lan
     pre-up ip link set dev gretap1 master br_lan
     pre-up ip link set dev br_lan up mtu 1500

     post-down ip link set dev eth0 nomaster
     post-down ip link set dev gretap1 nomaster
     post-down ip link del gretap1
     post-down ip link del br_lan

ブリッジの構成を確認

$ brctl show
bridge name     bridge id               STP enabled     interfaces
br_lan          8000.46242a251582       no              eth0
                                                        gretap1

ここで問題発生

上記設定を終えRaspberry Piを再起動、sudo arping <相手側ルータのIPアドレス> -i gretap1 -0 で取り合えず疎通確認してみますが、ifconfig gretap1でみるとパケット数が増えておらず相手が受け取ってません。syslogをよくみると以下のエラーが出てました。

Jun 11 09:07:29 raspberrypi4 tailscaled[576]: Drop: Unknown{100.92.41.28:0 > 100.86.118.83:0} 84 unknown

どうやらtailscaleがGREプロトコルを許可していない模様です... ここにくるまで紆余曲折があり、かなり時間を費やしていたのにがっかり。

tailscaleにGREプロトコル対応を追加

幸いtailscaleはgo言語で記述されたオープンソースで、githubにて公開されています (https://github.com/tailscale/tailscale)。
あきらめる前にやるだけやってみようと、syslogに出ていた"Drop"や"Unknown"でソースの該当箇所を探してみました。

この辺(filter.go) を見るに、ICMP, UDP, TCP以外は通しておらず、GRE IPプロトコルがUnknownとしてDropされているので、GREプロトコルを通すように修正します。また、この辺(packet.go) にもGRE IPプロトコル対応が必要です。

開発はRaspberry Pi上で行い、./build_dist.sh tailscale.com/cmd/tailscaledで出来たtailscaledバイナリを/usr/sbinにコピー。
テストケースも追加して、go test ../... でテストも確認。

Pull Requestをあげておきました: GRE L2/L3 tunneling protocol support。バージョン1.12でマージ予定とのことです。

めでたく当初の目標が達成できました!

もしかしてtailscaleの改造いらなかった?

man ip-linkでgretapの設定オプション見ると、encapオプションというのがありました。GRE IPパケットをUDPでさらにカプセル化してくれるようです(Foo-Over-UDP)。気づくのが遅かった...

    encap { fou | gue | none } - specifies type of
    secondary UDP encapsulation. "fou" indicates Foo-
    Over-UDP, "gue" indicates Generic UDP
    Encapsulation.

いろいろ確認してみる

ebtablesの動作を確認する

以下のコマンドで、適用回数(pcnt)を確認(元の表示は改行がおかしいので修正済み)。
ブリッジレベルなのにLayer3のフィルタリングが出来るってなんかすごいですね。

$ sudo ebtables -L --Lc
Bridge table: filter

Bridge chain: INPUT, entries: 0, policy: ACCEPT

Bridge chain: FORWARD, entries: 5, policy: ACCEPT
-p IPv6 -o gretap1 --ip6-proto ipv6-icmp --ip6-icmp-type 133:134/0:255 -j DROP , pcnt = 68 -- bcnt = 5400
-p IPv6 -o gretap1 --ip6-proto udp --ip6-dport 68 -j DROP , pcnt = 0 -- bcnt = 0
-p IPv6 -o gretap1 --ip6-proto udp --ip6-dport 67 -j DROP , pcnt = 0 -- bcnt = 0
-p IPv4 -o gretap1 --ip-proto udp --ip-dport 68 -j DROP , pcnt = 10 -- bcnt = 3280
-p IPv4 -o gretap1 --ip-proto udp --ip-dport 67 -j DROP , pcnt = 107 -- bcnt = 35249
Bridge chain: OUTPUT, entries: 0, policy: ACCEPT

ちゃんとIPv6のRAなどが遮断されてます。ちなみに最初はこれを遮断していなかったため、IPv6対応サイト(Qiitaもです)の表示に不具合が出てました。

MTUを確認するが、どうも怪しい

  • RapsberryPi同士でMTUを確認

RaspberryPi同士のpingにて拠点間のMTUが1500であることを確認してみます。
本来なら1500 - 20(IP header) - 8(ICMP header) = 1472バイト送信できるはずが、1458バイトしか送信できません。差分の14バイトがどこから来ているのか不明(Ethernetヘッダー?)。

pi@raspberrypi4:~ $ ping -M do -s 1458 raspberrypi3.local
PING raspberrypi3.local (192.168.2.121) 1458(1486) bytes of data.
1466 bytes from 192.168.2.121 (192.168.2.121): icmp_seq=1 ttl=64 time=12.3 ms
1466 bytes from 192.168.2.121 (192.168.2.121): icmp_seq=2 ttl=64 time=11.8 ms
^C
--- raspberrypi3.local ping statistics ---
2 packets transmitted, 2 received, 0% packet loss, time 2ms
rtt min/avg/max/mdev = 11.842/12.057/12.272/0.215 ms
pi@raspberrypi4:~ $ ping -M do -s 1459 raspberrypi3.local
PING raspberrypi3.local (192.168.2.121) 1459(1487) bytes of data.
From 192.168.2.68 (192.168.2.68) icmp_seq=1 Frag needed and DF set (mtu = 1500)
From 192.168.2.68 (192.168.2.68) icmp_seq=2 Frag needed and DF set (mtu = 1500)
^C
--- raspberrypi3.local ping statistics ---
2 packets transmitted, 0 received, +2 errors, 100% packet loss, time 3ms

実際、拠点間のRaspberryPi同士のscpコマンドがストールする。よろしくない状況です。
ifconfig gretap1でもTX errorが出てます。おそらくこのMTU問題だと思われます。

$ ifconfig gretap1
gretap1: flags=4419<UP,BROADCAST,RUNNING,PROMISC,MULTICAST>  mtu 1500
        inet6 fe80::14be:fdff:feeb:7771  prefixlen 64  scopeid 0x20<link>
        ether 16:be:fd:eb:77:71  txqueuelen 1000  (Ethernet)
        RX packets 51998  bytes 6357910 (6.0 MiB)
        RX errors 0  dropped 0  overruns 0  frame 0
        TX packets 47575  bytes 7269295 (6.9 MiB)
        TX errors 166  dropped 0 overruns 0  carrier 148  collisions 0
  • RaspberryPi以外からMTUを確認

Windowsから相手側拠点ルーターとRaspberryPiへのpingを行い、MTUを確認してみました。

>ping -f -l 1472 -n 1 192.168.2.108

192.168.2.108 に ping を送信しています 1472 バイトのデータ:
要求がタイムアウトしました。

192.168.2.108 の ping 統計:
    パケット数: 送信 = 1、受信 = 0、損失 = 1 (100% の損失)、

>ping 192.168.2.100

192.168.2.100 に ping を送信しています 32 バイトのデータ:
192.168.2.100 からの応答: バイト数 =32 時間 =33ms TTL=64
192.168.2.100 からの応答: バイト数 =32 時間 =20ms TTL=64

やはり失敗します。しかも普通はMTU超過時に表示される「パケットの断片化が必要ですが、DF が設定されています。」のメッセージもなくタイムアウト。

  • gretap1のMTUを1500+14に変更

ブリッジにつなげるインタフェースのMTUは同一でないと、パケットの転送に問題が出ます。そのため、br_lan, eth0, gretap1のMTUを最初はすべて1500にしていました。しかし、限界サイズのパケットがgretap1を通っていない状況なので、gretap1のMTUが結果的に足りてないように見えます。

そこでgretap1のMTUを、上記pingテストでわかった謎の差分14バイトを加えた1514バイトとしてみました:

>ping -f -l 1472 -n 1 192.168.2.100

192.168.2.100 に ping を送信しています 1472 バイトのデータ:
192.168.2.100 からの応答: バイト数 =1472 時間 =10ms TTL=64

192.168.2.100 の ping 統計:
    パケット数: 送信 = 1、受信 = 1、損失 = 0 (0% の損失)、
ラウンド トリップの概算時間 (ミリ秒):
    最小 = 10ms、最大 = 10ms、平均 = 10ms

>ping -f -l 1473 -n 1 192.168.2.100

192.168.2.100 に ping を送信しています 1473 バイトのデータ:
パケットの断片化が必要ですが、DF が設定されています。

192.168.2.100 の ping 統計:
    パケット数: 送信 = 1、受信 = 0、損失 = 1 (100% の損失)、

と、MTU1500でのpingが成功するようになりました。1500を超える場合のメッセージも期待通りです。ただし、この対応を行っても、なぜかRaspberryPi間のpingの状況は変わりませんでした。納得いきませんが、とりあえずいったんここまでとします。

パケットのフラグメント化を確認してみる

上記pingの内容をtcp dumpで確認してみます。
tcpdump -v -i tailscale0 '((ip[6:2] > 0) and (not ip[6] = 64))' でtailscale0を流れるフラグメント化されたパケットだけを見ることが出来ます。gretap1インタフェースを見てもフラグメント処理後の内容しか見れないので、tailscale0インタフェースの通信をチェックします。

$ sudo tcpdump -v -i tailscale0 '((ip[6:2] > 0) and (not ip[6] = 64))'
09:29:22.363936 IP (tos 0x0, ttl 64, id 19551, offset 0, flags [+], proto GRE (47), length 1276)
    100.86.118.83 > 100.92.41.28: GREv0, Flags [none], length 1256
        IP truncated-ip - 210 bytes missing! (tos 0x0, ttl 64, id 24567, offset 0, flags [DF], proto TCP (6), length 1448)
    192.168.2.100.http > 192.168.2.34.58448: Flags [.], seq 1408:2816, ack 1, win 980, length 1408: HTTP
09:29:22.364020 IP (tos 0x0, ttl 64, id 19551, offset 1256, flags [none], proto GRE (47), length 230)
    100.86.118.83 > 100.92.41.28: gre

最初のパケットに210 bytes missing!と出ていますが、その後のパケットが230バイト。IPヘッダ20バイトを除くとちょうど210バイトなので、意図したとおりフラグメント化されていることは確認できました。パケットの内容にflags:DFとあるように、フラグメント不可のパケットもGREでカプセル化の上フラグメント化されていることがわかります。

MSS Clampを試す

上記MTU問題の低減+TCP通信でのフラグメント化を避けるため、MSSの値を小さくしてみることにします。IPv4のTCP限定ですが、フラグメント化されずに通信できることになります。

MSSの値は1280(taiscaleのMTU) - 42(GREヘッダ: 4, Ether: 18, IPヘッダ: 20) - 20(IPヘッダ) - 20(TCPヘッダ) = 1198バイト

設定ファイル中の該当行は以下です。iptablesの-m physdev --physdev-out gretap1オプションで物理インタフェースを直接指定し、MSSの書き換えが出来ます。

iptables -t mangle -I POSTROUTING -m physdev --physdev-out gretap1 -p tcp --tcp-flags SYN,RST SYN -j TCPMSS --set-mss 1198

Windowsから相手側拠点ルーターのWebインタフェースにアクセスし、tcpdumpでMSSが書き換えられていること、フラグメント化がおきていないことを確認します。
速度測定編 にて計測してますが、MSS Clampを行うと2割ほど速度が改善されるようです。

続き

参考情報

結構古い情報も多く、パケットフィルタリングも今はiptablesからnetfilterに移行しているので注意が必要です。

Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
6
Help us understand the problem. What are the problem?