WARNING:  No route found for IPv6 destination fdbb:a1:0:0:1:: (no default route?)となってscapyのsr1()からパケットが上手く送信できない

送信元インターフェースにリンクローカルアドレスしかなく，デフォルトルートがない場合，scapyのsr1()が上手くいかなかったので，その状況を共有します．(こんな状況になる人は少ないとは思いますが)

## 状況

SRv6のテスト用でscapyを使って[自作ping](https://github.com/shu1r0/srv6_ping.git)っぽいものを作っています．
そのプログラムでは，パケット送信するために，scapyの`sr1()`関数を使っていました．
このとき，ルータとして動かしているLinuxノードからパケットを上手く送信できませんでした．

具体的には，以下のようなClosネットワークを動かしまして，各ノードはBGP unnumberedで設定しています．
このネットワークで，l1から実行したときに`WARNING: No route found for IPv6 destination fdbb:a1:0:0:1:: (no default route?)`と出て，パケットを送信できませんでした．

![clos3_srv6vpn_24.drawio.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2661127/5d7a5db6-76f1-ed05-d997-493d7936d87a.png)

以下は，mininet使って実験していたので，mininetのCLIから実行したやつです．
```bash
mininet> l1 srv6ping -c 2 -d fdbb:a1:0:0:1:: --src fdbb:c1:0:0:1::
WARNING: No route found for IPv6 destination fdbb:a1:0:0:1:: (no default route?)
WARNING: No route found for IPv6 destination fdbb:a1:0:0:1:: (no default route?)
WARNING: more No route found for IPv6 destination fdbb:a1:0:0:1:: (no default route?)
timeout.
timeout.
```

デフォルトルートは設定していませんでしたが，以下のようにルートを見てみると，到達先である`fdbb:a1::/64`へのルートは設定されていたので，原因が分からず詰まりました．
```bash
mininet> l1 ip -6 r
fdbb:a1::/64 nhid 33 proto bgp metric 20 pref medium
        nexthop via fe80::789b:afff:fe0f:8d1a dev l1_s2 weight 1 
        nexthop via fe80::286c:1ff:fe64:39f6 dev l1_s1 weight 1 
fdbb:a2::/64 nhid 33 proto bgp metric 20 pref medium
        nexthop via fe80::789b:afff:fe0f:8d1a dev l1_s2 weight 1 
        nexthop via fe80::286c:1ff:fe64:39f6 dev l1_s1 weight 1 
fdbb:a3::/64 nhid 33 proto bgp metric 20 pref medium
        nexthop via fe80::789b:afff:fe0f:8d1a dev l1_s2 weight 1 
        nexthop via fe80::286c:1ff:fe64:39f6 dev l1_s1 weight 1 
fdbb:a4::/64 nhid 33 proto bgp metric 20 pref medium
        nexthop via fe80::789b:afff:fe0f:8d1a dev l1_s2 weight 1 
        nexthop via fe80::286c:1ff:fe64:39f6 dev l1_s1 weight 1 
fdbb:a5::/64 nhid 33 proto bgp metric 20 pref medium
        nexthop via fe80::789b:afff:fe0f:8d1a dev l1_s2 weight 1 
        nexthop via fe80::286c:1ff:fe64:39f6 dev l1_s1 weight 1 
fdbb:a6::/64 nhid 33 proto bgp metric 20 pref medium
        nexthop via fe80::789b:afff:fe0f:8d1a dev l1_s2 weight 1 
        nexthop via fe80::286c:1ff:fe64:39f6 dev l1_s1 weight 1 
fdbb:b1::/64 nhid 34 via fe80::286c:1ff:fe64:39f6 dev l1_s1 proto bgp metric 20 pref medium
fdbb:b2::/64 nhid 16 via fe80::789b:afff:fe0f:8d1a dev l1_s2 proto bgp metric 20 pref medium
fdbb:b3::/64 nhid 33 proto bgp metric 20 pref medium
        nexthop via fe80::789b:afff:fe0f:8d1a dev l1_s2 weight 1 
        nexthop via fe80::286c:1ff:fe64:39f6 dev l1_s1 weight 1 
fdbb:b4::/64 nhid 33 proto bgp metric 20 pref medium
        nexthop via fe80::789b:afff:fe0f:8d1a dev l1_s2 weight 1 
        nexthop via fe80::286c:1ff:fe64:39f6 dev l1_s1 weight 1 
fdbb:b5::/64 nhid 33 proto bgp metric 20 pref medium
        nexthop via fe80::789b:afff:fe0f:8d1a dev l1_s2 weight 1 
        nexthop via fe80::286c:1ff:fe64:39f6 dev l1_s1 weight 1 
fdbb:b6::/64 nhid 33 proto bgp metric 20 pref medium
        nexthop via fe80::789b:afff:fe0f:8d1a dev l1_s2 weight 1 
        nexthop via fe80::286c:1ff:fe64:39f6 dev l1_s1 weight 1 
fdbb:c1:0:0:100:: nhid 12  encap seg6local action End.DT6 table 10 dev group1 proto bgp metric 20 pref medium

(略)
```

## 原因

結論から言うと，送信元アドレスの候補がなかったためにscapyでルートの設定が上手く取れていないこととデフォルトルートがないことが原因でした．

というのも，scapyでは独自のネットワークスタックを持っており，Linuxで設定されたルートがscapyに反映されないと上手く動きません．

scapyのルートは`conf.route6`に設定されており，l1では以下のルートが設定されてました．
```python
>>> from scapy.all import *
>>> from scapy.config import conf
>>> conf.route6
Destination                    Next Hop  Iface  Src candidates             Metric
fd00:1::/128                   ::        l1_h1  fd00:1::1                  0     
fd00:1::1/128                  ::        l1_h1  fd00:1::1                  0     
fd00:1::/64                    ::        l1_h1  fd00:1::1                  256   
fe80::/128                     ::        l1_h1  fe80::bca1:87ff:feb5:618b  0     
fe80::bca1:87ff:feb5:618b/128  ::        l1_h1  fe80::bca1:87ff:feb5:618b  0     
fe80::/64                      ::        l1_h1  fe80::bca1:87ff:feb5:618b  256   
fe80::/64                      ::        l1_s1  fe80::ec29:9ff:fe46:a00e   256   
fe80::/64                      ::        l1_s2  fe80::805d:b6ff:feea:ac55  256   
fe80::/64                      ::        l1_c1  fe80::ccdd:75ff:fefa:d2a8  256   
::1/128                        ::        lo     ::1                        0     
fdbb:c1::/128                  ::        lo     ::1                        0     
fdbb:c1:0:0:1::/128            ::        lo     ::1                        0     
fe80::/128                     ::        l1_s1  fe80::ec29:9ff:fe46:a00e   0     
fe80::/128                     ::        l1_s2  fe80::805d:b6ff:feea:ac55  0     
fe80::/128                     ::        l1_c1  fe80::ccdd:75ff:fefa:d2a8  0     
fe80::805d:b6ff:feea:ac55/128  ::        l1_s2  fe80::805d:b6ff:feea:ac55  0     
fe80::ccdd:75ff:fefa:d2a8/128  ::        l1_c1  fe80::ccdd:75ff:fefa:d2a8  0     
fe80::ec29:9ff:fe46:a00e/128   ::        l1_s1  fe80::ec29:9ff:fe46:a00e   0
```

ここで表示されているルートですが，BGPで学習したルート等が取れてません．

はじめは，ECMPやBGPでのルート設定が原因かと思っていましたが，ソースコードを追っていくと，scapyでの送信元アドレスの候補(Src candidates)の選択が原因とわかりました．

送信元アドレスの候補とは，パケット送信時に送信元アドレスが指定されていない場合，デフォルトで選択されるアドレスを表すものです．RFC6724で詳しく説明されていますが，基本的には送信アドレスからアドレスの候補を選択します．scapyでは，[この関数`construct_source_candidate_set`](https://github.com/secdev/scapy/blob/0dd08cd064134bcbf7c6bfbdbd356b69cdee05ac/scapy/utils6.py#L37)において，同じスコープのアドレスを送信元インターフェースのアドレスから選択しています．

ルートが取れていないノードでは，ルートのインターフェースにリンクローカルアドレスのみが設定されておりグローバルアドレスを設定していません．そのため，グローバルスコープのアドレスを宛先とするルートの送信元アドレスの候補がない状態となり，ルートがscapyに設定されませんでした．

また，デフォルトルートもない上であればデフォルトルートに送信されますが，デフォルトルートがないのでパケットが`conf.iface`に設定された`lo`に向けて送信されていました．


## 解決策

scapyにおいてルートが設定されていないのが基本的な原因なので，ルートを手動で`conf.route6`に入れれば動くやろという解決策でやります．本当は，`sr1()`以外を使うのがスマートだとは思いますが，良い解決策が思いつかなかったので，これでやります．

基本的には，以下のように，必要なルート設定してやることを考えます．
```python
conf.route6.routes.append((dst_prefix, prefix_len, next_hop, dev, iface_addr, metric))
```

しかし，毎回ルートを手動で設定していくのは面倒です．
そのため，Linuxのルーティングテーブルから学習できなかったルートを取得し，送信アドレスの候補を設定した上で，`conf.route6`に無理やり入れる方式でやります．

結果的には，以下のコードを作成しました．
```python
from scapy.all import in6_getifaddr
from scapy.config import conf
from scapy.utils6 import in6_ptop, in6_isincluded
from scapy.layers.inet6 import neighsol, ICMPv6NDOptDstLLAddr, ICMPv6ND_NA

def neighsol_and_chache(addr, iface):
    """MACアドレスを学習"""
    src = [ifaddr[0] for ifaddr in in6_getifaddr() if ifaddr[2] == iface][0]
    if conf.netcache.in6_neighbor.get(addr) is None:
        sol = neighsol(addr, src, iface, chainCC=True)
        if ICMPv6ND_NA in sol:
            taddr = sol[ICMPv6NDOptDstLLAddr].lladdr
            conf.netcache.in6_neighbor[addr] = taddr

def parse_addr6(addr: str) -> str:
    addr = ":".join([addr[i:i+4] for i in range(0, len(addr), 4)])
    return in6_ptop(addr)

def add_target_routes6(dst: str, src: str):
    """送信元アドレスの候補がないsr1()の宛先のルートを追加する

    Args:
        dst (str): sr1で送信するパケットの宛先
        src (str): sr1で送信するパケットの送信元
    """
    with open("/proc/net/ipv6_route", "r") as f:
        # 一行読み取る
        line = f.readline()
        while line:
            line = line.strip().split()
            # ルートのパラメータを読み取る
            d, nh, dev = line[0], line[4], line[9]
            prefix = int(line[1], 16)
            metric = int(line[5], 16)
            d = parse_addr6(d)
            nh = parse_addr6(nh)
            # 必要なルートを追加
            if d != "::" and in6_isincluded(dst, d, prefix):
                # 同じプレフィックスのルートがない場合
                if (d, prefix) not in [(r[0], r[1]) for r in conf.route6.routes]:
                    # ルートを追加
                    conf.route6.routes.append((d, prefix, nh, dev, [src], metric))
                    # デフォルトルートがない場合，ICMPv6 NSも上手く送信できないので手動でMACアドレスをキャッシュ
                    if nh != "::":
                        neighsol_and_chache(nh, dev)
            line = f.readline()
```
まず，scapyでは，`/proc/net/ipv6_route`からルートを抜き出していたので，学習できなかった宛先へのルートを同様に抜き出します．

また，送信元アドレスが決定できないので，送信元アドレスを手動で決めます．
コードの`add_target_routes6`では，`sr1()`で送信するための送信元アドレスを引数で取ります．(この記事では送信元アドレスを手動で指定するようにしていますが，loといった他のインターフェースから取ってきても良いと思います．)

上の`add_target_routes6`では，`neighsol_and_chache`でネクストホップのMACアドレスを取得して`conf.netcache.in6_neighbor`に設定しています．というのは，デフォルトルートがないので，Neighbor Solicitation (NS)が自動で送信できないためです．

以上のコードで，とりあえずこの状況では`sr1()`で送信できるようになりました．

## おわりに
もっと上手い方法がありそう．


## 参考文献

- https://github.com/secdev/scapy/blob/master/scapy/arch/linux.py
- https://datatracker.ietf.org/doc/html/rfc6724