Rustでマルチインターフェースパケットキャプチャを実装する

この記事は Rust Advent Calendar 2018 の3日目の記事です。

本記事では、タイトルの通り、Rustでマルチインターフェースパケットキャプチャを実装します。 今回の記事で達成したい目標は以下2点です。

  • Rustでネットワークプログラミングをしたい
  • マルチインターフェースにすることでマルチスレッド対応したい

どうやって低レイヤを扱うか

Rustでネットワークプログラミングを行うには、libpnetが便利なので、今回はこちらを利用します。libpnetを使えるようCargo.tomlには以下を記載しておきます。

[dependencies]
serde = "1.0"
serde_derive = "1.0"

[dependencies.pnet]
version = "0.21.0"

アーキテクチャ

今回実装するパケットキャプチャのアーキテクチャは以下です。

f:id:cipepser:20181202012641p:plain

I/F1からI/Fnの各インターフェース用のスレッドがrxを監視します。 rxで受信したパケットをqueueに渡します。 packet handlerは、queueにあるパケットを一つずつ処理(標準出力に表示)する役割を果たします。 tcpdumpなどでは対象や表示形式など引数を設定できますが、今回はL2-4までを対象として、表示対象を以下の通りとします。ARPは遊び心で加えました。

Layer 表示対象
L2 送信元MACアドレス
L2 宛先MACアドレス
L2-3 ARP request
L2-3 ARP reply
L3 送信元IPアドレス
L3 宛先IPアドレス
L4 送信元ポート番号
L4 宛先ポート番号

実装

パケットの実装

各パケットが どのインターフェースから着信したものか を保持するために、queueに渡すパケットを以下の構造体にまとめます。

#[derive(Clone, Debug)]
struct PacketWithInterface {
    interface: NetworkInterface,
    packet: Vec<u8>,
}

Queueの実装

各インターフェースのrxを監視する部分をマルチスレッドで実装します。 各スレッドからqueueを共有できるようArcMutexで包みます。

#[derive(Clone, Debug)]
struct Queue<T: Send> {
    inner: Arc<Mutex<VecDeque<T>>>,
}

上記Queueに対してnewgetaddを実装します。 注意が必要なのはgetaddを行う際にlockを取ってあげることくらいでしょうか。 なお、lockを取った際に_queueで束縛しており、_queueがスコープから外れたタイミングで自動的にunlockされます。

impl<T: Send> Queue<T> {
    fn new() -> Self {
        Self { inner: Arc::new(Mutex::new(VecDeque::new())) }
    }

    fn get(&self) -> Option<T> {
        let _queue = self.inner.lock();
        if let Ok(mut queue) = _queue {
            queue.pop_front()
        } else {
            None
        }
    }

    fn add(&self, obj: T) -> usize {
        let _queue = self.inner.lock();
        if let Ok(mut queue) = _queue {
            queue.push_back(obj);
            queue.len()
        } else {
            0
        }
    }
}

packet handlerの実装

queueからパケットを取り出す部分はmain内で実装するので、ここでは取り出したパケットの処理することに注力します。 基本的にはバイト列の先頭からL2 -> L3 -> L4と、各レイヤーのヘッダを確認し、該当するフィールドを表示する処理になります。 なお、ARPパケットはEthernetヘッダに記載されているEtherTypeで判断できます。

fn handle_ethernet_frame(interface: &NetworkInterface, ethernet: &EthernetPacket) {
    println!(
        "{}: {} > {}",
        interface.name,
        ethernet.get_source(),
        ethernet.get_destination(),
    );

    print!("  {}: ", ethernet.get_ethertype());
    match ethernet.get_ethertype() {
        EtherTypes::Arp => {
            let arp = arp::ArpPacket::new(ethernet.payload()).unwrap();
            match arp.get_operation() {
                arp::ArpOperations::Reply => {
                    println!(
                        "ARP reply({}): {} -> {}",
                        arp.get_sender_proto_addr(),
                        arp.get_sender_hw_addr(),
                        arp.get_target_hw_addr()
                    );
                }
                arp::ArpOperations::Request => {
                    println!(
                        "ARP request({}): {} -> {}",
                        arp.get_target_proto_addr(),
                        arp.get_sender_hw_addr(),
                        arp.get_target_hw_addr()
                    );
                }
                _ => (),
            }
        }
        EtherTypes::Ipv4 => {
            let ip = Ipv4Packet::new(ethernet.payload()).unwrap();
            println!("{} -> {}", ip.get_source(), ip.get_destination());
            handle_ip_packet(&interface, &ip)
        }
        _ => (),
    }
}

fn handle_ip_packet(interface: &NetworkInterface, ip: &Ipv4Packet) {
    print!("    {}: ", ip.get_next_level_protocol());
    match ip.get_next_level_protocol() {
        IpNextHeaderProtocols::Tcp => {
            let tcp = tcp::TcpPacket::new(ip.payload()).unwrap();
            handle_tcp_packet(&interface, &tcp);
        }
        IpNextHeaderProtocols::Udp => {
            let udp = udp::UdpPacket::new(ip.payload()).unwrap();
            handle_udp_packet(&interface, &udp);
        }
        _ => (),
    }
}

fn handle_tcp_packet(_interface: &NetworkInterface, tcp: &tcp::TcpPacket) {
    println!("{} -> {}", tcp.get_source(), tcp.get_destination());
}

fn handle_udp_packet(_interface: &NetworkInterface, udp: &udp::UdpPacket) {
    println!("{} -> {}", udp.get_source(), udp.get_destination());
}

mainの実装

インターフェースを初期化

キャプチャ対象とするインターフェースをここで指定します。 configファイルから読み取る実装もしてみましたが、今回は簡単のためmainにべた書きとしました。

let interface_names: HashSet<&str> = vec!["RT2_veth0", "RT2_veth1"]
    .into_iter()
    .collect();

let interfaces: Vec<NetworkInterface> = datalink::interfaces()
    .into_iter()
    .filter(|interface: &NetworkInterface| interface_names.contains(interface.name.as_str()))
    .collect();

パケットを受信する

各インターフェースのrxを取得して、ループでrxを監視します。そしてrxで受信したパケットをqueueに投げ込みます。
今回の実装でqueueに投げ込む部分が一番ハマりました。 rx.next()で受信できるパケットは&[u8]なので参照を渡そうとすると、spawnで作ったクロージャより長生きできず全然コンパイルが通りませんでした。

let mut handles: Vec<_> = interfaces.into_iter()
    .map(|interface| {
        let queue = queue.clone();
        thread::spawn(move || {
            let mut rx = datalink::channel(&interface, Default::default())
                .map(|chan| match chan {
                    Ethernet(_, rx) => rx,
                    _ => panic!("could not initialize datalink channel {:?}", interface.name),
                }).unwrap();

            loop {
                match rx.next() {
                    Ok(src) => {
                        queue.add(PacketWithInterface {
                            interface: interface.clone(),
                            packet: src.to_owned(),
                        });
                    }
                    Err(_) => {
                        continue;
                    }
                }
            }
        })
    }
    )
    .collect();

パケットを処理する

queueからパケットを取り出し、EthernetPacketmatchした後、handle_ethernet_frameに渡します。渡した後は上述のpacket handlerの責務です。 異常パケットを受信した場合はパケットをドロップし、無視します。異常パケット1つ受信しただけでpanicしてプロセスごと落ちない設計としました。

handles.push(thread::spawn(move || {
    loop {
        let queue = queue.clone();
        match queue.get() {
            Some(packet_with_interface) => {
                let _packet = packet_with_interface.packet.deref();
                match EthernetPacket::new(_packet) {
                    Some(packet) => {
                        handle_ethernet_frame(&packet_with_interface.interface, &packet);
                    },
                    _ => {
                        continue;
                    }
                }
            },
            _ => {
                continue;
            }
        }
    }
}));

実際に動かしてみる

環境

検証用の環境は以下の通りです。

$ uname -a
Linux vagrant-ubuntu-trusty-64 3.13.0-161-generic #211-Ubuntu SMP Wed Oct 3 14:52:35 UTC 2018 x86_64 x86_64 x86_64 GNU/Linux

ネットワーク構築

netnsで以下のネットワークを用意します。 構築スクリプトこちら

RT2の★マークの箇所RT2_veth0RT2_veth1がパケットキャプチャの対象インターフェースです。

f:id:cipepser:20181202011833p:plain

pingを打ったときのパケットキャプチャ

host192.168.0.1からRT1192.168.1.254pingを打ってみます。

# host
$ ping 192.168.1.254 -c 1
PING 192.168.1.254 (192.168.1.254) 56(84) bytes of data.
64 bytes from 192.168.1.254: icmp_seq=1 ttl=63 time=0.706 ms

--- 192.168.1.254 ping statistics ---
1 packets transmitted, 1 received, 0% packet loss, time 0ms
rtt min/avg/max/mdev = 0.706/0.706/0.706/0.000 ms

このときのパケットキャプチャは以下のようになりました。

# RT2
$ sudo ./target/debug/pcap
RT2_veth1: d6:c7:d0:99:8e:bc > ff:ff:ff:ff:ff:ff
  Arp: ARP request(192.168.1.254): d6:c7:d0:99:8e:bc -> 00:00:00:00:00:00
RT2_veth1: f6:63:73:99:cc:cd > d6:c7:d0:99:8e:bc
  Arp: ARP reply(192.168.1.254): f6:63:73:99:cc:cd -> d6:c7:d0:99:8e:bc
RT2_veth1: d6:c7:d0:99:8e:bc > f6:63:73:99:cc:cd
  Ipv4: 192.168.0.1 -> 192.168.1.254
    Icmp: RT2_veth1: f6:63:73:99:cc:cd > d6:c7:d0:99:8e:bc
  Ipv4: 192.168.1.254 -> 192.168.0.1
    Icmp: RT2_veth1: f6:63:73:99:cc:cd > d6:c7:d0:99:8e:bc

最初はまったく通信がないので、ARPによってIPアドレスからMACアドレスの解決を行っています。 その後、192.168.0.1から192.168.1.254へのICMP通信が発生し、その応答が返ってきていることがわかります。

TCP通信を発生させたときのパケットキャプチャ

RT1で以下のように1234ポートでリッスンしておきます。

# RT1
$ nc -l -p 1234

host192.168.0.1からRT1192.168.1.254:1234へ接続します。

# host
$ nc 192.168.1.254 1234

このときのパケットキャプチャは以下のようになりました。

# RT2
RT2_veth1: d6:c7:d0:99:8e:bc > f6:63:73:99:cc:cd
  Ipv4: 192.168.0.1 -> 192.168.1.254
    Tcp: 45693 -> 1234
RT2_veth1: f6:63:73:99:cc:cd > d6:c7:d0:99:8e:bc
  Ipv4: 192.168.1.254 -> 192.168.0.1
    Tcp: 1234 -> 45693
RT2_veth1: d6:c7:d0:99:8e:bc > f6:63:73:99:cc:cd
  Ipv4: 192.168.0.1 -> 192.168.1.254
    Tcp: 45693 -> 1234

正しくキャプチャできていますね。 45693は送信元なのでhighポートがOSによって割り当てられています。 また、3つ目のパケットから、ncコマンドでリッスンしていると応答がないのでTCPの再送が行われていることがわかります。

最後に

以上、Rustによるマルチインターフェースパケットキャプチャでした。 本文中でも述べましたがlifetimeですごいハマりました。 少しずつRustを勉強していますが、まだまだですね。今回みたいになにか作っているといろいろハマって理解が深まるので前向きにがんばります。

ご指摘等あればtwitterまでご連絡ください。

References

【GAS】「SpreadsheetApp.create を呼び出す権限がありません」を解決する

表題の通りです。
タイトルのエラーメッセージでググったところ、トリガーを設定し、解決されている方が多いようです。 しかし、当方の要件としては、トリガーで定期的に実行させる必要はなく、エラーメッセージは権限の問題なのでスコープを設定したかったので、appsscript.jsonでスコープを設定する方法を残します。

余談ですが、GUIから設定できるのかと思ったので、探してみたのですが、 ファイル -> プロジェクトのプロパティ -> スコープタブでは、権限が確認できるのみで変更はできなかったので、appsscript.jsonに以下を追加しました1

{
  "oauthScopes": ["https://www.googleapis.com/auth/spreadsheets.readonly"],
}

なお、参照権限のみで十分であれば、https://www.googleapis.com/auth/spreadsheets.readonlyのようにreadonlyを付けましょう。

References


  1. 必要な箇所のみ記載して、前後は省略しています。

clasp runがローカルで実行されない

背景

Google Apps Scriptをローカルで開発するために、 Google製のCLIツールclaspを導入しました。
Google Drive上のプロジェクトとclasp pushclasp pullで同期できてとても便利なのですが、 以下事象でハマったので記事に残します。

事象

  1. ローカルでコードを編集
  2. clasp pushする
  3. clasp run <function name>すると、2.でpushしたコードが実行されない

切り分けをしたところ

  • ブラウザ上のプロジェクトで見ると公開 - 実行可能APIとして導入...が2.で上げたバージョンになっていない

が原因のようでした。

解決方法

--devオプションをつければローカルで実行されます。

❯ clasp run --dev <function name>

References

protobufのNon-varintの互換性について

以前、Varintを64bitから32bitに変更したときの挙動を検証しました。

cipepser.hatenablog.com

上記記事の通りVarintは、下位32-bitを残して切り詰める結果でした。 今回は、Non-varintの互換性を検証します。

例によって、Language Guideを見てみると

fixed32 is compatible with sfixed32, and fixed64 with sfixed64.

と述べられています。

fixed32sfixed32fixed64sfixed64のように同じ長さであれば、互換性があるようです。

実際、Protocol Buffers - Encodingwire typesで以下のように定義されており、32bitと64bitでそもそもTypeが異なります。

Type Meaning Used For
0 Varint int32, int64, uint32, uint64, sint32, sint64, bool, enum
1 64-bit fixed64, sfixed64, double
2 Length-delimited string, bytes, embedded messages, packed repeated fields
3 Start group groups (deprecated)
4 End group groups (deprecated)
5 32-bit fixed32, sfixed32, float

.proto

今回は以下の.protoを使います。

syntax = "proto3";
package fixed;

message User {
  fixed32 id = 1;
}

idfixed32fixed64sfixed32sfixed64の4パターンで変化させて、検証します。

書き出し

書き出し用のコードは以下の通りです。 Idvalueはbitがすべて1になる値で検証します(user.Idフィールドのコメントアウト参照)。

package main

import (
    "io/ioutil"
    "log"

    pb "github.com/cipepser/protobuf-sample/fixed"
    "github.com/golang/protobuf/proto"
    "fmt"
    // "math" // fixed32とfixed64のときのみ利用
)

func main() {
    user := &pb.User{
        //Id: math.MaxUint32, // fixed32のとき
        //Id: math.MaxUint64, // fixed64のとき
        Id: -1,  // sfixed32とsfixed64のとき
    }

    fmt.Printf("%x\n", user.Id)

    if err := write("./fixed/fixed32.bin", user); err != nil {
        log.Fatal(err)
    }
}

func write(file string, user *pb.User) error {
    out, err := proto.Marshal(user)
    if err != nil {
        return err
    }
    fmt.Printf("% x\n", out)
    if err := ioutil.WriteFile(file, out, 0644); err != nil {
        return err
    }
    return nil
}

出力ファイルfixed32.binの部分は、fixed32fixed64sfixed32sfixed64ごとに別ファイルとして出力します。

読み込み

読み込み用のコードです。

package main

import (
    "fmt"
    "io/ioutil"
    "log"

    pb "github.com/cipepser/protobuf-sample/fixed"
    "github.com/golang/protobuf/proto"
)

func main() {
    if err := read("./fixed/fixed32.bin"); err != nil {
        log.Fatal(err)
    }
    if err := read("./fixed/fixed64.bin"); err != nil {
        log.Fatal(err)
    }
    if err := read("./fixed/sfixed32.bin"); err != nil {
        log.Fatal(err)
    }
    if err := read("./fixed/sfixed64.bin"); err != nil {
        log.Fatal(err)
    }
}

func read(file string) error {
    in, err := ioutil.ReadFile(file)
    if err != nil {
        return err
    }
    user := &pb.User{}

    if err := proto.Unmarshal(in, user); err != nil {
        return err
    }
    fmt.Printf("%d\n", user.Id)
    return nil

}

読み込みの前に、.protoidfixed32fixed64sfixed32sfixed64で変化させて、protoc -I=fixed/ --go_out=fixed/ fixed.protoを実行しています。

結果

書き出し、読み込みを全パターンで行った結果は以下の通りです。
※表のレイアウトが崩れるため、4294967295(math.MaxUint32)18446744073709551615(math.MaxUint64)MaxUint32MaxUint64で略記します。

正しく読み込めている()のは、書き出し/読み込みで同じ.protoを使ったパターンのみでした。

.proto(write時) 値(write) .proto(read時) 値(read) 結果
fixed32 MaxUint32 fixed32 MaxUint32
fixed32 MaxUint32 fixed64 0 ×
fixed32 MaxUint32 sfixed32 -1
fixed32 MaxUint32 sfixed64 0 ×
fixed64 MaxUint64 fixed32 0 ×
fixed64 MaxUint64 fixed64 MaxUint64
fixed64 MaxUint64 sfixed32 0 ×
fixed64 MaxUint64 sfixed64 -1
sfixed32 -1 fixed32 MaxUint32
sfixed32 -1 fixed64 0 ×
sfixed32 -1 sfixed32 -1
sfixed32 -1 sfixed64 0 ×
sfixed64 -1 fixed32 0 ×
sfixed64 -1 fixed64 MaxUint64
sfixed64 -1 sfixed32 0 ×
sfixed64 -1 sfixed64 -1

上記結果を見ると、 パースそのものに失敗して、値が0となった(×)ものと、 値は読み込めているものの元の値と異なる()もの があることがわかります。

よくよく見てみると×は32bitと64bitで長さが異なるもののみです。

Language Guide

fixed32 is compatible with sfixed32, and fixed64 with sfixed64.

の通りですね。
しかし、同じ長さであればcompatibleなはずなのにで元の値と変わってしまったものもあります。
もう少し深掘りするために、書き出したバイナリを表に加えてみます。(binary列)

proto(write時) 値(write) binary proto(read時) 値(read) 結果
fixed32 MaxUint32 0d ff ff ff ff fixed32 MaxUint32
fixed32 MaxUint32 0d ff ff ff ff fixed64 0 ×(32bit vs 64bit)
fixed32 MaxUint32 0d ff ff ff ff sfixed32 -1
fixed32 MaxUint32 0d ff ff ff ff sfixed64 0 ×(32bit vs 64bit)
fixed64 math.MaxUint64 09 ff ff ff ff ff ff ff ff fixed32 0 ×(32bit vs 64bit)
fixed64 math.MaxUint64 09 ff ff ff ff ff ff ff ff fixed64 math.MaxUint64
fixed64 math.MaxUint64 09 ff ff ff ff ff ff ff ff sfixed32 0 ×(32bit vs 64bit)
fixed64 math.MaxUint64 09 ff ff ff ff ff ff ff ff sfixed64 -1
sfixed32 -1 0d ff ff ff ff fixed32 MaxUint32
sfixed32 -1 0d ff ff ff ff fixed64 0 ×(32bit vs 64bit)
sfixed32 -1 0d ff ff ff ff sfixed32 -1
sfixed32 -1 0d ff ff ff ff sfixed64 0 ×(32bit vs 64bit)
sfixed64 -1 09 ff ff ff ff ff ff ff ff fixed32 0 ×(32bit vs 64bit)
sfixed64 -1 09 ff ff ff ff ff ff ff ff fixed64 math.MaxUint64
sfixed64 -1 09 ff ff ff ff ff ff ff ff sfixed32 0 ×(32bit vs 64bit)
sfixed64 -1 09 ff ff ff ff ff ff ff ff sfixed64 -1

まず、追加したbinary列の先頭の0d09でkeyのfield_numberwire_typeがわかります。
Protocol Buffers - Encodingに書いてあるとおり、keyは、(field_number << 3) | wire_typeエンコードされます。

つまり、
0d(1 << 3) | 5なので、type5(32-bit)のフィールド1(Id)であり、
09(1 << 3) | 1なので、type5(64-bit)のフィールド1(Id)です。

その後に続くff...value1です。

まとめ

以上から、×となった32bitと64bitは0d09でそもそものwire_typeが異なり、パースに失敗し、defalut valueである0となったようです。

となった箇所については、wire_typeが同じ(fixed32sfixed320dfixed64sfixed6409)ため、後続のバイナリをvalueとしてパースします。 パースする際に.protoが異なるため、書き込んだ値と違う値で解釈されてしまったわけです。 例えば、fixed32sfixed32では、 fixed32としてMaxUint32(4294967295)0xff ff ff ffとしてエンコードします。 しかし、sfixed320xff ff ff ffをパースすると-1として解釈されてしまいます。

パースに際してエラーが起きるわけではない2ので、正しく解釈できる範囲でcompatibleです。 上記の例(32bit)では、0x00 00 00 000x07 ff ff ffです。 これを超えると、正数と2の補数で表現された負数が区別できないため、signed/unsignedで結果が異なります。

References


  1. 今回はffとなっているため区別しませんが、valueはホストバイトオーダーで書き込まれるようなので、リトルエンディアンになることが多いと思います。バイナリを読む際にはご注意ください。

  2. 処理系によって異なるかもしれません。今回検証したGoの範囲での結果です。

protobufで0 byteをデコードする

Language Guideに以下のようにフィールドに値がセットされていない場合はdefault valueが使われることが書かれています。

When a message is parsed, if the encoded message does not contain a particular singular element, the corresponding field in the parsed object is set to the default value for that field.

ということは何も書かれていない(0 byteの)バリナリをパースしようとすると、すべてのフィールドがdefault valueとなるはずなので試してみました。

パースする.proto

以下のような.protoを用意します。

syntax = "proto3";
package blank;

message User {
  int32 id = 1;
  string name = 2;
  fixed64 age = 3;
  Contact contact = 4;
}

message Contact {
  string phone = 1;
  string email = 2;
}

protocします。

❯ protoc -I=blank/ --go_out=blank/ blank.proto

パースする

0 byteの入力としてin := []byte{}を使います。 inUserにパースし、各フィールドを標準出力して、default valueになっているか確認します。

package main

import (
    "fmt"
    "log"

    pb "github.com/cipepser/protobuf-sample/blank"
    "github.com/golang/protobuf/proto"
)

func main() {
    user := &pb.User{}
    in := []byte{}

    if err := proto.Unmarshal(in, user); err != nil {
        log.Fatal(err)
    }

    fmt.Println("ID: ", user.Id)
    fmt.Println("Name: ", user.Name)
    fmt.Println("Age: ", user.Age)
    fmt.Println("Contact: ", user.Contact)
 }

結果

実行結果は以下の通りです。

❯ go run BlankRead.go
ID:  0
Name:
Age:  0
Contact:  <nil>

ちゃんとdefault valueでパースできました。
ちなみに、上記の通りuser.Contactnilがdefault valueのため、PhoneEmailにアクセスしようとするとpanicになります。

fmt.Println("Phone: ", user.Contact.Phone)
// panic: runtime error: invalid memory address or nil pointer dereference

References

Golangで出力したprotobufバイナリをRustで読み込む

ここ三回くらいprotobufの記事を書いてきましたが、Goばかりだったので、Rustで読み込んでみました。

事前準備:Goでバイナリを出力する

user.protoを以下のように定義します。

syntax = "proto3";
package user;

message User {
  string name = 1;
  int32 age = 2;
}

Go用にprotocする

❯ protoc -I=./ --go_out=./ user.proto

user.pb.goが生成されます。

バイナリを書き出す

以下のユーザをprotobufのバイナリとしてgo_user.binを書き出します。

フィールド
Name Alice
Age 20

出力用のコードは以下の通りです。

package main

import (
    "fmt"
    "io/ioutil"
    "log"

    pb "github.com/cipepser/protobuf-sample/rust-protobuf-example/user"
    "github.com/golang/protobuf/proto"
)

func main() {
    p := &pb.User{
        Name: "Alice",
        Age:  20,
    }

    out, err := proto.Marshal(p)
    if err != nil {
        log.Fatal(err)
    }
    fmt.Println(out)
    if err := ioutil.WriteFile("./go_user.bin", out, 0644); err != nil {
        log.Fatalln("Failed to write:", err)
    }
}

Rustで読み込む

いよいよ本題のRustです。

Cargo.tomlの設定

Rustでprotobufを扱うためにrust-protobufを使います。 Cargo.tomldependenciesprotobufを記載します。 READMEにも書いてありますが、bytesクレートを使っているのでbytesも記載します。

[dependencies]
protobuf = { version = "~2.0", features = ["with-bytes"] }

2019/08/03追記
stepancheg/rust-protobuf: Rust implementation of Google protocol buffersに、version2未満がサポート対象外になったと書いてあるので、追記現在では以下のように記載します。

[dependencies]
protobuf = { version = "2", features = ["with-bytes"] }

Rust用にprotocする

user.protoは、Goでprotocしたときと同じものです。

❯ protoc --rust_out src/ user.proto

以下のようなメッセージが出る場合は、protoc-gen-rustがないのでインストールしましょう。

protoc-gen-rust: program not found or is not executable
--rust_out: protoc-gen-rust: Plugin failed with status code 1

以下でインストール出来ます(Cargo.tomlに書いてもいいのかも)。

❯ cargo install protobuf-codegen

protocがうまくいけば、src/user.rsがgenerateされます。

読み込む

ファイル読み込み、User型にmerge、標準出力するまでのコードは以下のとおりです。

extern crate protobuf;

mod user;

use user::User;
use std::fs::File;
use std::io::{BufReader};
use protobuf::{CodedInputStream, Message};

fn main() {
    let file = File::open("./go_user.bin").expect("fail to open file");
    let mut buffered_reader = BufReader::new(file);
    let mut cis = CodedInputStream::from_buffered_reader(&mut buffered_reader);

    let mut u = User::new();
    u.merge_from(&mut cis).expect("fail to merge");

    println!("Name: {}", u.get_name());
    println!("Age: {}", u.get_age());
}

実行すると、以下のようにNameAgeが読み取れていることがわかります。

/usr/local/bin/cargo run --color=always --package rust-protobuf-example --bin rust-protobuf-example
    Finished dev [unoptimized + debuginfo] target(s) in 0.04s
     Running `target/debug/rust-protobuf-example`
Name: Alice
Age: 20

Process finished with exit code 0

References

protobufのVarintを64bitから32bitに変更したときの挙動

protobufのVarintProtocol Buffers - Encodingで定義されるように、32-bit(int32, uint32, sint32)や64-bit(int64, uint64, sint64)のどちらも含まれています。 Varintエンコーディングは、その値によってbyte長が変わりますが(負数は-1のような小さい数でも最上位bitが1になりエンコードした結果長くなってしまうので非推奨となっている)、64-bitでエンコードされた値を32-bitで読み込んだ場合、どのような動作になるのか気になったので試してみました。

なお、Varintには、boolenumも含まれていますが、 その変換については以下の記事で検証したのため、興味があればそちらをご覧ください。

cipepser.hatenablog.com

cipepser.hatenablog.com

結論から言うと仕様に、以下のように書いてあるので、処理系の実装依存と思われますが、Goで実際に試してみたメモとして残します。

If a number is parsed from the wire which doesn't fit in the corresponding type, you will get the same effect as if you had cast the number to that type in C++ (e.g. if a 64-bit number is read as an int32, it will be truncated to 32 bits).

環境

❯ go version
go version go1.10.3 darwin/amd64

❯ protoc --version
libprotoc 3.6.0

検証(uint64 -> uint32)

uint64math.MaxUint32よりも大きくなる値をuint32で読み込んでみて動作を検証します。
(最後にintも検証結果も記載しますが、コードも含めてすべて記載すると長いのでuintのみとします)

今回の例では、読み込む値をUserがもつIdとし、以下のように.protoを定義します。

syntax = "proto3";
package max;

message User {
  uint64 id = 1;
}

書き出し

次に書き出し用のコードです。 math.MaxUint32よりも大きくなる値を0b1111111111111111111111111111111100000000000000000000000000000000とします。 これは前半32-bitが1で後半32-bitが0となる値で、math.MaxUint64 - math.MaxUint32で生成します。この数字を使うことでuint32で読み込んだときに後半32-bitだけに切り詰められる(0になる)のか、math.MaxUint32より大きくオーバーフローしてしまうのか確認します。

// MaxWrite.go
package main

import (
    "io/ioutil"
    "log"
    "math"

    pb "github.com/cipepser/protobuf-sample/max"
    "github.com/golang/protobuf/proto"
)

func main() {
    user := &pb.User{
        Id: math.MaxUint64 - math.MaxUint32,
    }

    if err := write("./max/uint.bin", user); err != nil {
        log.Fatal(err)
    }
}

func write(file string, user *pb.User) error {
    out, err := proto.Marshal(user)
    if err != nil {
        return err
    }
    if err := ioutil.WriteFile(file, out, 0644); err != nil {
        return err
    }
    return nil
}

読み込み

読み込み用のコードです。

// MaxRead.go
package main

import (
    "fmt"
    "io/ioutil"
    "log"

    pb "github.com/cipepser/protobuf-sample/max"
    "github.com/golang/protobuf/proto"
)

func main() {
    if err := read("./max/uint.bin"); err != nil {
        log.Fatal(err)
    }
}

func read(file string) error {
    in, err := ioutil.ReadFile(file)
    if err != nil {
        return err
    }
    user := &pb.User{}

    if err := proto.Unmarshal(in, user); err != nil {
        return err
    }
    fmt.Printf("0d%d\n0b%b\n", user.Id, user.Id)
    return nil
}

10進数(0d)と2進数(0b)でuser.Idを標準出力します。 試しに実行してみると、以下のように正常にデコードできていることがわかります。

❯ go run MaxRead.go
0d18446744069414584320
0b1111111111111111111111111111111100000000000000000000000000000000

.protouint32に変更する

では、uint32に変更して、上記で出力したuint64のバイナリを読み込みましょう。

syntax = "proto3";
package max;

message User {
  uint32 id = 1;
}

protocしてから読み込みます。

go run MaxRead.go
0d0
0b0

0になったので、後半32-bitだけに切り詰められたようです。

結果

同様の検証をint64 -> int32sint64 -> sint32でもやってみた結果も含めて、まとめると以下のようになりました。

検証パターン(.proto) 読み込む値 64-bitのまま読み込み 32-bitにして読み込み
uint64 -> uint32 math.MaxUint64 - math.MaxUint32 0d18446744069414584320
0b1111111111111111111111111111111100000000000000000000000000000000
0d0
0b0
int64 -> int32 math.MaxInt64 - math.MaxInt32 - 1 <<31 0d9223372032559808512
0b111111111111111111111111111111100000000000000000000000000000000
0d0
0b0

結果、uintでもintでも同様に後半32-bitに切り詰められる結果となりました。

余談

intのほうで1 << 31を引いているのですが、最初、math.MaxInt64 - math.MaxInt32としていました。 符号付き整数なので、これだと下位31-bitしか0にできず、この値をint32として読み込むと以下のようになってしまいます。

0d-2147483648
0b-10000000000000000000000000000000

検証としては、下位32-bitを残して切り詰める結果から相違ないので、参考としてこの結果も載せておきます。

ちなみにmath.MinInt64 - math.MinInt32を読み書きしても上記と同じ結果になります(2進数で見たときに同じ値になるので)。

References