165行で実装するProtocol Buffersデコーダ(ミニマム版)
この記事は Go2 Advent Calendar 2018の11日目の記事です。
今年の後半くらいに Protocol Buffers の仕様を読み始めたら、とてもシンプルかつコンパクトな仕様なのにcompatibilityへの考慮が凄まじくて、2018年後半に書いた記事の大半がProtocol Buffersに関するものでした。 仕様とバイナリを睨めっこしていたら、自分でもバイナリをデコードしたくなったので、実装してみました。
本内容は、あくまでProtocol Buffersの勉強を目的としたもので、仕様には完璧に添っていません。 というか、わかりやすさ(と実装のしやすさ)を優先して、コンパクトな仕様のさらにミニマム版な内容となっています。 当然ですが、実運用する際にはofficialの実装を利用してください。
どこまで実装するか
上述の通り、ミニマム版として、以下を実装範囲とします。
- バイナリからGoの構造体へ
Unmarshal
する Unmarshal
する構造体は既知とする- wire typeは、値によって長さが変化する wire type 0(
Varint
)とwire type 2(Length-delimited
)を対象とする
Length-delimited
やVarint
は後ほど説明するので、今は一旦飛ばしてOKです。
3.について、後述のLexer
は、他のwire typeにも適用できるように設計してるので、長さが固定なことに注意すれば実装可能です。Future Workです。
ミニマム版ですが、Protocol Buffersでも最初にハマるVarint
も含めているので、Protocol Buffersよくわからないという方にも、お伝えできることがあるんじゃないかと思います。
Varint
の仕様がわかっていると値は127
以下にするのが望ましい(バイナリが短くなる)とかも理解できるのうれしいです。
バイナリの生成
まずはofficalのエンコーダでバイナリを生成していきましょう。 正しく生成されたバイナリをデコードすることで、もとの構造体が復元できることを目標にします。
まず、すべての大元になる.proto
ファイルを以下のように定義します。
Person
型にName
とAge
のフィールドをもたせています。
wire type 2(Length-delimited
)を表現したかったので、string
とint32
のままでよさそうなName
とAge
をwrapしています。
(Name
をFirstName
とLastName
とかにしたくなるかもしれないし......)
syntax = "proto3"; package person; message Person { Name name = 1; Age age = 2; } message Name { string value = 1; } message Age { int32 value = 1; }
protoc
して、上記.proto
ファイルに対応したライブラリ.pb.go
をgenerateします。
❯ protoc -I=./ --go_out=./ person.proto
以下表の値を設定したバイナリを生成します。
フィールド | 値 |
---|---|
Name | Alice |
Age | 20 |
package main import ( "io/ioutil" "log" pb "github.com/cipepser/protobufDecoder/Person" "github.com/golang/protobuf/proto" ) func main() { p := &pb.Person{ Name: &pb.Name{ Value: "Alice", }, Age: &pb.Age{ Value: 20, }, } if err := write("./person/alice.bin", p); err != nil { log.Fatal(err) } } func write(file string, p *pb.Person) error { out, err := proto.Marshal(p) if err != nil { return err } if err := ioutil.WriteFile(file, out, 0644); err != nil { return err } return nil }
生成したperson/alice.bin
を適当なバイナリエディタで見てみると以下のようになります。
なお、vimなら:%!xxd
で見れます。
hexdump person/alice.bin
をターミナル上で実行するのでもよいと思います。
0a07 0a05 416c 6963 6512 0208 14 ....Alice....
この時点で、バイナリではあるものの暗号化されているわけではない(Alice
が見えている)ので、ちょっと安心感を覚えます。
Protocol Buffersのバイナリの読み方
さて、実装を始める前にProtocol Buffersのバイナリがどのようなフォーマットなのか説明します。
Protocol Buffers のエンコーディング仕様の解説でも述べられているように以下が基本です。
key = タグナンバー * 8 + タイプ値
タグナンバーは.proto
で定義した値です。
例えば、今回のAge age = 2;
であれば、2
がタグナンバーです。
タイプ値は、メッセージタイプを表す値で、
公式ドキュメントにwire typesとして、以下のように定義されています。
冒頭からLength-delimited
やVarint
と言っていたやつです。
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 |
これだけだとよくわからないと思うので、例として、上で生成したバイナリをデコードしていきましょう。
改めて生成したバイナリを記載します。
0a 07 0a 05 41 6c 69 63 65 12 02 08 14
わかりやすいように色分けしました。
一つずつ読んでいきます。
まず初めの0x0a
は、
0d10
= タグname(1)
* 8 + Length-delimited(type 2)
であることからタグナンバーとタイプ値がわかります。
※Name
は自身で定義したmessageなので、表中のembedded message
が該当し、Length-delimited
となります。
Length-delimited(type 2)
だったので、lengthを得るために続く0x07
を読みます。これがタグname(1)
のlengthとなるので、後続の7バイト0a 05 41 6c 69 63 65
をName
としてデコードします。
Name
(赤色の7バイト)の初め0x0a
は、
0d10
= タグvalue(1)
* 8 + Length-delimited(type 2)
です。
Length-delimited(type 2)
だったので、lengthを得るために続く0x05
を読みます。これがタグvalue(1)
のlengthとなるので、後続の5バイト41 6c 69 63 65
をstring
として読んでいきます。
この5バイトをASCIIとして読むと41 6c 69 63 65
はAlice
となります。
いい感じです。この調子で残りの12 02 08 14
も読んでいきましょう。
0x12
は、
0d18
= タグage(2)
* 8 + Length-delimited(type 2)
です。
またまた、Length-delimited(type 2)
だったので、lengthを得るために続く0x02
を読みます。これがタグage(2)
のlengthとなるので、後続の2バイト08 14
をAge
としてデコードします。
0x08
は、
0d08
= タグvalue(1)
* 8 + Varint(type 0)
です。
やっとVarint(type 0)
が登場しましたね。
Varint(type 0)
はちょっとトリッキーなので、このあとすぐ説明します。
値が128
未満であれば、そのままデコードしてあげることができるので、
今回の例では0x14
をint32
として読んで0x14
= 0d20
が得られます。
以上から、もともと定義したAlice
と20
をデコードできました。
フィールド | 値 |
---|---|
Name | Alice |
Age | 20 |
Varintの仕様について
Varint(type 0)
について、もう少し詳しく見ていきます。
値が128
未満であれば、そのままデコードできると述べたので、128
以上の値として131
としてみましょう。(128
だと1
が立つbitが一つしかないのでもう少しわかりやすく131
にしました)
上記例で最後にVarint
として読み込んだ08 14
(緑色の箇所)を思い出しましょう。
0x08
= 0d08
= タグvalue(1)
* 8 + Varint(type 0)
からVarint(type 0)
であることがわかり、
0x14
をint32
としてデコードし、0d20
を得ました。
.proto
のAge: 20,
をAge: 131,
に変更してバイナリを生成し直してみます。
コードは上述と同じなので省略しますが、実行して得られるバイナリは、08 83 01
となります。
先頭1バイトは変わらず0x08
なので、value(tag 1)
,Varint(type 0)
となるので83
をVarintとして読み込みます。
ここで0x83
を2進数で表記すると0b1000 0011
です。
Varint
では、先頭1bitが1
のとき、次の1バイトに数値が続いていることを表します。
次の1バイトを読み込むと0x01
= 0b0000 0001
となり、先頭1bitが0
となったため、読み込みはここで終了です。
あとは0x83
と0x01
を組み合わせてint32
にデコードしてあげればいいのですが、仕様に以下のように書かれているので、リトルエンディアンで読んでいく必要があります。
varints store numbers with the least significant group first
また、先頭1bitは無視することにも注意です。
Varint
の値
= 0x01
(0b0000 0001
)から先頭1bit落としたもの ++ 0x83
(0b1000 0011
)から先頭1bit落としたもの // リトルエンディアンなので0x01
が先
= 000 0001
++ 000 0011
= 0b1000 0011
// 先頭の6bitは0なので省略
= 0d131
以上より、131
にデコードできます。
※++
演算子はバイナリを結合する操作を表します。
実装
バイナリの読み方がわかったところで実装に入っていきます。
Lexer
バイナリを読む部分を実装します。 ちょうど最近、Go言語でつくるインタプリタを読んでいるので、こちらの実装を大きく参考にさせて頂いています。
まず、Lexer
を以下のように定義します。バイナリのバイト列b
を1バイトずつ読んでいくため、
現在の位置position
と次のバイトの位置readPosition
を保持します。
type Lexer struct { b []byte position int readPosition int }
コンストラクタはバイト列input
を受け取って*Lexer
を返します。
func New(input []byte) *Lexer { l := &Lexer{b: input} l.readPosition = l.position + 1 return l }
以下は補助的な役割を果たすメソッドですが、実装しておくと便利です。
バイト列がまだ読み込めるかをhasNext()
で判断します。
また、next()
で1バイト先に進められるようにします。
func (l *Lexer) hasNext() bool { return l.readPosition < len(l.b) } func (l *Lexer) next() { l.position++ l.readPosition = l.position + 1 }
readCurByte()
は現在の位置の1バイトを読み込み、位置を1つ進めます。
Varint
を読み込む際はこちらを使います。
func (l *Lexer) readCurByte() byte { b := l.b[l.position] l.next() return b }
readBytes(n int)
はreadCurByte()
のn文字版です。
Length-delimited
を読み込む際に利用します。
今回は省略しましたが、hasNext()
でEOF
の判定を入れたほうがいいですね。
func (l *Lexer) readBytes(n int) []byte { bs := l.b[l.position : l.position+n] for i := 0; i < n; i++ { l.next() } return bs }
Varintのデコード
Varint
のデコーダを実装します。
readCurByte()
で1バイト読んできて、先頭1bitが1
である限り(0
になるまで)、1バイトずつ読み込みます。
先頭1bitは数値としてデコードする際には不要なので、0x7f
とANDで論理積を取ります。
また、リトルエンディアンとしてデコードする必要があるので、スタックに積んでおきます。
スタックといいつつ、Goのcontainer/list
はあんまり使われている印象がない(おそらくsliceのほうが速い?)ので、
bs
の[]byte
にappend
することにしました。
あとは先頭1bitを取り除いて、++
で結合してあげればよいので、7bitほど左シフトさせて足しこんでいきます。
func (l *Lexer) decodeVarint() (uint64, error) { if len(l.b) == l.position { return 0, errors.New("unexpected EOF") } var bs []byte b := l.readCurByte() for bits.LeadingZeros8(b) == 0 { // 最上位bitが1のとき bs = append(bs, b&0x7f) b = l.readCurByte() } // 最上位bitが0のとき = 最後の1byte x := uint64(b) for i := 0; i < len(bs); i++ { x = x<<7 + uint64(bs[len(bs)-1-i]) } return x, nil }
Unmarshalの実装
Person
、Name
、Age
それぞれについて、Unmarshal
の実装します。
どの型も流れは共通で、以下の流れになります。
- 1バイト読み込み、タグナンバー
tag
とタイプ値wire
を計算する wire
ごとに後続の何バイト読み込むかがわかるので、その数だけ読み込む- 読み込んだ値を
tag
に応じて評価する
1.のtag
とwire
の計算は、
key = タグナンバー * 8 + タイプ値
であることを思い出すと、以下のように実装できます。
tag := key >> 3 // 下位4bit目以上を抜き出す wire := int(key) & 7 // 下位3bitのみ抜き出す
tag
を0
にしたとき
コラム的な話になりますが、tag
を0
になるようなバイナリを与えると以下のようにpanic
します。
panic: proto: person.Person: illegal tag 0 (wire type 2)
逆に0
でないtag
ではpanic
(どころかエラーにもならない)になりません。
今回の例でいうとPerson
型はtag
が1
、2
しか定義していませんが、tagが3
になるようなバイナリを読み込ませてもエラーにはなりません。
これはcompatibilityを考慮してのことで、tag
が3
となるフィールドが増えた際に、
tag
が2
までしかない古い.proto
しか知らないクライアントでもエラーが起きないようにするためだと思われます。
今回の例(tag
が1
と2
しかない状態)で、tag
が3
になるようにバイナリを作り、デコードすると、そのフィールドはnil
になります。
panic
させる動作については、table_unmarshal.go
に以下のように書かれています。
Explicitly disallow tag 0. This will ensure we flag an error when decoding a buffer of all zeros. Without this code, we would decode and skip an all-zero buffer of even length. [0 0] is [tag=0/wiretype=varint varint-encoded-0].
PersonのUnmarshal
今回の.proto
で定義したPerson
は以下のような構造体となります。
type Person struct { Name *Name // tag: 1 Age *Age // tag: 2 }
このPerson
型に対してUnmarshal
を実装します。
今回Person
型には、Length-delimited(type 2)
になるフィールドしかないため、wire
が意味を持つのはcase 2
のときのみです。
tag
はName(1)
とAge(2)
があるので、場合分けします。
func (p *Person) Unmarshal(b []byte) error { l := New(b) for l.hasNext() { key := uint64(l.readCurByte()) tag := key >> 3 wire := int(key) & 7 switch wire { case 2: length := int(l.readCurByte()) v := l.readBytes(length) switch tag { case 0: return errors.New("illegal tag 0") case 1: p.Name = &Name{} if err := p.Name.Unmarshal(v); err != nil { return err } case 2: p.Age = &Age{} if err := p.Age.Unmarshal(v); err != nil { return err } } default: // Person型はwire type 2以外は存在しない return fmt.Errorf("unexpected wire type: %d", wire) } } return nil }
NameのUnmarshal
Person
と同じように.proto
で定義したName
は以下のような構造体となります。
type Name struct { Value string // tag: 1 }
このName
型に対してUnmarshal
を実装します。
今回Name
型には、Length-delimited(type 2)
になるフィールドしかないため、wire
が意味を持つのはPerson
と同じくcase 2
のときのみです。
tag
はValue(1)
のみなので、読み込んだバイト列をstring
としてデコードします。
func (n *Name) Unmarshal(b []byte) error { l := New(b) for l.hasNext() { key := uint64(l.readCurByte()) tag := key >> 3 wire := int(key) & 7 switch wire { case 2: length := int(l.readCurByte()) v := l.readBytes(length) switch tag { case 0: return errors.New("illegal tag 0") case 1: n.Value = string(v) } default: // Name型はwire type 2以外は存在しない return fmt.Errorf("unexpected wire type: %d", wire) } } return nil }
AgeのUnmarshal
Person
、Name
と同じように.proto
で定義したAge
は以下のような構造体となります。
type Age struct { Value int32 // tag: 1 }
このAge
型に対してUnmarshal
を実装します。
今回Age
型には、Varint(type 0)
になるフィールドしかないため、wire
が意味を持つのはcase 0
のときのみです。
tag
はValue(1)
のみなので、読み込んだバイト列をdecodeVarint()
でint32
としてデコードします。
func (a *Age) Unmarshal(b []byte) error { l := New(b) for l.hasNext() { key := uint64(l.readCurByte()) tag := key >> 3 wire := int(key) & 7 switch wire { case 0: switch tag { case 0: return errors.New("illegal tag 0") case 1: i, err := l.decodeVarint() if err != nil { return err } a.Value = int32(i) } default: // Age型はwire type 1以外は存在しない return fmt.Errorf("unexpected wire type: %d", wire) } } return nil }
以上でデコーダ本体の実装は完了です。お疲れ様でした。
テスト
最後にテストを書いていきます。
今回の例で使ったバイナリ、ゼロ値(nil
)になるパターン、マルチバイトになるVarint
をテストパターンとします。
package decoder import ( "encoding/hex" "testing" "github.com/google/go-cmp/cmp" ) func atob(s string) []byte { b, _ := hex.DecodeString(s) return b } func TestUnmarshalPerson(t *testing.T) { tests := []struct { b []byte expect Person }{ { // 今回の例 b: atob("0a070a05416c69636512020814"), expect: Person{ Name: &Name{Value: "Alice"}, Age: &Age{Value: 20}, }, }, { // ゼロ値 b: atob(""), expect: Person{}, }, { // Ageのみゼロ値 b: atob("0a070a05416c696365"), expect: Person{ Name: &Name{Value: "Alice"}, }, }, { // Nameのみゼロ値 b: atob("12020814"), expect: Person{ Age: &Age{Value: 20}, }, }, { // Varintが2バイトになる場合 b: atob("1203088301"), expect: Person{ Age: &Age{Value: 131}, }, }, { // Varintが3バイトになる場合 b: atob("120408928002"), expect: Person{ Age: &Age{Value: 32786}, }, }, } for i, tt := range tests { p := Person{} if err := p.Unmarshal(tt.b); err != nil { t.Fatalf("test[%d - failed to Unmarshal. got err:%q", i, err) } if diff := cmp.Diff(p, tt.expect); diff != "" { t.Fatalf("test[%d - failed to Unmarshal. expected=%q, got=%q", i, tt.expect, p) } } }
ちゃんとテストが通ります。
❯ go test ./decoder
ok github.com/cipepser/protobufDecoder/decoder 0.006s
最後に
というわけで、Protocol Buffersのバイナリをデコードしてみました。 全体を表示しても以下のように165行程度なのでだいぶコンパクトに実装できたと思います。
今回はPerson
、Name
、Age
とそれぞれUnmarshal
を実装しましたが、同じような処理になるのでコード生成したいですね。
あと.proto
からバイナリを生成するMarshal
のほうとかも実装していきたいです。
ご指摘等あればtwitterまでご連絡ください。
References
- Protocol Buffers
- Protocol Buffers - Encoding
- Protocol Buffers - Language Guide (proto3)
- Protocol Buffers - Google's data interchange format
- Protocol Buffers のエンコーディング仕様の解説
- Go support for Protocol Buffers - Google's data interchange format
- Proto3 Language Guide(和訳)
- The Go Programming Language Specification
- 作者: Thorsten Ball,設樂洋爾
- 出版社/メーカー: オライリージャパン
- 発売日: 2018/06/16
- メディア: 単行本(ソフトカバー)
- この商品を含むブログを見る
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"
アーキテクチャ
今回実装するパケットキャプチャのアーキテクチャは以下です。
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
を共有できるようArc
とMutex
で包みます。
#[derive(Clone, Debug)] struct Queue<T: Send> { inner: Arc<Mutex<VecDeque<T>>>, }
上記Queue
に対してnew
、get
、add
を実装します。
注意が必要なのはget
、add
を行う際に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
からパケットを取り出し、EthernetPacket
にmatch
した後、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_veth0
とRT2_veth1
がパケットキャプチャの対象インターフェースです。
pingを打ったときのパケットキャプチャ
host
の192.168.0.1
からRT1
の192.168.1.254
へpingを打ってみます。
# 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
host
の192.168.0.1
からRT1
の192.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
- [GAS]実行に失敗: その操作を実行する権限がありません。に悩んだこと
- [GAS] 実行する権限がありません。についての対策まとめ
- Manifests - Google Apps Script
- Authorization Scopes - Google Apps Script
-
必要な箇所のみ記載して、前後は省略しています。↩
clasp runがローカルで実行されない
背景
Google Apps Scriptをローカルで開発するために、
Google製のCLIツールclaspを導入しました。
Google Drive上のプロジェクトとclasp push
やclasp pull
で同期できてとても便利なのですが、
以下事象でハマったので記事に残します。
事象
- ローカルでコードを編集
clasp push
するclasp run <function name>
すると、2.でpush
したコードが実行されない
切り分けをしたところ
- ブラウザ上のプロジェクトで見ると
公開 - 実行可能APIとして導入...
が2.で上げたバージョンになっていない
が原因のようでした。
解決方法
--dev
オプションをつければローカルで実行されます。
❯ clasp run --dev <function name>
References
protobufのNon-varintの互換性について
以前、Varint
を64bitから32bitに変更したときの挙動を検証しました。
上記記事の通りVarint
は、下位32-bitを残して切り詰める結果でした。
今回は、Non-varint
の互換性を検証します。
例によって、Language Guideを見てみると
fixed32 is compatible with sfixed32, and fixed64 with sfixed64.
と述べられています。
fixed32
とsfixed32
、fixed64
とsfixed64
のように同じ長さであれば、互換性があるようです。
実際、Protocol Buffers - Encodingのwire 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; }
id
をfixed32
、fixed64
、sfixed32
、sfixed64
の4パターンで変化させて、検証します。
書き出し
書き出し用のコードは以下の通りです。
Id
のvalueは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
の部分は、fixed32
、fixed64
、sfixed32
、sfixed64
ごとに別ファイルとして出力します。
読み込み
読み込み用のコードです。
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 }
読み込みの前に、.proto
のid
をfixed32
、fixed64
、sfixed32
、sfixed64
で変化させて、protoc -I=fixed/ --go_out=fixed/ fixed.proto
を実行しています。
結果
書き出し、読み込みを全パターンで行った結果は以下の通りです。
※表のレイアウトが崩れるため、4294967295(math.MaxUint32)
と18446744073709551615(math.MaxUint64)
をMaxUint32
とMaxUint64
で略記します。
正しく読み込めている(○
)のは、書き出し/読み込みで同じ.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で長さが異なるもののみです。
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
列の先頭の0d
や09
でkeyのfield_number
とwire_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
)です。
まとめ
以上から、×
となった32bitと64bitは0d
と09
でそもそものwire_type
が異なり、パースに失敗し、defalut valueである0
となったようです。
△
となった箇所については、wire_type
が同じ(fixed32
とsfixed32
は0d
、fixed64
とsfixed64
は09
)ため、後続のバイナリをvalueとしてパースします。
パースする際に.proto
が異なるため、書き込んだ値と違う値で解釈されてしまったわけです。
例えば、fixed32
→sfixed32
では、
fixed32
としてMaxUint32(4294967295)
を0xff ff ff ff
としてエンコードします。
しかし、sfixed32
で0xff ff ff ff
をパースすると-1
として解釈されてしまいます。
パースに際してエラーが起きるわけではない2ので、正しく解釈できる範囲でcompatibleです。
上記の例(32bit)では、0x00 00 00 00
〜0x07 ff ff ff
です。
これを超えると、正数と2の補数で表現された負数が区別できないため、signed/unsignedで結果が異なります。
References
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{}
を使います。
in
をUser
にパースし、各フィールドを標準出力して、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.Contact
はnil
がdefault valueのため、Phone
やEmail
にアクセスしようとすると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.toml
のdependencies
にprotobuf
を記載します。
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()); }
実行すると、以下のようにName
とAge
が読み取れていることがわかります。
/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