FRRはRFC5549な経路をLinuxカーネルのルーティングテーブルへどうやってインストールするのか

FRRouging (FRR)という、Quaggaからフォークしたルーティングデーモンがある。

github.com

FRRはRFC 5549に対応しており、bgpdで受信したIPv6ネクストホップを持つIPv4経路をzebra経由でLinuxカーネルのルーティングテーブルへインストールすることができる。しかし、Linuxカーネルのルーティングテーブルにそのような経路をインストールすることは通常できないはずである。正確に言うと最近のiproute2ではip routeコマンドでそのような指定自体は可能だか、実際には反映されない。

$ ip -V
ip utility, iproute2-ss170905
$ sudo ip route add 192.168.254.0/24 via inet6 fe80::a00:27ff:fe25:4c23 dev eth1
$ ip route
192.168.254.0/24 dev eth1 

ではFRRはこの問題をどのように解決しているのか?気になったので少し調べてみた。

観察

FRR 2.0をインストールした2台のサーバ(server1, server2)のネットワークインタフェース(eth1)どうしを直結した上でBGP unnumberedピアを設定し、server1からserver2に対し経路広報を行った。

動作を観察してみると、ネイバーテーブルにダミーの169.254.0.1向けエントリを作成したのち、ルーティングテーブル上でネクストホップとして本来のIPv6リンクローカルアドレスの代わりに169.254.0.1を指定することで問題を解決しているようである。 下記の例ではserver1からRFC 5549で広報された172.16.254.0/24の経路がserver2のルーティングテーブルにインストールされた際の状態を示している。

server2$ ip neigh
fe80::a00:27ff:fe25:4c23 dev eth1 lladdr 08:00:27:25:4c:23 router STALE
169.254.0.1 dev eth1 lladdr 08:00:27:25:4c:23 PERMANENT
server2$ ip route
172.16.254.0/24 via 169.254.0.1 dev eth1  proto zebra  metric 20 onlink 

ネイバーテーブル・ルーティングテーブルのメンテナンス

気になるのが、FRRはどのようにネイバーテーブルやルーティングテーブルをメンテナンスしているのか、という点。ここではFRR 2.0のコードを元に、その方法を追ってみる。

まずはコード内を "5549" というキーワードでgrepしてみると、ルーティングテーブルにエントリをインストールしている箇所がzebra/rt_netlink.cでヒットする。ルーティングテーブルのエントリ作成はこの部分が担当しているようである。

https://github.com/FRRouting/frr/blob/frr-2.0/zebra/rt_netlink.c#L654-L678

実際にzebraの実行時のログを確認すると、この部分が使われている様子が観測できる。

ZEBRA: zebra message comes from socket [13]
ZEBRA: 0:172.16.254.0/24: Inserting route rn 0x108a4c0, rib 0x108a3f0 (type 9) existing (nil)
ZEBRA: 0:172.16.254.0/24: Adding route rn 0x108a4c0, rib 0x108a3f0 (type 9)
ZEBRA: netlink_route_multipath() (single hop): RTM_NEWROUTE 172.16.254.0/24 vrf 0 type IPv6 nexthop with ifindex
ZEBRA:  5549: _netlink_route_build_singlepath() (single hop): nexthop via 169.254.0.1 if 3
ZEBRA: netlink_talk: netlink-cmd (NS 0) type RTM_NEWROUTE(24), len=60 seq=7 flags 0x405
ZEBRA: netlink_parse_info: netlink-cmd (NS 0) ACK: type=RTM_NEWROUTE(24), seq=7, pid=0
ZEBRA: 0:172.16.254.0/24: Redist update rib 0x108a3f0 (type 9), old (nil) (type -1)

ではネイバーテーブルはどうか?コード内を "169.254.0.1" でgrepすると、先のzebra/rt_netlink.cとは別にzebra/interface.cif_nbr_ipv6ll_to_ipv4ll_neigh_update()がヒットする。

https://github.com/FRRouting/frr/blob/frr-2.0/zebra/interface.c#L752-L765

この部分を読むと、与えられたIPv6アドレスがEUI-64方式であることを前提としてそのMACアドレスを割り出したのち、それを用いてネイバーテーブルに169.254.0.1のエントリを作成しているようである。

ではif_nbr_ipv6ll_to_ipv4ll_neigh_update()を呼んでいるのは誰か?逆順に辿っていくと、

  • zebra/zserv.c:nbr_connected_add_ipv6()
  • zebra/rtadv.c:rtadv_process_advert()
  • zebra/rtadv.c:rtadv_process_packet()
  • zebra/rtadv.c:rtadv_read()
  • ...

という具合に、IPv6 Router Advertisement (RA) の受信処理部分に辿り着く。また、上記パスとは別に、

  • zebra/interface.c:if_nbr_ipv6ll_to_ipv4ll_neigh_add_all()
  • zebra/interface.c:if_up()
  • ...

からも呼ばれている。 これらより、zebraはネットワークインターフェースがUPしたとき(zebra自身の起動時を含む)とIPv6 RAを受信したときにネイバーテーブルのエントリを作成していることが読み取れる。

まとめ

…といったことを挙動やコードから追っていたところ、Cumulus Linuxのドキュメントにしっかり記述されていた*1

Border Gateway Protocol - BGP - Cumulus Linux 3.4.1 - Cumulus Networks

BGP and Extended Next-hop Encoding:

For link-local peerings enabled by dynamically learning the other end's link-local address using IPv6 neighbor discovery router advertisements, an IPv6 next-hop is converted into an IPv4 link-local address and a static neighbor entry is installed for this IPv4 link-local address with the MAC address derived from the link-local address of the other end.

It is assumed that the IPv6 implementation on the peering device will use the MAC address as the interface ID when assigning the IPv6 link-local address, as suggested by RFC 4291.

Managing Unnumbered Interfaces:

the IPv4 link-local address 169.254.0.1 is used to install the route and static neighbor entry to facilitate proper forwarding without having to install an IPv4 prefix with IPv6 next-hop in the kernel

*1:CumulusはFRR開発の中心的存在で、最近のCumulus LinuxではルーティングデーモンとしてFRRを採用している

Linuxにおける Equal Cost Multipath (ECMP) の設定方法と挙動に関するメモ

Linuxのネットワークスタックでmultipathを使用する場合の設定方法と挙動に関するメモ。

設定方法

設定自体は ip route add コマンドで nexthop を複数指定するだけ。src の指定は必要に応じて。

# ip route add 10.1.1.0/24 src 10.0.0.3 \
    nexthop via 192.168.11.1 weight 1 \
    nexthop via 192.168.12.1 weight 1
$ ip route
(snip)
10.1.1.0/24  src 10.0.0.3 
        nexthop via 192.168.11.1  dev eth1 weight 1
        nexthop via 192.168.12.1  dev eth2 weight 1

なお、カーネルCONFIG_IP_ROUTE_MULTIPATH=yコンパイルされていることが必要。

カーネルバージョンごとの挙動の差異

カーネルバージョン毎にトラフィックバランシング方法が異なるため、注意が必要*1

  • ~ 3.5: レイヤ3情報 (src/dst IP address) に基づく per-flow ECMP
  • 3.6 ~ 4.3: per-packet ECMP
  • 4.4 ~ 4.11: レイヤ3情報に基づく per-flow ECMP
  • 4.12 ~: レイヤ3情報 or 5-tuple (src/dst IP addr, src/dst port, L4 proto) に基づく per-flow ECMP

カーネル3.6から4.4の間の状況の概要はredditのこのスレッドに纏まっている:

www.reddit.com

カーネル3.6で一旦消滅した per-flow ECMP は、カーネル4.4で復活した:

github.com

ちなみに RHEL 7 (CentOS 7) のカーネルは3.10ベースであるため、当初の挙動は per-packet ECMP だった。ただし、3.10.0-514 (=7.3の初期カーネルパッケージ) で上記パッチがバックポートされたため、これ以降は動作が per-flow ECMP に変わっている。

https://git.centos.org/blob/rpms!kernel.git/f91430943f84931525936ea22f3cb2f24577f23a/SPECS!kernel.spec#L17805

レイヤ4情報も含めた per-flow ECMP はカーネル4.12でマージされた。net.ipv4.fib_multipath_hash_policy で 1 を指定すれば使用可能。(デフォルトは0 = L3のみ)

github.com

*1:IPv4の場合。IPv6に関しては未調査

Linux VRF with L3 Master Device

Linux kernel 4.4から登場した、L3 Master Device (l3mdev) によるVirtual Routing and Forwading (VRF) を軽く触ってみたメモ。

そもそも何をするためのものなのか

Linuxのネットワーク廻りを触っていると、たまに「特定のネットワークインタフェースから入ってきた通信に、特定のルーティングルールを適用したい」といった場面がある。こうしたケースでは、以前より Routing Policy Database (RPDB) を利用して Policy Based Routing (PBR) を行う方法が知られている。

d.hatena.ne.jp

一方、ネットワーク機器の世界では一般的に、L3ドメインを分割する手段として Virtual Routing and Forwarding (VRF) という機能が PBR とは別に存在している。VRFは、あるネットワークインタフェース群に適用されるルーティングテーブルを分離させるためのものである。このような概念をLinux上で実現するために登場したのが、L3 Master Device である。

net: L3 master device [LWN.net]

kernel/git/davem/net-next.git - David Miller's -next networking tree

使用方法

前提として、カーネルが4.4以降かつ NET_L3_MASTER_DEV=yコンパイルされている必要がある。たとえばFedoraの場合、4.11以前のパッケージではこのオプションが無効であるため注意が必要。また、iproute2 のバージョンが古いと、後述するl3mdevが暗黙的に追加するポリシーが ip rule コマンドで正しく表示されないので同様に注意する必要がある。

Bug 1428530 – Set NET_L3_MASTER_DEV=y to enable ipvlan module

設定のおおまかな流れとしては次の3ステップ。

  1. L3 Master Device (l3mdev) のネットワークインタフェースを作成する
  2. 既存のネットワークインタフェースのmasterとしてl3madevを指定する
  3. VRF毎にルーティングを設定する

流れ自体はbridge interfaceを作成する場合と似ている。l3mdev は通常のネットワークインタフェース (net_device) と同様に振る舞うので、l3mdev自体にIPアドレスを振ることも可能。ネットワーク機器で言うところのloopback address的な使い方が可能。

使用例

まずはl3mdevを作成する。ここでは vrf-xvrf-y の2個のVRFを作成する。

# ip link add dev vrf-x type vrf table 10
# ip link set dev vrf-x up
# ip link add dev vrf-y type vrf table 20
# ip link set dev vrf-y up

ip link コマンドを実行すると、2個のインタフェースが作成されていることが分かる。

$ ip link
2: ens4: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc fq_codel state UP mode DEFAULT group default qlen 1000
    link/ether 52:54:00:15:7f:1c brd ff:ff:ff:ff:ff:ff
3: ens5: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc fq_codel state UP mode DEFAULT group default qlen 1000
    link/ether 52:54:00:4e:69:5d brd ff:ff:ff:ff:ff:ff
4: ens6: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc fq_codel state UP mode DEFAULT group default qlen 1000
    link/ether 52:54:00:cb:ef:bd brd ff:ff:ff:ff:ff:ff
5: ens7: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc fq_codel state UP mode DEFAULT group default qlen 1000
    link/ether 52:54:00:f1:2e:11 brd ff:ff:ff:ff:ff:ff
6: ens3: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc fq_codel state UP mode DEFAULT group default qlen 1000
    link/ether 52:54:00:83:4c:ca brd ff:ff:ff:ff:ff:ff
9: vrf-x: <NOARP,MASTER,UP,LOWER_UP> mtu 65536 qdisc noqueue state UP mode DEFAULT group default qlen 1000
    link/ether 5a:b6:1d:4a:84:21 brd ff:ff:ff:ff:ff:ff
11: vrf-y: <NOARP,MASTER,UP,LOWER_UP> mtu 65536 qdisc noqueue state UP mode DEFAULT group default qlen 1000
    link/ether 76:c3:e8:67:00:01 brd ff:ff:ff:ff:ff:ff

ここでは ens4, ens5vrf-x に、ens6, ens7vrf-y に所属させることにする。

# ip link set dev ens4 master vrf-x
# ip link set dev ens5 master vrf-x
# ip link set dev ens6 master vrf-y
# ip link set dev ens7 master vrf-y

再度 ip link コマンドを実行すると、ens[4-7] のmasterが設定されていることが分かる。

$ ip link
2: ens4: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc fq_codel master vrf-x state UP mode DEFAULT group default qlen 1000
    link/ether 52:54:00:15:7f:1c brd ff:ff:ff:ff:ff:ff
3: ens5: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc fq_codel master vrf-x state UP mode DEFAULT group default qlen 1000
    link/ether 52:54:00:4e:69:5d brd ff:ff:ff:ff:ff:ff
4: ens6: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc fq_codel master vrf-y state UP mode DEFAULT group default qlen 1000
    link/ether 52:54:00:cb:ef:bd brd ff:ff:ff:ff:ff:ff
5: ens7: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc fq_codel master vrf-y state UP mode DEFAULT group default qlen 1000
    link/ether 52:54:00:f1:2e:11 brd ff:ff:ff:ff:ff:ff
6: ens3: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc fq_codel state UP mode DEFAULT group default qlen 1000
    link/ether 52:54:00:83:4c:ca brd ff:ff:ff:ff:ff:ff
9: vrf-x: <NOARP,MASTER,UP,LOWER_UP> mtu 65536 qdisc noqueue state UP mode DEFAULT group default qlen 1000
    link/ether 5a:b6:1d:4a:84:21 brd ff:ff:ff:ff:ff:ff
11: vrf-y: <NOARP,MASTER,UP,LOWER_UP> mtu 65536 qdisc noqueue state UP mode DEFAULT group default qlen 1000
    link/ether 76:c3:e8:67:00:01 brd ff:ff:ff:ff:ff:ff

また、ip rule コマンドを実行すると、priority 1000に l3mdev-table というポリシーが自動的に追加されていることが分かる。なお、この機能はkernel 4.8から実装されたもの*1なので、4.4~4.7のカーネルでl3mdevを使用する場合、手動でルールを追加する必要がある。

$ ip rule
0:      from all lookup local 
1000:   from all lookup [l3mdev-table] 
32766:  from all lookup main 
32767:  from all lookup default

ここで、例として 172.16.0.1/32 の経路を各VRFに追加する。

$ ip addr
2: ens4: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc fq_codel master vrf-x state UP group default qlen 1000
    link/ether 52:54:00:15:7f:1c brd ff:ff:ff:ff:ff:ff
    inet 192.168.1.1/24 scope global ens4
       valid_lft forever preferred_lft forever
    inet6 fe80::5054:ff:fe15:7f1c/64 scope link 
       valid_lft forever preferred_lft forever
3: ens5: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc fq_codel master vrf-x state UP group default qlen 1000
    link/ether 52:54:00:4e:69:5d brd ff:ff:ff:ff:ff:ff
    inet 192.168.2.1/24 scope global ens5
       valid_lft forever preferred_lft forever
    inet6 fe80::5054:ff:fe4e:695d/64 scope link 
       valid_lft forever preferred_lft forever
4: ens6: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc fq_codel master vrf-y state UP group default qlen 1000
    link/ether 52:54:00:cb:ef:bd brd ff:ff:ff:ff:ff:ff
    inet 10.0.1.1/24 scope global ens6
       valid_lft forever preferred_lft forever
    inet6 fe80::5054:ff:fecb:efbd/64 scope link 
       valid_lft forever preferred_lft forever
5: ens7: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc fq_codel master vrf-y state UP group default qlen 1000
    link/ether 52:54:00:f1:2e:11 brd ff:ff:ff:ff:ff:ff
    inet 10.0.2.1/24 scope global ens7
       valid_lft forever preferred_lft forever
    inet6 fe80::5054:ff:fef1:2e11/64 scope link 
       valid_lft forever preferred_lft forever
6: ens3: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc fq_codel state UP group default qlen 1000
    link/ether 52:54:00:83:4c:ca brd ff:ff:ff:ff:ff:ff
    inet 10.168.20.201/24 brd 10.168.20.255 scope global ens3
       valid_lft forever preferred_lft forever
    inet6 fe80::24fd:811e:6432:bd36/64 scope link 
       valid_lft forever preferred_lft forever
9: vrf-x: <NOARP,MASTER,UP,LOWER_UP> mtu 65536 qdisc noqueue state UP group default qlen 1000
    link/ether 5a:b6:1d:4a:84:21 brd ff:ff:ff:ff:ff:ff
11: vrf-y: <NOARP,MASTER,UP,LOWER_UP> mtu 65536 qdisc noqueue state UP group default qlen 1000
    link/ether 76:c3:e8:67:00:01 brd ff:ff:ff:ff:ff:ff
# ip route add 172.16.0.1/32 via 192.168.1.2 table 10 
# ip route add 172.16.0.1/32 via 10.0.1.2 table 20

ip route list を見ると、デフォルトおよび各VRF毎に独立したルーティングテーブルが設定されていることが確認できる。

$ ip route list
default via 10.168.20.1 dev ens3 proto static metric 100 
10.168.20.0/24 dev ens3 proto kernel scope link src 10.168.20.201 metric 100
$ ip route list vrf vrf-x
172.16.0.1 via 192.168.1.2 dev ens4 
192.168.1.0/24 dev ens4 proto kernel scope link src 192.168.1.1 
192.168.2.0/24 dev ens5 proto kernel scope link src 192.168.2.1 
$ ip route list vrf vrf-y
10.0.1.0/24 dev ens6 proto kernel scope link src 10.0.1.1 
10.0.2.0/24 dev ens7 proto kernel scope link src 10.0.2.1 
172.16.0.1 via 10.0.1.2 dev ens6 

Network Namespaceとの比較

Linuxのネットワークリソースを分離・独立させる手段としては、最近はNetwork Namespaceがポピュラーである。Network Namespaceはネットワークスタック全体を分離するのに対し、l3mdevはL3のルーティングのみを分離する。l3mdevのモデルは、L3ドメインをまたいでプロセスを実行したい場合、具体的にはルーティングデーモンを実行する場合に都合が良い。このあたりの背景は、L3 Master Deviceの提案者であるDavid Ahernが書いた記事が詳しい。

cumulusnetworks.com

アプリケーション側の挙動に関しては、カーネル付随ドキュメントの Applications セクションに記載がある通り sysctlnet.ipv4.tcp_l3mdev_accept / net.ipv4.udp_l3mdev_accept によって変化するようである。このあたりはまた別途触ってみる予定。

Reference

Generic XDPを使えばXDP動作環境がお手軽に構築できるようになった

XDP (eXpress Data Path) ネタ。

パフォーマンス上の理由から、XDPのパケット処理はskbが割り当てられる前にNICドライバから直接呼び出させる。そのため、XDPプログラムの動作にはNICドライバ側でのサポートが必要となる。

5月時点での対応ドライバリストは下記エントリ末尾に記載した。Linux kernel 4.12からixgbeでXDPがサポートされる予定とは言え、ハードウェアの準備も必要であり、従来XDPの動作を気軽に試すことは少し難しかった。

yunazuno.hatenablog.com

そこでkernel 4.12から登場するのが Generic XDP と呼ばれる機能である。これはskbが割り当てられた後にXDPのパケット処理を適用するものである*1。これによりNICドライバのサポートが無くともXDPプログラムを実行可能になった。パフォーマンスを犠牲にする代わりに、XDPプログラムの開発環境として使用したりXDP自体の間口を広げることを目的としている。

動作環境

前述した通り、Generic XDPはkernel 4.12でリリース予定である。4.12 rc3時点で既に最初のコミットに加えいくつかの微修正やサンプルコードの更新もマージされている。

プログラムのアタッチ時の動作

ネットワークインタフェースにXDPプログラムをアタッチする際*2、ドライバ側でのXDPサポート状況やフラグの値により、通常のNIC-level XDPを使用するか、あるいはGeneric XDPを使用するかが決定される。

  • デフォルト: まずNIC-level XDPでのアタッチを試み、失敗したら (=NICドライバがサポート外) Generic XDPを使用する
  • XDP_FLAGS_SKB_MODE を指定: Generic XDPのみを使用する
  • XDP_FLAGS_DRV_MODE を指定: NIC-level XDPでのアタッチのみを試みる。Generic XDPは使用しない

また、iproute2 (ipコマンド) はバージョン4.10からXDPプログラムローダとしての機能を持っている。更に次回リリースではGeneric XDPに特化したロード (上記 XDP_FLAGS_SKB_MODE に該当) もサポートされる予定である。

実際に試してみる

簡単なXDPプログラムを用意した上でそれをXDP非対応NICにアタッチし、Generic XDPが期待通り動作することを確認する。 また、XDPプログラムローダとして、過去のエントリで使用したiovisor/bccではなくiproute2を使用する。

  • Kernel: 4.12 rc3 (Fedora rawhide 4.12.0-0.rc3.git0.2.fc27.x86_64)
  • iproute2: 4.11+ (5a3ec4b)
  • NIC: 8139cp (qemu-kvm RTL8139, ens4)
    • NIC-level XDPは非サポート

まず、全ての受信パケットをドロップするXDPプログラムを用意し、コンパイルする:

#include <linux/bpf.h>

#ifndef __section
# define __section(NAME)                  \
   __attribute__((section(NAME), used))
#endif

__section("prog")
int xdp_drop(struct xdp_md *ctx)
{
    return XDP_DROP;
}

char __license[] __section("license") = "GPL";
$ clang -O2 -Wall -target bpf -c xdp_drop.c -o xdp_drop.o

続いて、これをipコマンドでens4にアタッチする:

# ip link set dev ens4 xdp obj xdp_drop.o

ip link showの出力結果にxdpgenericという文字列が出現し、Generic XDPでXDPプログラムがアタッチされていることが確認できる*3

# ip link show dev ens4
2: ens4: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 xdpgeneric qdisc fq_codel state UP mode DEFAULT group default qlen 1000
    link/ether 52:54:00:71:fc:9b brd ff:ff:ff:ff:ff:ff

デタッチは同様にipコマンドで:

# ip link set dev ens4 xdp off

このとき別ノードからens4宛にpingを打ってみると、XDPプログラムをアタッチしていた間、pingのレスポンスが無くなっていたことが確認できる。

$ ping 192.168.11.239
PING 192.168.11.239 (192.168.11.239) 56(84) bytes of data.
64 bytes from 192.168.11.239: icmp_seq=1 ttl=64 time=0.560 ms
64 bytes from 192.168.11.239: icmp_seq=2 ttl=64 time=0.318 ms
64 bytes from 192.168.11.239: icmp_seq=3 ttl=64 time=0.385 ms
64 bytes from 192.168.11.239: icmp_seq=8 ttl=64 time=0.594 ms
64 bytes from 192.168.11.239: icmp_seq=9 ttl=64 time=0.508 ms
64 bytes from 192.168.11.239: icmp_seq=10 ttl=64 time=0.624 ms
^C
--- 192.168.11.239 ping statistics ---
10 packets transmitted, 6 received, 40% packet loss, time 9205ms
rtt min/avg/max/mdev = 0.318/0.498/0.624/0.111 ms

References

*1:具体的に言うとnet/core/dev.cのnetif_receive_skb_internal

*2:netlink経由で実行する; サンプル: http://elixir.free-electrons.com/linux/v4.12-rc3/source/samples/bpf/bpf_load.c#L674

*3:通常のNIC-level XDPの場合は単に xdp と表示される

自宅のルータをAT-AR2050Vに入れ替えた & 回線をv6プラス化した

自宅マンションで光配線方式のフレッツが使えるようになったので、これを機にルータの入れ替えとv6プラス(IPv6 IPoE)化を実施したメモ。 結論から言うと、IPv4 PPPoE時代に発生していた夜間帯の輻輳が解消され、かなり快適になった。

構成変更前の環境

現住居に引っ越した時点ではフレッツのVDSL方式しか選択肢が無かったため、一旦考えるのをやめて最低限インターネットに接続できる環境だけを構築して使っていた。

ひかり電話契約は無し。VDSLモデムの下にルータが居て、そこでIPv4 PPPoEを終端するだけの単純な構成。

構成変更後の構成

回線が光配線方式に。また、Biglobe経由でJPNEのv6プラスも適用。

AT-AR2050Vなのは昨年末までキャンペーンで3万円弱で買えたため。昨年末、似たような価格でJuniper SRX300がeBayで大量に出回っていて気になったものの見送り。

v6プラスの効果

今回の構成変更で一番効果が有ったのは、v6プラス化したことにより夜間帯の輻輳が解消されたこと。

特にDNS名前解決が速くなったのが体感速度向上に貢献しているように思う。帯域幅に関してはそれほど気にしていないが、試しに ftp.kddilabs.jp から適当なisoイメージをダウンロードすると400Mbps弱出ている。

ネットワーク構成

それほど複雑なことはしていない。HGW (PR-500MI) - ルータ (AT-AR2050V) - L2 (GS908M) というシンプルな構成。 AR2050Vでrouted VLAN interfaceを2個作成し、それぞれを日常生活用と各種実験用として使っている。VLANはGS908Mまでtag VLANで運び、そこでポート毎に解いている。

IPv4インターネット接続に関しはHGWでNAT(MAP-E)されて出ていく。AR2050Vの下に居るサブネット宛のstatic routeをHGWに設定する必要があるので注意。 IPv6インターネット接続に関しては、HGWからAR2050VにDHCPv6-PDで/60が振ってくるので、これをクライアントに振っている。

IPv6まわりに関しての設定例がアライドテレシスのサイトにあるので、それほど苦労せずに設定できた。

AT-AR2050V/AT-AR3050S/AT-AR4050S コマンドリファレンス 5.4.7: 設定例集#44: フレッツ 光ネクスト IPv6インターネット接続(IPv6 IPoE、DHCPv6 PD方式)

機器周辺はこんな感じ。横向きなのがHGWで、隙間に居るのはDNSフルリゾルバ兼DHCPサーバのRaspberry Pi. 上の段に居るのは各種実験用に使っているQuanta LB6M. 無線LANはHGW内蔵APは使用せず、Aterm WG1200HPをブリッジモードで使用。

f:id:yunazuno:20170513161528j:plain

Facebookはレイヤ4ロードバランサをIPVS(LVS)からXDPベースのものに乗り換えつつある

4月に開催されたnetdev 2.1で面白いセッションがあったのでメモ。

Facebookが使用しているレイヤ4のロードバランサに関する発表で、従来はIPVS (LVS) を使用していたが、XDPベースで自ら開発したものに移行しつつある、という内容。

XDP Production Usage: DDoS Protection and L4LB (slide)

www.youtube.com

XDP (eXpress Data Path) については以前のエントリで簡単に紹介した。

yunazuno.hatenablog.com

XDPを改めて簡単に紹介すると、Linuxカーネルのネットワークスタックの最下部 (NICに一番近い場所) でパケット処理を行う仕組みのことで、オーバーヘッドが非常に小さい高速パケット処理の実現を目的としている。XDPはeBPFを用いており、eBPFが提供するmapやhelper functionといった機能を用いて柔軟なプログラムが書けることに加え、JITによる高速化やカーネル自身の堅牢性をそのまま享受することができるとされている。

XDPがリリースされたのは2016年10月のカーネル4.8なので、半年程で実際の事例が出てきたことになる。

FacebookにおけるXDPのユースケース: L4LB & DDoS防御

FacebookはIPVS (LVS) ベースのレイヤ4ロードバランサ (L4LB, 通称Shiv) を利用していることが知られている。

blog.stanaka.org

yunazuno.hatenablog.com

Shivの役割はConsistent Hashによる負荷分散先の決定とIP encapsulation (IPIP) によるパケット転送である。また、ローカルにセッションキャッシュを持つことにより、Consistent Hashの弱点であるリマップ時のセッション切断問題をカバーしている。

今回の発表では、これらの機能をXDPの基盤上で実装したのちIPVSベースの既存環境を置き換えていることが明かされた。スライドでは下のグラフが示され、XDPで実装された新しいShivがIPVSベースのそれと比較して高いスループットを発揮しつつもCPU使用率が低く抑えられている、と述べられている*1

f:id:yunazuno:20170507222416p:plain (スライドp3より; それぞれ1本だけラインが離れているものがXDP, 固まっているものがIPVS)

また、DDoS防御のための仕組み (Dropletと呼ばれる) も併せて紹介されている。

DDoS防御にあたっては、攻撃パケットを可能な限り早い段階で検出・遮断することが望ましい。加えて、攻撃パケット検出のシグネチャプログラマブルに構成・変更可能であることが望ましい。彼らはこれもXDP上に実装している。L4LBのプログラムの前にDDoS検出・遮断のプログラムが実行されるようにすることで、L4LBが余分なリソースを消費することを防いでいる。

f:id:yunazuno:20170507222600p:plain (スライドp10より; #0 XDP Dumpはデバッグ用のもの)

ちなみに、XDPによるDDoS防御についてはCloudflareのエンジニアも同様の発表を行っている。こちらはよりBPFにフォーカスした内容。

XDP in practice: integrating XDP in our DDoS mitigation pipeline (slide)

www.youtube.com

実装の詳細

冒頭のビデオで発表部分が12分弱で収まっていることからも分かる通り、やっていること自体は非常にシンプルである。

ただその裏では、(発表内で直接は触れられていないものの) XDPやその基礎となるeBPFの機能がふんだんに使われていることが見て取れる。これらの機能はFacebookのエンジニア自身によって実装されたのちLinuxカーネルにマージされたものも少なくない。

BPF_MAP_TYPE_LRU_HASH: セッション維持のためのLRU cache

Consistent Hashを用いるL4LBではその特性上、分散先のリアルサーバが増減したタイミングで幾許かのTCPセッションが意図せず切断されてしまう。この弱点を補うため、いくつかのL4LB実装ではセッションテーブルを併用している。TCP SYNパケットがL4LBに到達すると同時にセッションテーブルにセッション情報を記録すれば、仮にConsistent Hashのリマップが発生したとしてもセッションテーブルに基づいてTCPセッションを維持できる、という仕組みである。ただセッションテーブルのサイズは有限であるため、何らかの方法でセッションテーブルのメンテナンスを行う必要がある。

そこで登場するのがeBPF map objectのひとつ、BPF_MAP_TYPE_LRU_HASHである。その名前が示す通り、Least Recently Used (LRU) なハッシュテーブルである。これを利用すれば、先に述べた要件を満たすセッションテーブルを容易に実現できる。 この機能は冒頭で紹介したセッションのスピーカーであるMartin Lauによって開発され、kernel 4.10以降で利用可能である。

xdp_adjust_head(): IP encapsulation

ShivはL3DSRによるリアルサーバへのパケット転送にIPIP方式を採用している。これはリアルサーバ宛てIPヘッダで元のVIP宛てパケットをカプセル化することでパケット転送を実現するものである*2。 これをXDPで実現するため、メモリページ上であらかじめ空間 (headroom) を確保しておき、そこにデータ(ここではリアルサーバ宛てのIPヘッダ)を追加するインタフェースが用意されている。このインタフェースがeBPF helper functionのひとつ、bpf_xdp_adjust_head()である。この機能も同じくMartin Lauによって開発され、kernel 4.10以降で利用可能である。

なお、NICドライバには通常のXDPサポートとは別にheadroom確保のサポートが要求される。

BPF_MAP_TYPE_PROG_ARRAY & tail call: 複数のeBPFプログラムを連携させる

XDPはその仕様上、1個のネットワークインタフェースに対して1個のeBPFプログラムのみアタッチすることができる。これは1個のネットワークインタフェースに対して複数の独立した処理を適用したい場合に都合が悪い。 これを解決するのがBPF_MAP_TYPE_PROG_ARRAYbpf_tail_call() である。

yunazuno.hatenablog.com

上記エントリで紹介した通り、BPF_MAP_TYPE_PROG_ARRAYbpf_tail_call()を組み合わせることで、複数のeBPFプログラムを連結して呼び出したり実行中のeBPFプログラムを動的に入れ替えたりできる。ちなみにこの機能を実装したAlexei StarovoitovFacebookに在籍している*3

余談: XDPの近況

2016年10月にXDPがリリースされてから約半年が経過したが、その間に様々なアップデートが行われた。

まず大きなトピックとしてNICドライバのサポート拡充が挙げられる。当初はMellanox mlx4のみであったものが徐々に拡充され、先日遂にIntel ixgbeサポートがnet-nextにマージされた。恐らくカーネル4.12でリリースされると思われ、XDPが利用可能な環境が大幅に広がることになる。また変わり種として、CaviumのARM SoCであるThunderXでのサポートもnet-nextにマージされている

現時点では、下記のNICドライバで利用可能あるいは近日中に利用可能となる:

  • mlx4: Mellanox ConnectX-3
  • mlx5: Mellanox ConnectX-4 or later
  • nfp: Netronome Agilio
  • qede: QLogic FastlinQ
  • virtio_net: Virtio
  • bnxt_en: Broadcom NetXtreme C-Series
  • ixgbe: Intel X5xx (82598, 82599) (from kernel 4.12?)
  • thunderx: Cavium ThunderX (from kernel 4.12?)

また、NICドライバのサポートが無くてもXDPプログラムを実行できるようにする仕組み (“Generic XDP”) も登場した。

kernel/git/torvalds/linux.git - Linux kernel source tree - net: Generic XDP

これは主に開発用やお試し用を意図しており、パフォーマンスに関しては考慮されていない。これまでXDPプログラムを実行するには、対応NICが挿さったハードウェアを用意するか、あるいはe1000, virtio_netの環境を用意する必要があった。Generic XDPの登場により、より手軽に開発環境を整えられるようになる。

なお、各カーネルバージョン毎にサポートされるeBPF/XDP関連の機能やドライバの情報はiovisor/bccのドキュメントに纏まっている:

bcc/kernel-versions.md at master · iovisor/bcc · GitHub

XDPリリース直後の2016年12月にPLUMgrid*4VMWareに買収される出来事があったものの、特に大きな影響も無くXDP周辺の発展は続いている様子である。eBPF自体を含め機能がどんどん拡充されており、かつXDPのユースケースも徐々に出始めてきたことで、今後の動向がますます楽しみ。

*1:正直なところ、このグラフは読み方や環境、条件がよく分からないので何とも言えないところ。もちろん、XDPがIPVSと比較して同一条件下で高スループットが得られる、という結果自体は疑いようが無いものの・・・。

*2:対してIPヘッダのDSCPフィールドを使うのがDSCP方式 https://www.nanog.org/meetings/nanog51/presentations/Monday/NANOG51.Talk45.nanog51-Schaumann.pdf

*3:当時はPLUMgrid

*4:XDPを推進していた陣営のひとつ

eBPF tail call 使い方メモ

eBPFにはtail callと呼ばれるプログラム呼び出し方法が存在する。 これは、ある実行中のBPFプログラムから別のBPFプログラムにジャンプするものである。あくまでジャンプであって、通常の関数呼び出しと違って呼び出し元に処理は戻らない。ユースケースとしては例えば、役割が違う複数のプログラムの独立性を保ったまま連携させたい場合に役立つ。

bpf: introduce bpf_tail_call() helper [LWN.net]

また、tail callの面白い点として、ジャンプ先のルックアップは BPF_MAP_TYPE_PROG_ARRAY という種類のBPF mapを使う点が挙げられる。このmapは通常のBPF map同様、プログラム実行中にユーザ空間側から動的に書き換えることができる。したがって、ジャンプ先のプログラムを動的に入れ替えたり、あるいはジャンプを取りやめるといったことが容易に実現できる。

通常の使用例

tail callを使うには、BPF_MAP_TYPE_PROG_ARRAY のBPF mapを用意した上で bpf_tail_call() helperを使えばよい。カーネルsamples/bpf/sockex3_kern.c が参考になる。

linux/sockex3_kern.c at master · torvalds/linux · GitHub

該当部分を抜粋する*1:

#define PARSE_VLAN 1
#define PARSE_MPLS 2
#define PARSE_IP 3
#define PARSE_IPV6 4

static inline void parse_eth_proto(struct __sk_buff *skb, u32 proto)
{
    switch (proto) {
    case ETH_P_8021Q:
    case ETH_P_8021AD:
        bpf_tail_call(skb, &jmp_table, PARSE_VLAN);
        break;
    case ETH_P_MPLS_UC:
    case ETH_P_MPLS_MC:
        bpf_tail_call(skb, &jmp_table, PARSE_MPLS);
        break;
    case ETH_P_IP:
        bpf_tail_call(skb, &jmp_table, PARSE_IP);
        break;
    case ETH_P_IPV6:
        bpf_tail_call(skb, &jmp_table, PARSE_IPV6);
        break;
    }
}

jmp_table がprogram arrayである。protoETH_P_8021Q であれば jmp_table[PARSE_VLAN] が示すファイルディスクリプタのBPFプログラムにジャンプする。なお、もしここでjmp_table[n] が有効なBPFプログラムでない場合、そのまま処理が続行(fall-through)される点に注意。逆にこの特性を利用すれば、トラブルシューティングの時だけデバッグ用のBPFプログラムを登録する、といったことが可能になる。

bccでの使用例

ここではiovisor/bccを使った場合の動作を試してみることにする。bccにはprog_array.call()という構文のsyntax sugarが用意されており、bpf_tail_call()と等価な処理を容易に実現できる。

ここでは例として、通常時は全てのパケットを通過するが、コマンドによって全てのパケットをドロップするXDPプログラムを考える*2

XDP program

カーネル空間側で実行されるXDPのコードは下記の通り。通常時はprog_entryを実行するが、パケットをドロップさせたい時はprog_dropにジャンプさせる。jmp_tableがprogram arrayである。

なお、events.perf_submit() はログをカーネル空間からユーザ空間に送るのに使用する。これもbccのsyntax sugarで、bpf_perf_event_output()と等価である。

// xdp_chain.c
BPF_TABLE("prog", int, int, jmp_table, 8);
BPF_PERF_OUTPUT(events);

struct event_t {
    u64 timestamp;
    char message[128];
};

int prog_drop(struct xdp_md *ctx)
{
    struct event_t e = {
        .timestamp = bpf_ktime_get_ns(),
        .message = "Enter prog_drop\0",
    };
    events.perf_submit(ctx, &e, sizeof(e));

    return XDP_DROP;
}


int prog_entry(struct xdp_md *ctx)
{
    struct event_t e_enter = {
        .timestamp = bpf_ktime_get_ns(),
        .message = "Enter prog_entry\0",
    };
    events.perf_submit(ctx, &e_enter, sizeof(e_enter));

    jmp_table.call(ctx, 0);

    struct event_t e_exit = {
        .timestamp = bpf_ktime_get_ns(),
        .message = "Exit prog_entry\0",
    };
    events.perf_submit(ctx, &e_exit, sizeof(e_exit));

    return XDP_PASS;
}

動作を観察する

ここでは簡単のため、ユーザ空間側の処理はipython経由で逐次実行する。

まずは下準備としてプログラムのロードまでを実施する。ここでevents.open_perf_buffer()は前述したログを読み出すためのもので、bpf.kprobe_poll()を呼び出してログが到着するとprint_event()がコールバックされる。

$ sudo ipython
In [1]: %cpaste
Pasting code; enter '--' alone on the line to stop or use Ctrl-D.
:import ctypes as ct
:from bcc import BPF
:
:class Event(ct.Structure):
:    _fields_ = [
:        ("timestamp", ct.c_uint64),
:        ("message", ct.c_char * 128),
:    ]
:
:def print_event(cpu, data, size):
:    event = ct.cast(data, ct.POINTER(Event)).contents
:    print event.timestamp, event.message
:
:
:bpf = BPF(src_file="./xdp_chain.c")
:
:events = bpf["events"]
:events.open_perf_buffer(print_event)
:
:prog_entry = bpf.load_func("prog_entry", BPF.XDP)
:prog_drop = bpf.load_func("prog_drop", BPF.XDP)
:<EOF>

prog_entryをインタフェースens9にアタッチ。XDPでのパケット処理が開始される。

In [2]: bpf.attach_xdp("ens9", prog_entry)

ログは以下のようになっている。jmp_table に何も登録されていないためfall-throughされていることが確認できる。

In [3]: bpf.kprobe_poll()
87485224130884 Enter prog_entry
87485224144293 Exit prog_entry
87487208201839 Enter prog_entry
87487208220583 Exit prog_entry

この状態で、jmp_table[0]prog_drop を登録してみる。

In [4]: jmp_table = bpf["jmp_table"]
In [5]: jmp_table[0] = prog_drop.fd 

外部からens9宛のpingが通らなくなり、パケットがドロップされるようになったことが確認できる。ログを確認すると、prog_dropが実行され、かつprog_entryの末尾部分に処理が戻っていないことも確認できる。

In [6]: bpf.kprobe_poll()
87669224146031 Enter prog_entry
87669224160730 Enter prog_drop
87671208094900 Enter prog_entry
87671208110055 Enter prog_drop

jmp_table[0]を削除すれば、再び最初の状態に戻る。

In [7]: del jmp_table[0]
In [8]: bpf.kprobe_poll()
87875240128813 Enter prog_entry
87875240144164 Enter prog_drop
87877224181681 Enter prog_entry
87877224199698 Enter prog_drop
87879208128601 Enter prog_entry
87879208145440 Exit prog_entry

*1:ライセンスは抜粋元のGPLに従う

*2:もちろん実用上は全てのパケットをドロップするのではなく何らかの条件に従ってドロップすることになる