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