BYB

低レイヤ好き学生エンジニアによる備忘録

c言語でNAT自作 [実機で動作確認]

やったこと

  • jsonファイルの設定をcのプログラムから読み込めるようにする。
  • jsonファイル形式で書き込んだルーティングテーブルの内容を読みこんで、それを基にパケットフォワーディングできるようにする
  • NATを作る

jsonの読み取り

janssonというライブラリを使用すると、jsonファイルの内容を関数一つで構造体に取り出せるようになる。
qiita.com

簡易ルーティングテーブルの読み取り

元のルーターにはルーティングテーブルという概念がなく、デフォルトゲートウェイまたは直に繋がっているサブネットにしか送信できないものだったので、ルーティングテーブルを基にパケットフォワーディングをできるようにした。
本来はCLIでルーティングテーブルの要素の追加や削除を行えるようにするのがよいが、時間がないので今回はルーティングテーブルの情報はあらかじめすべてjsonファイルに書いておいて、それをプログラムから読むという仕様にした。
次のような記法で、ネットワークインターフェースとルーティングテーブルの中身のリストを記述する。

{
    "interfaces":[
        "net0",
        "net1",
        "net2"
    ],
    "routing_table":[
        {
            "dest_addr":"10.1.0.0",
            "subnet_mask":"16",
            "next_hop":"10.2.23.2"
        },
        {
            "dest_addr":"10.1.14.0",
            "subnet_mask":"24",
            "next_hop":"10.2.24.2"
        }
    ]
}

データ構造

ルーティングテーブルのデータ構造として、Radix Tree を採用する。
ja.tech.jar.jp
テーブルをRadix Tree にすると、宛先の情報・サブネットの情報・ネクストホップの情報をその構造自体に内包することができるため、木の探索を行うだけで最長一致によるフォワード先の決定ができる。

struct node  {
        struct node      *parent;
        struct node      *child[2];
        u_int32_t       daddr_subnet;
        u_int8_t        subnet_mask;
        u_int32_t       next_hop;
        u_int32_t       daddr_full;
        int             is_empty;
        int             is_root;
};

処理内容と難しかったポイント

アドレス、サブネットアドレス、サブネットマスクに関する計算する部分を実装するのが一番手間がかかった。
ノードの最長一致での探索は、マスクを一つずつシフトしながら一致するものがないか見てゆく。
今回はシフト処理がうまく書けなかったので、サブネットマスクにあたる数字(0~32)をインクリメントしていき、その数字をネットバイトオーダのマスクに変換する関数に渡している。
この際バイトオーダに注意が必要である。
ノードのroot(サブネットマスク0)からチェックしていき、サブネットが一致するものが見つかったらそれを保存したのち、さらに長いサブネットを持つものと一致しないか探しに木を下ってゆく。
ノードの挿入においては、挿入する本体の親がいないと探索の時に困るため、ダミーノードを作ることで対応した。

NAT機能実装

データ構造

LAN側とWAN側の5tuple(src addr / port , dest addr / port , protocol)を対応づけたものをNATテーブルの要素とする。
またその他にも、現在払い出しているポート番号の情報などもNATテーブルにて保持する。

struct nat_table{
    struct nat_table_element *start;
    struct nat_table_element *end;
    int     num;
    int     used_port[MAX_TABLE_SIZE];
    int     last_gave_port;
};

struct nat_table_element{
    struct  nat_table_element *prev;
    struct  nat_table_element *next;
    struct  five_tuple  *loc_tpl;
    struct  five_tuple  *glo_tpl;
    u_int8_t     protocol;
    time_t      last_time;
    int         is_tcp_estab;
    int         tcp_state;
};

struct five_tuple{
    u_int32_t   src_addr;
    u_int16_t   src_port;
    u_int32_t   dst_addr;
    u_int16_t   dst_port;
    u_int8_t    protocol;
};

処理

パケットの宛先port,addrと送信元port,addrとプロトコルの5つを識別要素として通信の単位を構成し、LAN側からのパケットにその単位ごとにWANでの送信元portを割り振り、パケットを書き換える。
WAN側からのパケットを受け取ったらNATテーブルを参照し、宛先を然るべきLAN側のport,addrに書き換える。

難しかったポイント

ポートの概念がないICMPパケットに関しては、ICMPヘッダに含まれるicmp idをport代わりに識別子として使わなければならないことに気づくまでに時間がかかった。
また、TCPUDPチェックサムには擬似ヘッダを用いなければならない点などは、事前知識として知ってなければならないだろう。

実験

ネットワークシミュレーションツールのtinet を用いてNATの動作確認を行ったので、その様子を載せる。

上記のようなネットワークを組んでc2からr1にpingを飛ばし、r1でパケットキャプチャを行った。
結果は以下の通り。

r2でNATが行われた結果、r1では10.255.1.2(r2)からパケットが飛んできているように見えている。

実機で実験

一方のNICグローバルIPを持ち、もう片方のNICはLANのアドレスをもつUbuntuマシンで動作確認を行った。

ICMP

きちんとNATの役割を果たした。

NATプログラム起動前後のping疎通の様子

TCP

帰りのパケットが壊れてしまう現象に見舞われた。

TCPパケットが壊れて帰ってくる様子