OSPFによる動的ルーティングをdocker container間で動かす

ひとつのVM内でdocker containerを複数立ち上げ,その間でOSPFを動かす.

pingを通すだけでなく,動的に経路が切り替わるのを確認する. コードはこちら.

github.com

ホストの立ち上げから必要な設定の注入に関しては以下のサイトにとても丁寧に書いてある. 今回の設定については,ほんの数行なのでgithubを参照してください. qiita.com

環境

ネットワーク構成

large/docker-compose.yamlの通りです.

動作

1. コンテナの立ち上げ

$ git clone https://github.com/kkti4216/docker-ospf.git
$ cd docker-ospf/large
$ docker-compose up -d

2. h1からh2への経路を確認

h1 -> frr1 -> frr2 -> h2となっている.

$ docker container exec -it h1 bash
bash-5.1# traceroute -n 192.168.2.12
traceroute to 192.168.2.12 (192.168.2.12), 30 hops max, 46 byte packets
 1  192.168.1.11  0.004 ms  0.002 ms  0.002 ms
 2  192.168.11.22  0.003 ms  0.003 ms  0.002 ms
 3  192.168.2.12  0.002 ms  0.002 ms  0.003 ms

3. frr1 - frr2のリンクのコストを大きく

沼に嵌った話はおまけへ.

frr1-eth1 (frr2側のインターフェース) のコストを編集.

$ docker container exec -it frr1 bash
bash-5.1# vtysh

Hello, this is FRRouting (version 8.2.2_git).
Copyright 1996-2005 Kunihiro Ishiguro, et al.

frr1# conf
frr1(config)# int eth1
frr1(config-if)# ip ospf cost 100

frr2-eth0も同様にコストを編集.

$ docker container exec -it frr2 bash
bash-5.1# vtysh

Hello, this is FRRouting (version 8.2.2_git).
Copyright 1996-2005 Kunihiro Ishiguro, et al.

frr2# conf
frr2(config)# int eth0
frr2(config-if)# ip ospf cost 100

再びh1からh2への経路を確認. frr3を経由するようになった.

$ docker container exec -it h1 bash
bash-5.1# traceroute -n 192.168.2.12
traceroute to 192.168.2.12 (192.168.2.12), 30 hops max, 46 byte packets
 1  192.168.1.11  0.005 ms  0.002 ms  0.002 ms
 2  192.168.33.33  0.001 ms  0.002 ms  0.002 ms
 3  192.168.22.22  0.002 ms  0.003 ms  0.002 ms
 4  192.168.2.12  0.002 ms  0.003 ms  0.003 ms

この時のh1のルーティングテーブルは以下のようになる.

$ docker container exec -it frr1 bash
frr1# show ip ospf route
============ OSPF network routing table ============
N    192.168.1.0/24        [10] area: 0.0.0.0
                           directly attached to eth0
N    192.168.2.0/24        [30] area: 0.0.0.0
                           via 192.168.33.33, eth2
N    192.168.3.0/24        [20] area: 0.0.0.0
                           via 192.168.33.33, eth2
N    192.168.11.0/24       [100] area: 0.0.0.0
                           directly attached to eth1
N    192.168.22.0/24       [20] area: 0.0.0.0
                           via 192.168.33.33, eth2
N    192.168.33.0/24       [10] area: 0.0.0.0
                           directly attached to eth2

============ OSPF router routing table =============

============ OSPF external routing table ===========

4. frr2-eth2をdownさせる

frr3 - frr2の接続が切れるため,h1 -> h2のパケットはコストの大きいリンクを通ることになる.

$ docker container exec -it frr2 bash
bash-5.1# ip link set eth2 down

h1から再びtracerouteすると,たしかにfrr3を経由しない経路へ戻った.

$ docker container exec -it f1 bash
bash-5.1# traceroute -n 192.168.2.12
traceroute to 192.168.2.12 (192.168.2.12), 30 hops max, 46 byte packets
 1  192.168.1.11  0.004 ms  0.003 ms  0.003 ms
 2  192.168.11.22  0.003 ms  0.002 ms  0.003 ms
 3  192.168.2.12  0.002 ms  0.003 ms  0.002 ms

この時のfrr1のルーティングテーブルは以下. 192.168.2.0/24へのコストが大きくなっている.

 $ docker container exec -it frr1 bash
 frr1# show ip ospf route
============ OSPF network routing table ============
N    192.168.1.0/24        [10] area: 0.0.0.0
                           directly attached to eth0
N    192.168.2.0/24        [110] area: 0.0.0.0
                           via 192.168.11.22, eth1
N    192.168.3.0/24        [20] area: 0.0.0.0
                           via 192.168.33.33, eth2
N    192.168.11.0/24       [100] area: 0.0.0.0
                           directly attached to eth1
N    192.168.22.0/24       [20] area: 0.0.0.0
                           via 192.168.33.33, eth2
N    192.168.33.0/24       [10] area: 0.0.0.0
                           directly attached to eth2

============ OSPF router routing table =============

============ OSPF external routing table ===========

また,frr3のルーティングテーブルを確認すると,h2 (192.168.2.0/24) への経路がfrr1経由となっておりdownさせたリンクを迂回できている.

$ docker container exec -it frr3 bash
bash-5.1# vtysh

Hello, this is FRRouting (version 8.2.2_git).
Copyright 1996-2005 Kunihiro Ishiguro, et al.

frr3# show ip ospf route
============ OSPF network routing table ============
N    192.168.1.0/24        [20] area: 0.0.0.0
                           via 192.168.33.11, eth2
N    192.168.2.0/24        [120] area: 0.0.0.0
                           via 192.168.33.11, eth2
N    192.168.3.0/24        [10] area: 0.0.0.0
                           directly attached to eth1
N    192.168.11.0/24       [110] area: 0.0.0.0
                           via 192.168.33.11, eth2
N    192.168.22.0/24       [10] area: 0.0.0.0
                           directly attached to eth0
N    192.168.33.0/24       [10] area: 0.0.0.0
                           directly attached to eth2

============ OSPF router routing table =============

============ OSPF external routing table ===========

5. まとめ

実際に経路が切り替わるのはネットワークを弄れている実感が湧いて楽しい. docker network や docker-compose などdockerまわりのいい勉強にもなった.

おまけ

コストを大きくする際,片方のインターフェースのみしか編集しておらず沼に嵌った.

3. frr1 - frr2のリンクのコストを大きくから分岐.

frr1のeth1 (frr2と接続しているinterface) のコストを大きくする.

$ docker container exec -it frr1 bash
bash-5.1# vtysh
frr1# conf
frr1(config)# interface eth1
frr1(config-if)# ip ospf cost 100

再びh1からh2への経路を確認. frr3を経由するようになっている.

$ docker container exec -it h1 bash
bash-5.1# traceroute -n 192.168.2.12
traceroute to 192.168.2.12 (192.168.2.12), 30 hops max, 46 byte packets
 1  192.168.1.11  0.004 ms  0.002 ms  0.002 ms
 2  192.168.33.33  0.002 ms  0.002 ms  0.003 ms
 3  192.168.11.22  0.001 ms  0.002 ms  0.002 ms
 4  192.168.2.12  0.002 ms  0.002 ms  0.003 ms

ただし,3 192.168.11.22 0.001 ms 0.002 ms 0.002 ms のルータのアドレスが 192.168.22.22 になっていない. 試しにfrr2-eth0をdownさせてみる.

$ docker container exec -it frr2 bash
bash-5.1# ip link set eth0 down

もう一度経路を確認.

$ docker container exec -it h1 bash
bash-5.1# traceroute -n 192.168.2.12
traceroute to 192.168.2.12 (192.168.2.12), 30 hops max, 46 byte packets
 1  192.168.1.11  0.004 ms  0.002 ms  0.003 ms
 2  192.168.33.33  0.001 ms  0.002 ms  0.003 ms
 3  192.168.22.22  0.002 ms  0.002 ms  0.003 ms
 4  192.168.2.12  0.002 ms  0.003 ms  0.003 ms

正しく192.168.22.22を経由できている. tracerouteTTL値をインクリメントしながら送信し,返ってくるICMPパケットを見る. frr2でTTL=0となるパケットの経路がfrr2-eth0経由となったのが原因と考えられる.

本来一番最初にするべきであるルーティングテーブルを確認. frr2では,

$ docker container exec -it frr2 bash
frr2# show ip ospf route
============ OSPF network routing table ============
N    192.168.1.0/24        [20] area: 0.0.0.0
                           via 192.168.11.11, eth0
N    192.168.2.0/24        [10] area: 0.0.0.0
                           directly attached to eth1
N    192.168.3.0/24        [20] area: 0.0.0.0
                           via 192.168.22.33, eth2
N    192.168.11.0/24       [10] area: 0.0.0.0
                           directly attached to eth0
N    192.168.22.0/24       [10] area: 0.0.0.0
                           directly attached to eth2
N    192.168.33.0/24       [20] area: 0.0.0.0
                           via 192.168.11.11, eth0
                           via 192.168.22.33, eth2

============ OSPF router routing table =============

============ OSPF external routing table ===========

h1(192.168.1.12)へのパケットはコストを100に設定したはずのfrr1-eth1を経由することになっている(通常のインターフェースのコストは10). インターフェースにコストを設定した場合,送信する側のコストのみが影響する様子? 調べてみると一般的には受信側のコストで計算されるらしいがよくわからず...

frr1のルーティングテーブルも確認してみると,192.168.11.0/24への経路はfrr1 -> frr3 -> frr2 -> となっている.

$ docker container exec -it frr1 bash
bash-5.1# vtysh
frr1# show ip ospf route
============ OSPF network routing table ============
N    192.168.1.0/24        [10] area: 0.0.0.0
                           directly attached to eth0
N    192.168.2.0/24        [30] area: 0.0.0.0
                           via 192.168.33.33, eth2
N    192.168.3.0/24        [20] area: 0.0.0.0
                           via 192.168.33.33, eth2
N    192.168.11.0/24       [30] area: 0.0.0.0
                           via 192.168.33.33, eth2
N    192.168.22.0/24       [20] area: 0.0.0.0
                           via 192.168.33.33, eth2
N    192.168.33.0/24       [10] area: 0.0.0.0
                           directly attached to eth2

============ OSPF router routing table =============

============ OSPF external routing table ===========

frr2でTTL=0となるパケットの経路がfrr2-eth0経由となったのが原因と考えられる.

で正しそう. 本当はfrr1-eth1でtcpdumpとかすれば良さそう.

ここでfrr2-eth0のコストもfrr-eth1と同様にコストを高くする.

$ docker container exec -it frr2 bash
frr2:/# ip link set eth0 up
frr2:/# vtysh
frr2# conf
frr2(config)# int eth0
frr2(config-if)# ip ospf cost 100

経路を確認.

traceroute to 192.168.2.12 (192.168.2.12), 30 hops max, 46 byte packets
 1  192.168.1.11  0.005 ms  0.002 ms  0.002 ms
 2  192.168.33.33  0.024 ms  0.002 ms  0.002 ms
 3  192.168.22.22  0.002 ms  0.002 ms  0.002 ms
 4  192.168.2.12  0.004 ms  0.002 ms  0.002 ms

正しく192.168.22.22を経由してくれるようになった.

おまけここまで

Pythonに前置インクリメントを追加

はじめに

この記事は東京大学工学部電子情報工学科3年後期実験「大規模ソフトウェアを手探る」の報告記事です。

ここではPython3.10に前置インクリメントを追加する方法について書きます。

以下は同じ班だった同期の記事です。先に「ビルド&構造把握」の記事を読むことをお勧めします。

環境

version
Ubuntu 18.04
grep 3.1
gdb 8.1.0
vscode 1.49.1

python自体のビルドについては班員の記事に書いてあります。

実装

基本的には他の単項演算子である "-" (-1倍)、 "~" (ビット反転)を参考に進めていきます。

これらの単項演算子との違いは変数の値が更新されるところにあります。

公式サイト24. Changing CPython’s Grammarにしたがって書き換えていくとほとんどのファイルは自動生成されます。

Grammar/Tokens(L56)

"++"をトークンとして定義します

INCREMENT               '++'

Grammar/python.gram(L458)

"++"が単項演算子であることを定義します。単項演算子の部分に付け加えましょう。

factor[expr_ty] (memo):
    | '+' a=factor { _Py_UnaryOp(UAdd, a, EXTRA) }
    | '-' a=factor { _Py_UnaryOp(USub, a, EXTRA) }
    | '~' a=factor { _Py_UnaryOp(Invert, a, EXTRA)}
    | '++' a=NAME { _Py_UnaryOp(UInc, a, EXTRA)}
    | power

factorではなくNAMEとなっているのは"++"が付くのは変数に対してのみだからです。++(-x)だと-xをインクリメントすることになり文法として望ましくないです。一方 ~(-x)は値の更新がないので問題ありません。

Parser/Python.asdl(L99)

こちらも他の単項演算子に倣って付け加えます。

unaryop = Invert | Not | UAdd | USub | UInc

Python/ast_unparse.c(L179)

ここでは演算子の優先度を定義しています。優先度が本当にこれで適切かは考慮するべきかもしれません。

switch (e->v.UnaryOp.op) {
    case Invert: op = "~"; pr = PR_FACTOR; break;
    case Not: op = "not "; pr = PR_NOT; break;
    case UAdd: op = "+"; pr = PR_FACTOR; break;
    case USub: op = "-"; pr = PR_FACTOR; break;
    case UInc: op = "++"; pr = PR_FACTOR; break;
    default:
        ...

Lib/opcode.py(L68)

インクリメントのオペコードを定義しています。

def_op('UNARY_POSITIVE', 10)
def_op('UNARY_NEGATIVE', 11)
def_op('UNARY_NOT', 12)
def_op('UNARY_INCREMENT', 13)

Python/compile.c(L5010)

ここまでで定義してきた単項演算子の実際の動作を書いていきます。バイトコードで処理を考えていきましょう。 今回の実装ではインクリメントのオペコードは「+1した値を返す」を行い、演算子の処理で値の更新を行いました。

case UnaryOp_kind:
    if(e->v.UnaryOp.op==UInc){
      VISIT(c, expr, e->v.UnaryOp.operand);
      ADDOP(c, unaryop(e->v.UnaryOp.op));
      ADDOP(c, DUP_TOP);
      assert( e->v.UnaryOp.operand->kind==Name_kind);
      compiler_nameop(c,e->v.UnaryOp.operand->v.Name.id,Store);
    }else{
       VISIT(c, expr, e->v.UnaryOp.operand);
       ADDOP(c, unaryop(e->v.UnaryOp.op));
    }

else以下が既存の単行演算子の処理です。 if以下が"++"の処理ですが、最初の2文は他の単行演算子と同様です。スタックの一番上に+1された後の値が積まれているのでそれをADDOPで複製、一番上をプッシュして値を更新します。

複製を行わないとインクリメントそした時の返り値がなくなってしまいます。

Python/ceval.c(L1608)

インクリメントのオペコードを記述します。スタックの頭をプッシュして+1して積めば良いです。

case TARGET(UNARY_INCREMENT): {
        PyObject *right = TOP();
        PyObject *inv,*sum;
        //-(~x)=x+1
            inv=PyNumber_Invert(right);
            if (inv == NULL)
          goto error;
            sum = PyNumber_Negative(inv);
            Py_DECREF(inv);
            if (sum == NULL)
              goto error;
        Py_DECREF(right);
            SET_TOP(sum);
        DISPATCH();
        }

既存のオペコードとしてビット反転と-1倍がありますのでそれを利用すれば以下のように+1という演算は実装できます。

    x + ~x = 11...11 = 00...00 - 1 = -1 

<=>  x + 1 = -(~x)

確認

以下のようにすればインクリメント演算子が追加されたPythonが使用できるようになります。

make clean
make regen-all
make
make install

ちゃんと動きます!

>>> a=0
>>> ++a
1
>>> a
1

まとめ

ファイルの自動生成が優秀で公式のチェックリストに従って変更していけばそこそこ進められると思います。自動生成されるファイルも大体ファイル冒頭にどのファイルを元に生成されているのかなど詳しく書いてくれているのでとても親切でした。何も考えずに走らせてたPythonが内部でしていることに触れられてとても勉強になりました。

参考