SRv6とは

SRv6はIPv6拡張の一つでSource Routingを実現するもの。Source Routingは、データ送信者がその宛先だけでなく、経路についても指定することを意味する。 経由するノードをSID(Segment Identifier)によって識別し、そのリストをパケットヘッダに含めることで、経路を自由に制御できる。SRv6では、IPv6アドレスがSIDに対応する。

SRv6は、EITF(Internet Engineering Task Force)を中心に仕様の策定が進められている 1 。 2020年3月にはRFC8754 2 として公開された。

SRv6で使われるIPv6ヘッダのSRH(Segment Routing Header)について詳しく見ていく。 まず、Routing TypeはSegment Routingではマジックナンバー4になる。 Segment List[0] ~ Segment list[n] のエントリに、最後のセグメントから降順に経由させたいセグメント一覧を列挙していく。 次のセグメントへの番号をSegments Left、最後のSegmentの番号をLast Entryに格納する。 セキュリティ機構であるHMACなど付加情報がある場合には、TLV(Type Length Value)として追加する。

Routing headers are defined in [RFC8200]. The Segment Routing Header (SRH) has a new Routing Type (4).

The SRH is defined as follows:

 0                   1                   2                   3
 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Next Header   |  Hdr Ext Len  | Routing Type  | Segments Left |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|  Last Entry   |     Flags     |              Tag              |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|                                                               |
|            Segment List[0] (128-bit IPv6 address)             |
|                                                               |
|                                                               |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|                                                               |
|                                                               |
                              ...
|                                                               |
|                                                               |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|                                                               |
|            Segment List[n] (128-bit IPv6 address)             |
|                                                               |
|                                                               |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
//                                                             //
//         Optional Type Length Value objects (variable)       //
//                                                             //
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+

RFC8754より引用

以降、Segment Routingを開始するノードをIngress ノード、Segmentに対応するノードをSegment endpoint、Segment Routingを終了するノードをEgressノードと呼んで区別する。 SRv6には、SRHをどこに配置するかによって、2種類の実現方法がある。

1つ目は、Encapである。オリジナルのパケットはそのままに、SRH込みのIPv6ヘッダでカプセル化する(以降InnerとOuterで区別)。ingressノードでは、Outer IPv6ヘッダの宛先を最初のセグメントに埋め込んで送り出す。Segment Endpointでは、順次Outer IPv6ヘッダの宛先を更新しながら次のSegmentへと転送する。Egressノードでは、Inner パケットを取り出して、オリジナルの宛先へと転送する。

2つ目は、Inline(あるいはDirect)である。単純に、オリジナルなIPv6ヘッダの直後にSRHヘッダを挿入する。

Linux KernelでのSRv6実装

Linux KernelにおけるSRv6実装について詳細がARNW ‘17で公開されている 3。 エンドホストとルータの両方で利用可能な実装となっている。Linux Kernel 4.10 にマージ済み。当時初めてのOpen Source実装だった。のちにfd.io vpp でも実装された。

パケットを受信すると、ネットワークドライバが skb を割り当てる。その後、ルーティングに関する幾つかのステージを経由する。IPv6ではそれぞれのステージは、ip6_rcv()がPREROUTING、ip6_forward()がFORWARD、ip6_output()がPOSTROUTING、ip6_input()がINPUT、ip6_xmit()とip6_local_out()がOUTPUTステージに対応する。

seg6_enabled sysctl が有効になっている場合、INPUTステージで ipv6_srh_rcv()が呼ばれ、SRv6独自の処理が実施される。例えば、Segment Left が 0 であれば、Egressノードなので、innerパケットを取り出す。0 以外であれば、Segment Endpointなので、Segment Left のデクリメントと宛先の更新を実施して、次のセグメントに転送する。

出典: Implementing IPv6 Segment Routing in the Linux Kernel

続いて、SRHの追加・削除部分についても詳しく見ていく。 カプセリングは、インターフェイスを作らない仮想的なトンテルである Light Weight Tunnels (LWTs)の仕組みを使って実装している。ルートごとにinput、outputの2つの関数ポインタを持てるので、ここでSRv6特有の処理を設定している。最終的には、seg6_do_srh_inline() あるいは seg6_do_srh_encap() によってSRHが追加される。 なお、通常は iproute2 の ip route コマンドで設定するが、setsockopt()を使ってソケット単位でも制御できる。 また、Segment EndpointではEnd.DX4などのfunctionを実行できる 4

性能

SRH(Segment Routing Header)の挿入やカプセリングによるオーバヘッドは小さく、15%未満に抑えられる。 一方で、セキュリティ機構であるHMACは高コストだった。

出典: Implementing IPv6 Segment Routing in the Linux Kernel

Plainが通常のIPv6フォワーディングに対応する。 初回の計測結果、以下のカーネル関数でCPU時間を大量に消費していた。

  • fib6_lookup():seg6_input()のキャッシュ機構を見直した。seg6_output()にはキャッシュ機構が存在していた。
  • __slab_free():SRHのための領域確保を別CPUで実施すると解放時にspinlockしてしまう。skbの headroom を使って解決した。

2回目の計測では、オーバヘッドの低下を抑えられた。パケットサイズを変更してみても傾向は変わらなかった。

出典: Implementing IPv6 Segment Routing in the Linux Kernel

動作例

全てのRouterはL3 IPv6フォワーディングを担当する。 スタティックルートを事前に追加しておく。 全てのRouterとHostは namespace で区別する。 R1をingress nodeと見立てて、R4を経由するようにSRHを付与する。R1においてSRHを付与した直後にパケットをキャプチャする。

# create namespaces
for ns in h1 h2 r1 r2 r3 r4
do
  ip netns del $ns
  ip netns add $ns
  ip netns exec $ns sysctl net.ipv6.conf.all.forwarding=1
  ip netns exec $ns sysctl net.ipv6.conf.all.seg6_enabled=1
done
 
# setup veth pair
ip link add h1-r1 type veth peer name r1-h1
ip link add r1-r2 type veth peer name r2-r1
ip link add r2-r3 type veth peer name r3-r2
ip link add r3-h2 type veth peer name h2-r3
ip link add r2-r4 type veth peer name r4-r2
 
# assign veth to namespace
ip link set h1-r1 netns h1
ip link set r1-r2 netns r1
ip link set r2-r3 netns r2
ip link set r3-h2 netns r3
ip link set r2-r4 netns r2
 
ip link set r1-h1 netns r1
ip link set r2-r1 netns r2
ip link set r3-r2 netns r3
ip link set h2-r3 netns h2
ip link set r4-r2 netns r4
 
# link up
ip netns exec h1 ip link set h1-r1 up
ip netns exec r1 ip link set r1-r2 up
ip netns exec r2 ip link set r2-r3 up
ip netns exec r3 ip link set r3-h2 up
ip netns exec r2 ip link set r2-r4 up
 
ip netns exec r1 ip link set r1-h1 up
ip netns exec r2 ip link set r2-r1 up
ip netns exec r3 ip link set r3-r2 up
ip netns exec h2 ip link set h2-r3 up
ip netns exec r4 ip link set r4-r2 up
 
# assign ipv6 addr
ip netns exec h1 ip addr add fc00:a::1/64 dev h1-r1
ip netns exec r1 ip addr add fc00:b::1/64 dev r1-r2
ip netns exec r2 ip addr add fc00:c::1/64 dev r2-r3
ip netns exec r3 ip addr add fc00:d::1/64 dev r3-h2
ip netns exec r2 ip addr add fc00:e::1/64 dev r2-r4
 
ip netns exec r1 ip addr add fc00:a::2/64 dev r1-h1
ip netns exec r2 ip addr add fc00:b::2/64 dev r2-r1
ip netns exec r3 ip addr add fc00:c::2/64 dev r3-r2
ip netns exec h2 ip addr add fc00:d::2/64 dev h2-r3
ip netns exec r4 ip addr add fc00:e::2/64 dev r4-r2
 
# add route
ip netns exec h1 ip -6 route add fc00:d::/64 via fc00:a::2
ip netns exec h1 ip -6 route add fc00:e::/64 via fc00:a::2
ip netns exec r1 ip -6 route add fc00:d::/64 via fc00:b::2
ip netns exec r1 ip -6 route add fc00:e::/64 via fc00:b::2
ip netns exec r2 ip -6 route add fc00:d::/64 via fc00:c::2
ip netns exec r4 ip -6 route add fc00:d::/64 via fc00:e::1
 
ip netns exec h2 ip -6 route add fc00:a::/64 via fc00:d::1
ip netns exec r4 ip -6 route add fc00:a::/64 via fc00:e::1
ip netns exec r3 ip -6 route add fc00:a::/64 via fc00:c::1
ip netns exec r2 ip -6 route add fc00:a::/64 via fc00:b::1
 
# start tcpdump
rm -rf /tmp/srv6-demo.pcap
timeout 20 ip netns exec r2 tcpdump -i r2-r1 ip6 -w /tmp/srv6-demo.pcap &
 
# ping with default path from h1 to h2
sleep 3
ip netns exec h1 ping -6 -c 1 fc00:d::2
 
# enable seg6
ip netns exec r4 sysctl net.ipv6.conf.r4-r2.seg6_enabled=1
ip netns exec r2 sysctl net.ipv6.conf.r2-r4.seg6_enabled=1
ip netns exec r3 sysctl net.ipv6.conf.r3-r2.seg6_enabled=1
ip netns exec h2 sysctl net.ipv6.conf.h2-r3.seg6_enabled=1
 
for mode in encap inline
do
  # add seg6 route
  ip netns exec r1 ip -6 route del fc00:d::/64
  ip netns exec r1 ip -6 route add fc00:d::/64 encap seg6 mode $mode \
    segs fc00:e::2 dev r1-r2
  ip netns exec r1 ip -6 route show
 
  # ping with SRv6 path from h1 to h2
  sleep 3
  ip netns exec h1 ping -6 -c 1 fc00:d::2
done
 
for job in `jobs -p`
do
  wait $job
done

キャプチャ内容を見ていく。

$ tshark -r /tmp/srv6-demo.pcap | grep "(ping) request"

# Plain ping6
   17   2.941160    fc00:a::1 → fc00:d::2    ICMPv6 118 Echo (ping) request id=0x1839, seq=1, hop limit=63
# SRv6 Encap ping6
   21   5.962394    fc00:a::1 → fc00:d::2    ICMPv6 182 Echo (ping) request id=0x1847, seq=1, hop limit=64
# SRv6 Inline ping6
   27   8.974298    fc00:a::1 → fc00:d::2    ICMPv6 158 Echo (ping) request id=0x184c, seq=1, hop limit=63

$ tshark -r /tmp/srv6-demo.pcap -Y "frame.number == 17 or frame.number == 21 or frame.number == 27" -V

# Plain ping6
# ベースライン。素直にH1 fc00:a::1からH2 fc00:d::2へ送られる
Frame 17: 118 bytes on wire (944 bits), 118 bytes captured (944 bits)
Ethernet II, Src: 2a:46:70:ec:24:c3 (2a:46:70:ec:24:c3), Dst: 32:b7:05:4c:f8:81 (32:b7:05:4c:f8:81)
Internet Protocol Version 6, Src: fc00:a::1, Dst: fc00:d::2
    Source: fc00:a::1
    Destination: fc00:d::2
Internet Control Message Protocol v6

# SRv6 Encap ping6
Frame 21: 182 bytes on wire (1456 bits), 182 bytes captured (1456 bits)
Ethernet II, Src: 2a:46:70:ec:24:c3 (2a:46:70:ec:24:c3), Dst: 32:b7:05:4c:f8:81 (32:b7:05:4c:f8:81)
Internet Protocol Version 6, Src: fc00:b::1, Dst: fc00:e::2 # outer IPv6 header
    Source: fc00:b::1
    Destination: fc00:e::2
    Routing Header for IPv6 (Segment Routing)
        Next Header: IPv6 (41)
        Length: 2
        [Length: 24 bytes]
        Type: Segment Routing (4)
        Segments Left: 0
        First segment: 0
        Flags: 0x00
            0... .... = Unused: 0x0
            .0.. .... = Protected: False
            ..0. .... = OAM: False
            ...0 .... = Alert: Not Present
            .... 0... = HMAC: Not Present
            .... .000 = Unused: 0x0
            [Expert Info (Note/Undecoded): Dissection for SRH TLVs not yet implemented]
                [Dissection for SRH TLVs not yet implemented]
                [Severity level: Note]
                [Group: Undecoded]
        Reserved: 0000
        Address[0]: fc00:e::2
        [Segments in Traversal Order]
            Address[0]: fc00:e::2
Internet Protocol Version 6, Src: fc00:a::1, Dst: fc00:d::2 # inner IPv6 header、src/dstは変更しない
    Source: fc00:a::1
    Destination: fc00:d::2
Internet Control Message Protocol v6

# SRv6 Inline ping6
Frame 27: 158 bytes on wire (1264 bits), 158 bytes captured (1264 bits)
Ethernet II, Src: 2a:46:70:ec:24:c3 (2a:46:70:ec:24:c3), Dst: 32:b7:05:4c:f8:81 (32:b7:05:4c:f8:81)
Internet Protocol Version 6, Src: fc00:a::1, Dst: fc00:e::2
    Source: fc00:a::1
    Destination: fc00:e::2
    Routing Header for IPv6 (Segment Routing)
        Next Header: ICMPv6 (58)
        Length: 4
        [Length: 40 bytes]
        Type: Segment Routing (4)
        Segments Left: 1
        First segment: 1
        Flags: 0x00
            0... .... = Unused: 0x0
            .0.. .... = Protected: False
            ..0. .... = OAM: False
            ...0 .... = Alert: Not Present
            .... 0... = HMAC: Not Present
            .... .000 = Unused: 0x0
            [Expert Info (Note/Undecoded): Dissection for SRH TLVs not yet implemented]
                [Dissection for SRH TLVs not yet implemented]
                [Severity level: Note]
                [Group: Undecoded]
        Reserved: 0000
        Address[0]: fc00:d::2 [next segment] # 最終的な宛先はSRHに含まれる
        Address[1]: fc00:e::2
        [Segments in Traversal Order]
            Address[1]: fc00:e::2
            Address[0]: fc00:d::2 [next segment]
Internet Control Message Protocol v6

まとめ

シンプルな構成で動作を確認できた。End.DX4などのfunctionも使ってみたい。 encapとinlineのPros/Consを知りたい。