外部記憶装置

脳みそ小さすぎるからメモしとく

Zen言語でTCP/IPプロトコルスタックをスクラッチしたかった

この記事は 自作 OS アドベントカレンダー 2019 の 8 日目の記事です

adventar.org

近況報告

こんにちは。VTb(@PiBVT)です。最近はtwitterで発言する機会が少なくなり表舞台からは消えてますが、一応生きてます。 このブログも去年のアドカレ以来の更新になります...放置しすぎた。

今年はロボコンやら未踏やらなんやらかんやらで、てんてこ舞いになり自作OS界隈から離れてしまっていたため自作OSとはあまり関係のない内容になります。ご了承ください。

ちなみに、未踏ではプログラムを暗号化された状態で実行するプラットフォームを3人のチームで開発してます。 専用のISA,CPU,コンパイラも自作してるので、低レイヤネタとしてプロジェクト終了後にまた記事にしたいと考えています。

Zen言語とは

Zen言語は京都にある株式会社コネクトフリーで開発されているプログラミング言語です。 組み込みなどのベアメタル環境においてもメモリー安全かつ確実なエラーハンドリング、HashMapなどの一般的なコレクション(Zen言語ではcontainer)群が提供されています。 zen-lang.org

僕も京都の大学に通学してる都合上、 何度か会社の方にお邪魔させてもらったことがありました。 そういったつながりでZen言語を使ってみたいと思っていたので、Zen言語でプロトコルスタック(一部)を書いてみました。 (インターン等には行ってないのでZen言語に関してはど素人です)

今回作ったもの

TAPデバイス経由で流れてきたパケットを処理してpingに応答するソフトウェアです。 f:id:PiBVT:20191208012451p:plain

github.com

本当は level-ip のようにソケットライクなAPIを実装してガンガンTCPパケット流したかったのですが、いかんせん時間がなかったので断念しました。無念。 github.com

※注: ベアメタル開発はペリフェラルドライバ開発にかなり時間を使うため、今回はLinux上でのシステムプログラミングをしてみました。 おそらく、この分野はZen言語ではあまり想定されていない(主眼を置いていない)と思われるので、 ベアメタル開発とは開発の勝手がかなり違う可能性があります。

できること

L2パケットを処理してpingに対して応答する。 f:id:PiBVT:20191208012451p:plain

以上です。

全体の構成

プロトコルとしてはEthernet,ARP,IPv4,ICMPv4で、pingに対して応答できる最低限の実装になってます。

色々とひどいのでソースコードは全く参考にならないと思います。

TAPデバイス

TAPはL2デバイスをエミュレートした仮想デバイスです。(TUNはL3) OpenVPNとかでよく用いられているアレです。

TAPを使うことで仮想NICを作成したことになるので、今回は仮想NICにつながる先の部分を書いたことになります。

似たようなことをするときにRaw Socketも使えるのですが、これは既存のNICに生のEthernetフレームを流すものなのでTAPデバイスとは全く異なるものです。

ちなみに、Raw Socketを作成するのにはroot権限が必要です。一方でpingはRaw Socketでパケットを流していますが、root権限なしでも利用出来ます。 この仕組みについては、Linux Capability を調べると分かります。(常識?)

TAPデバイスの初期化部分はLinuxシステムプログラミングの領域なので、C言語と相性の良いZen言語なら楽にかけます(もちろんlibcはリンクしてます)

pub fn create_netdev() NetDevError!std.os.fd_t {
    const sock = try std.os.openC(c"/dev/net/tun", std.os.O_RDWR, 0);
    errdefer {
        std.os.close(sock);
        std.debug.warn("Failed to open /dev/net/tun\n");
    }

    const ifr_addr = tun.ifreq_addr{
        .ifr_flags = tun.IFF_TAP | tun.IFF_NO_PI,
    };

    const zero = ([1]u8{0}) ** 12;

    var ifr = tun.ifreq{
        .ifr_name = "test" ++ zero,
        .addr = ifr_addr,
    };

    const err = tun.ioctl(sock, tun.get_tunsetiff(), &ifr);
    if (err < 0) {
        std.debug.warn("{}", err);
        return NetDeviceError.FailedToCreateTUN;
    }
    std.debug.warn("Successfully Created TUN Device:{}\n", ifr.ifr_name);
    _ = tun.system(c"ip a add 10.0.0.2/24 dev test");
    _ = tun.system(c"ip link set test up");
    return sock;
}

こんな感じになります。TUNSETIFFマクロだけはZen言語単体では無理だったので、Cから呼び出してます。 TUNデバイスを作成したらIPv4アドレスを割り当ててリンクを上げます。(エラー処理してませんが。) これでTUNデバイスを使えるようになったので、あとはwrite,readでパケット投げ放題です。

TUNSETIFFマクロも追いかけてみると結構楽しかったのですが、話が脱線するので割愛します。

ARP(arp.zen)

RFC 826 を参考に実装しました tools.ietf.org

pub const pkt_hdr = packed struct {
    hwtype: u16,
    protype: u16,
    hwsize: u8,
    prosize: u8,
    op: u16,
    sha: u48,
    spa: u32,
    tha: u48,
    tpa: u32,
};

今回はpingに反応するだけなので、ARP Request に対して ARP Reply で応答すると同時にARPテーブルを更新するだけの処理になっています。

本来はパケット送信時にARP Requestを投げたり、テーブルのキャッシュクリア等をする必要があるのですが、pingへの応答には必要なかったので実装しませんでした(バッサリ)

C言語ではsha,tha等はuint8_t[6]で実装するのですが、Zen言語では自由にビット幅を設定できるのでu48一発でいけます。 (むしろ[6]u8使うと謎にバグることが多くて辛かった)

IPv4(ipv4.zen)

RFC 791 を参考に実装しました tools.ietf.org

pub const pkt_hdr = packed struct {
    header_length: u4,
    version: u4,
    tos: u8,
    total_len: u16,
    identification: u16,
    frags: u16,
    ttl: u8,
    protocol: u8,
    chksum: u16,
    src_addr: u32,
    dst_addr: u32,
    payload: [*]u8,
};

単純にIPv4ヘッダーを読んでプロトコルを判断するだけです。 フラグメント,デフォルトゲートウェイ等は一切実装していません。気にしたら負けです。

RFC上では、version, header_length となっているのですが、エンディアンの関係か、逆の順番でないと正常に動作しませんでした。 ココらへんをC言語で書こうとすると

struct header {
#if __BYTE_ORDER == __LITTLE_ENDIAN
    uint8_t len :4;
    uint8_t ver  :4;
#elif __BYTE_ORDER == __BIG_ENDIAN
    uint8_t ver  :4;
    uint8_t len :4;
#endif
} __attribute__((packed));

こんな感じで対応できていたので、Zen言語でも対応できると嬉しいです。

ICMP(icmp.zen)

RFC 792 を参考に実装しました tools.ietf.org

Echo Request に対して Echo Reply を返すだけです。

ちなみに、pingに応答するためにはペイロードを丸々コピーしないといけないようです。本当はどうなのか分かりませんが、とりあえず動いてるのいいでしょう。(おい)

Zen言語を触ってみて

良いところ

  • Cと相性がいい。何も考えずにリンクできる
  • ビルドシステム
  • テストフレームワーク
  • 自由なビット幅整数型
  • エラーが日本語

C言語と何も考えずに普通にリンクできるので、少しずつZenで書き直していくという戦略は有効に感じました。が、C言語と同じ書き方は出来ないことを理解しておく必要はあります。

ビルドシステムやテストフレームワークは、まだ十分に使ってないのでその力を引き出せてはいないのですが、C言語のときは毎回悩みの種だったので予め標準で備わっているのは非常に強い点です。

個人的には自由なビット幅整数型が便利に感じました。エンディアンには気をつける必要があるのですが、 そこさえ気をつければ今までuint8_t配列で処理していたものをまとめて固めることができるので有効に使っていきたいです。

エラーが日本語なだけで安心感が段違いです。(英語だと読まない人がいるので...)

微妙なところ

  • 配列とポインタの関係が分かりにくい
  • ドキュメントがところどころ不十分
  • Zenコンパイラがたまにクラッシュする
  • コミュニティ,相談場所が存在しない

配列とポインタの関係については、C言語ほど単純なものではないので概要を理解しておく必要があります。 プログラムを書いてる時間の大半を配列とポインタ周りのバグでこじらせてたので... 逆に言うと、配列とポインタの組み合わせはすべきではない。ということかもしれません(今気づいた)

ドキュメントの不十分さやZenコンパイラのクラッシュは、公開されて半年もしていないので当然のことなのですが.. 標準ライブラリの実装が参考になりました。 一般的に使われるようになるにはまだ時間がかかりそうですが、これからに期待です。

個人的に一番困ったのが、相談できる場所がないことです。 クラッシュ,バグ報告をしようとしても、それらの再現コードの貼付ける場所が存在しないため 現状では積極的に改善要求を出すことがつらい状況にあるように感じました。 せめてGitHubリポジトリが存在すれば、再現コードを貼ってissueを投げることが出来たのですが....

まとめ

パケットの処理系をZen言語で書いてみた一番の印象は、Zen言語はC言語との相性はいいけれど、C言語では決してないと感じました。

Zen言語ではベアメタル開発においても容易で安全なプログラミング環境を提供する方針で言語仕様自体やライブラリが整備されており、 遊びでC言語を書くノリで書こうとすると安全でないコードになるので、コンパイラが自動的に弾いたり実行時にエラーを吐いてくれます。 慣れないうちはモヤモヤするのですが、慣れれば快適に書けるようになると思います。

僕自身は15時間ほどしか書いてないので全く慣れてないのですが、今後も少しずつ書いていきたいとは思っています。

蛇足・独り言

本当はQCOW2フォーマットのディスクをブロックデバイスとして直に認識させるLinuxカーネルモジュールを書くつもりだったのですが、時間の都合上断念しました。 nbdを利用したものとのパフォーマンスの違いを計測したかったんですよね...

パケット処理系ネタはもともとは2018年Seccamp後にNICデバドラを実装してガバガバTCP処理系を実装したのが最初でした。 その後、今年の3月にはSTM32F767上にデバドラからUDP処理系までを書いてみたりしていたのですが、それ以降は全くやっていなかったので今回ネタとして取り上げてみました。 いい加減ガッツリ、バッファリング周りから全部設計して常用できるTCP/IPスタックを書きたいところではあります。

ロボコンではそこらへんの担当(マイコンの通信プログラム,Ethernet,PCの管理)をしてたのですが、まさか世界大会に行くことになるとは。人生何があるか分からんもんです。