家のインターネットが遅かったからなんとかした話


この記事は、 Yahoo! JAPAN 18 新卒 2 Advent Calendar 2018 22日目の記事です。
前回はkyaritaさんの「Vue.jsとVuetify.jsでTwitterに投稿されている都内のご飯情報を検索するやつをつくってみる」でした。

今日は、この頃家のインターネットが遅いのでそれをなんとかした話について書きたいと思います。

はじめに

最近、夜間になるとやたらインターネットが遅い気がしていて、遅延計測ツールである SmokePing を仕掛けてみたところ、IPv4の外部との通信だけ深夜に品質が低下することがわかりました。
そこで、IPv4の通信もIPv6に乗せて出ていけば回避できると考え、それを実現した話です。

現状

まず、現状の確認からしていきます。

以下は、 Google Public DNS 宛のSmokePingの結果です。
上の図は IPv4 (8.8.8.8宛)、下の図はIPv6 (2001:4860:4860::8888宛) となっています。
IPv4では、1日周期で遅延が大きくなり、またパケットロスも発生していることが確認できます。
一方、IPv6では時間による変動はなく、ほぼ 3.4ms 程度を維持しており、パケットロスも特に生じていません。


念のため、インターネットに出ず家の中で完結する経路も見てみましょう。
上はIPv4、下はIPv6の通信で、SmokePingがあるサーバから別のセグメントにあるサーバへPingを送っています。
こちらは、IPv4/IPv6で特に差はなく、 1.8ms 程度を維持しているようです。

どうしてこうなるのか

自宅はフレッツ回線を利用するプロバイダと契約し、IPv4の通信はPPPoE、IPv6の通信はNGNを介したIPoEの接続になっています。
PPPoEのプロバイダ側の終端装置は、プロバイダごと/地域ごとにあることが知られています。
ここにたくさんの利用者からの接続が殺到し多くのトラフィックが流れると、そこで混雑が生じ輻輳などの原因になります 1
今回、IPv4の通信だけ夜間に品質が劣化したのは、これが原因と考えられます。
一方、家の回線のIPv6の接続では、この終端装置を経由せず混雑部分を経由しないため、通信品質の低下を回避することができます。
そこで、IPv4の通信をIPv6に乗せることでボトルネックとなる部分を迂回しようという話になります。

最近のプロバイダ事情

一方、各プロバイダもこの状況は認知していて、いくつかのプロバイダからは上の話と同様の手段で混雑を回避してIPv4の通信をする方法がサービスとして提供されています。
その具体的な例として、MAP-EやDS-Liteがあり、前者はBIGLOBE 2 など、後者はIIJ 3 などが提供しています。
基本的には、この混雑の回避のためには上記のサービスを利用すれば問題ありませんが、本記事では上記のような構成を自力で構築してみたいと思います。
(どうして自力で構築したのかという話は最後の方に書こうと思います。)

なんとかするやり方

概要

まず、IPv4でどこから出ていくかという話ですが、今回はVPSを利用したいと思います。
家とVPSをIPv6で繋ぎ、家から来たIPv4の通信はIPv6に乗せて運び、VPSで荷降ろししてIPv4としてインターネットへ出ていきます。
よく利用されるトンネル技術としてはL2TP/IPSecがありますが、今回は通信路を秘匿したいなどの要件はないので、よりシンプルなプロトコルであるIPIP 4 を利用します。
したがって、VPSと自宅側機器で IPIP (IPv4-over-IPv6) のトンネルを張ることになります。
VPS側のOSですが、深く考えずに FreeBSD 12.0-RELEASE を利用します。

自宅側機器ですが、IPIPの終端として NEC IX3110 を利用したいと思います。
また、家の中の一部セグメントのみVPSから出ていきたいので、その制御として家の中でL3スイッチとして利用している Juniper EX3200 も利用します。

構成

以下にアドレス等のパラメータを書いておきます。

  • 自宅で利用しているアドレスレンジ: 10.0.0.0/16
  • VPSを経由したいアドレスレンジ: 10.0.1.0/24
  • トンネル:
    • VPS側終端アドレス: 2001:db8:1::1 (VPSのI/Fについているアドレス)
    • 自宅側終端アドレス: 2001:db8:2::1 (トンネルを終端するIX3110についているアドレス)
  • VPS側NIC:
    • vtnet0: VPSがインターネットに面しているI/F (グローバル: 192.0.2.1)
    • gif0: IPIPトンネルのI/F (10.0.1.0)

全体の構成図

下図は、トンネル外側の構成図です。
IX3110の Gi1.0 およびVPSの vtnet0 でIPIPを終端しています。

          <-- IPIP Traffic -->
       +------ (Internet) ------+
       |                        | 192.0.2.1
 Gi1.0 | 2001:db8:2::1   vtnet0 | 2001:db8:1::1
  +----+---+                +---+---+
  | IX3110 |                |  VPS  |
  +--------+                +-------+

また、下図はトンネルに関する論理構成です。

      Internet
          ^
          | 
   vtnet0 | (IPv4 Global IP)
      +---+---+ <-- Source NAT
      |  VPS  | 
      +---+---+
     gif0 | 10.0.1.0/31  \
          |               |
      (Internet)          | IPv4-over-IPv6 Tunnel
          |               |
Tunnel1.0 | 10.0.1.1/31  /     \
     +----+---+ <-- Adjust MSS  | IX3110
     | IX3110 |                 | "VRF-VPS" VRF
     +----+---+                 |
    Gi2.1 | 10.0.1.2/31        /
          | (VLAN 1000)
vlan.1000 | 10.0.1.3/31  <--- EX3200 "VRF-VPS" VRF
     +----+---+
     | EX3200 |
     +----+---+
  vlan.10 | 10.0.10.0/24  <--- EX3200 "VRF-Main" VRF
          :
      Some hosts 

VPS側

自宅のアドレスはプライベートIPアドレスなので、VPSが持つグローバルIPアドレスで出ていくためにアドレスを変換する必要があります。
ここではFreeBSDでは一般的なpfを利用して NAT (ソースNAT) します。

自宅側

まず、EX3200について見ていきましょう。

自宅側は、EX3200にいくつかのセグメントを収容しています。
今回は、このうち VLAN 10 (10.0.10.0/24) のみVPSを経由するという設定にしたいのですが、このままではデフォルトルートが複数あり (PPPoEで出ていく経路とVPS経由で出ていく経路)、パケットの宛先のみを用いて送信先を決めるというIPの原則により自宅発のパケットの送り先がどちらか一方だけになってしまいます。
そのため、まずPPPoEで出ていく経路とVPS経由で出ていく経路をスイッチ内で切り離すことを考え、ルーティングテーブルを分割することのできるVRFを利用します。
前者および後者のVRFを VRF-Main および VRF-VPS とします。
(なお、 VRF-VPS にはIX3110と繋がっているI/F、 VRF-Main にはそれ以外の全てのI/Fが所属しています。)

VRF-VPS には、スタティックルートとしてデフォルトルート (0.0.0.0/0) -> 10.0.1.2 (IX3110宛) を入れています。
これにより、VRF-VPSに流入したパケットはこの経路に従い全てIX3110へ出ていきます。
VRF-VPS のルーティングテーブルにある経路はこれだけです。

しかし、このままだとVRFによりルーティングテーブルが分離されているため、VRF-VPSへパケットが流入することはなく、またVPSから帰ってきたパケットの行き先もありません。
そのため、今回はEX3200に対し Filter-based forwarding (一般には、ポリシーベースルーティング (PBR) と呼ばれることが多いです) を利用し、該当セグメント (VLAN 10) およびIX3110と繋がっているI/Fのパケットが流入する向きにそれぞれ下記のようなフィルタを適用します。

  • EX3200の該当セグメント (VLAN 10) のI/F:
    • パケットの送信先が自宅内のアドレスの範囲 (10.0.0.0/16) なら VRF-Main へ転送
    • それ以外なら VRF-VPS へ転送
  • EX3200のIX3110と繋がっているI/F:
    • VRF-Main へ転送

これにより、該当セグメント (VLAN 10) のインターネットへ向かうパケットのみVPSを経由する設定が出来上がり、VPSから帰ってきたパケットは全て VRF-Main のルーティングテーブルに従い該当セグメントへ流出させることができます。

次に、IX3110について見ていきます。

IX3110では、2つのスタティックルートを書いています。
1つはデフォルトルート (0.0.0.0/0) -> 10.0.1.0 (VPSのgif0宛) であり、これによりEX3200から入ってきたインターネット向きのパケットは全てトンネル経由でVPSへ送り出すことができます。
また、もう1つは 10.0.0.0/16 -> 10.0.1.3 (EX3200のvlan.1000) であり、これによりVPSから帰ってきたパケットをEX3200へ送り出すことができます。
経路としてはこれで十分なのですが、IX3110ではこれ以外の用事でもIPv4を利用しており、そちらとルーティングテーブルを混ぜたくないため、やはりVRFを用いてルーティングテーブルの分離をしています。

各種コンフィグ

ここまでで一通り解説したので、実際のコンフィグを見てみましょう。
(アドレスやいくつかの名前等は実際のものから多少手を加えてあります)

VPS

/etc/rc.conf (抜粋)
pf_enable="YES"

cloned_interfaces="gif0"
ifconfig_gif0="inet6 tunnel 2001:db8:1::1 2001:db8:2::1 mtu 1460"
ifconfig_gif0_alias0="inet 10.0.1.0/31 10.0.1.1"

static_routes="to_home"
route_to_home="-net 10.0.0.0/16 10.0.1.1"

gateway_enable="YES"
/etc/pf.conf
nat on vtnet0 from 10.0.0.0/16 to any -> (vtnet0)

なお、実際には ipfw を用いてWAN側のパケットフィルタ (ファイアウォール) も適用していますが、今回の件とは直接的に関係ないため設定は省略しています。

IX3110

ip route vrf VRF-VPS default 10.0.1.0
ip route vrf VRF-VPS 10.0.0.0/16 10.0.1.3

interface GigaEthernet1.0
  description Uplink
  no ip address
  ipv6 enable
  ipv6 address 2001:db8:2::1/64
  no shutdown

interface GigaEthernet2.1
  description "to EX3200"
  encapsulation dot1q 1000 tpid 8100
  ip vrf forwarding VRF-VPS
  ip address 10.0.1.2/31
  no shutdown

interface Tunnel1.0
  description "to VPS"
  tunnel mode 4-over-6
  tunnel destination 2001:db8:2::1
  tunnel source 2001:db8:1::1
  ip vrf forwarding VRF-VPS
  ip address 10.0.1.1/31
  ip tcp adjust-mss 1200  ! 1200の理由は後述
  no shutdown

EX3200

set interfaces vlan unit 10 description "Specific segment"
set interfaces vlan unit 10 family inet filter input pbr-specific-segment-to-vps
set interfaces vlan unit 10 family inet address 10.0.10.0/24

set interfaces vlan unit 1000 description "To IX3110"
set interfaces vlan unit 1000 family inet filter input pbr-vps-to-specific-segment
set interfaces vlan unit 1000 family inet address 10.0.1.3/31

set firewall family inet filter pbr-specific-segment-to-vps term t1 from destination-prefix-list home-all-v4
set firewall family inet filter pbr-specific-segment-to-vps term t1 then routing-instance VRF-Main
set firewall family inet filter pbr-specific-segment-to-vps term t2 then routing-instance VRF-VPS
set firewall family inet filter pbr-specific-segment-to-vps term t1 then routing-instance VRF-Main

set routing-instances VRF-VPS instance-type virtual-router
set routing-instances VRF-VPS interface vlan.1000
set routing-instances VRF-VPS routing-options rib VRF-VPS.inet.0 static route 0.0.0.0/0 next-hop 10.0.1.2

結果

その他、ルーティングテーブル等の情報は下記のようになります。
(こちらも同様に多少手を加えてあります)

VPS

$ ifconfig gif0
gif0: flags=8051<UP,POINTOPOINT,RUNNING,MULTICAST> metric 0 mtu 1460
        options=80000<LINKSTATE>
        tunnel inet6 2001:db8:1::1 --> 2001:db8:2::1
        inet 10.0.1.0 --> 10.0.1.1 netmask 0xfffffffe
        groups: gif
        nd6 options=29<PERFORMNUD,IFDISABLED,AUTO_LINKLOCAL>
$ netstat -nr -f inet | egrep -v 'lo0|169.254.169.254'
Routing tables

Internet:
Destination        Gateway            Flags     Netif Expire
default            192.0.2.1          UGS      vtnet0
10.0.0.0/16        10.0.1.1           UGS        gif0
10.0.1.1           link#3             UH         gif0
192.0.2.1/24       link#1             U        vtnet0

IX3110

ix3110.bb# show ip route vrf VRF-VPS
VRF: VRF-VPS
IP Routing Table - 4 entries, 1 hidden, unlimited
Entries: 2 Connected, 2 Static, 0 RIP, 0 OSPF, 0 BGP
Codes: C - Connected, S - Static, R - RIP, O - OSPF, IA - OSPF inter area
       N1 - OSPF NSSA external type 1, N2 - OSPF NSSA external type 2
       E1 - OSPF external type 1, E2 - OSPF external type 2, B - BGP
       * - Candidate default, s - Summary
Timers: Age
S*   0.0.0.0/0 [1/1] via 10.0.1.0, Tunnel1.0, 21d3h20m54s
     10.0.0.0/8 is subnetted, 3 subnets
C      10.0.1.0/31    [0/0] is directly connected, Tunnel1.0, 21d3h20m54s
C      10.0.1.2/31    [0/0] is directly connected, GigaEthernet2.1, 21d3h20m54s
S      10.0.0.0/16    [1/1] via 10.0.1.3, GigaEthernet2.1, 21d3h20m54s

EX3200

> show route table VRF-VPS.inet.0

VRF-VPS.inet.0: 3 destinations, 3 routes (3 active, 0 holddown, 0 hidden)
+ = Active Route, - = Last Active, * = Both

0.0.0.0/0          *[Static/5] 3w0d 03:27:15
                    > to 10.0.1.2 via vlan.1000
10.0.1.2/31        *[Direct/0] 3w0d 03:27:15
                    > via vlan.1000
10.0.1.3/32        *[Local/0] 3w0d 03:27:22
                      Local via vlan.1000

SmokePing

以下は、 Google Public DNS (IPv4) 宛のSmokePingの結果です。
12/18にSmokePingサーバがVPS経由でインターネットに出ていく設定を実施し、それ以降は大幅に遅延が改善していることが確認できます。
(図中の12/18付近にある8ms程度の山が切り替えたタイミングです。)
なお、12/19の1日ほど継続的に遅延の増加と軽微なパケットロスが見られますが、これはVPS側影響 によるものと思われます。体感での影響は特にありませんでした。

その他

Wiresharkでは、パケットの積まれている様子が以下のように見えます。

また、該当セグメントからの traceroute は以下のように見えます。
ちゃんとVPSを経由して出ていってますね。

$ traceroute -n 8.8.8.8
traceroute to 8.8.8.8 (8.8.8.8), 30 hops max, 60 byte packets
 1  10.0.10.1  1.843 ms  1.794 ms  1.771 ms
 2  10.0.1.2  0.143 ms  0.132 ms  0.189 ms
 3  10.0.1.0  4.208 ms  3.944 ms  3.986 ms
 :
 :  (省略)
 :
10  8.8.8.8  4.376 ms  4.351 ms  4.669 ms

スピードテストの結果で見ても、以下のように十分なスループットとなっていて、特に問題なさそうです。

おわりに

ここまで、家のインターネットが遅かったのでなんとかしたやり方を紹介してきました。
既存の構成に乗せる形で今回の構築を行ったため多少複雑になってしまいましたが、家のホスト全体がVPSから出ていくだけなら自宅側機器としてはルータ1台でも構成可能です。
IPIP (IPv4-over-IPv6) や GRE (v6) が使えるルータがあれば良いので、YAMAHAのRTXシリーズやNECのIXシリーズなどが候補に入ると思います。
興味がある方はぜひやってみてください。

落ち穂拾い

MSSについて

上記のIX3110のコンフィグでMSSを1200に調整していることに気になった方がいるかもしれません。
これについて少し解説します。

フレッツからIPoEで出ていく場合、MTUは1500です。
この場合、トンネルI/FのMTUは、IPIPのためのオーバーヘッド (IPv6ヘッダー: 40バイト) がつくために1460となります。
この上に、IPv4ヘッダー (20バイト)、TCPヘッダー (20バイト) が乗るため、MSSは1420が妥当と考えられます。

最初はこの設定にしていたのですが、スピードテストの結果が非常に悪く、Wiresharkでパケットを観測したところ、TCPのペイロードが1232バイトと228バイトに2分割される形でフラグメントされていました。
よくよく調べるとVPSから家宛のパケットのみこの症状が見られ、原因を探していたところ、FreeBSDの実装によるものと判明しました。
下記が該当部分のソースコードです (sys/netinet6/in6_gif.c)。

原因は、IPv6の仕様 (RFC8200) で定められている最小MTUである1280バイトへ強制的に分割する実装によるものでした。
結局、外側I/FのMTUを1280として同様の計算を行いMSSを1200とし、現在はフラグメントを発生させることなくTCPのトラフィックが流れています。

本来であればこの構成でのMSSはMTUから80バイト減じた値で問題ないので、もし設定される際はお気をつけください。