Rustで低レベルのネットワークプログラムをつくる


Rustでパケットをキャプチャするプログラムを作りたいと思います。

Rustで低レベルのネットワークプログラミングをする場合pnetというライブラリーを使うようですが、
このライブラリーを呼び出すためにCargoというパッケージ管理ツールを使用します。
Rustのチュートリアルサイトによると、
CargoとはRustのビルドシステムでありパッケージマネージャーとされています。
またプロジェクトの管理もしてくれるようです。


プロジェクト作成


Cargoを使ってプロジェクトを作成します。
以下のようにしてコマンドを実行します。
これはpnet_sampleというプロジェクトを作成するということです。

cargo new --bin pnet_sample

Cargo.tomlを修正


pnetを使えるようにしましょう。
以下のように記述します

[package]
name = "pnet_sample"
version = "0.1.0"
authors = ["euniclus"]

[dependencies]
pnet = "0.22.0"

実装

ネットワークインターフェース取得

extern crate pnet;

use pnet::datalink::{self, NetworkInterface};

use std::env;

fn main() {
  // インターフェース名を引き数で取得
  let network_interface = env::args().nth(1).unwrap();

  // すべてのネットワークインターフェースを取得
  let interfaces = datalink::interfaces();

  // インターフェースを取得
  let interface = interfaces.into_iter().filter(|interface: &NetworkInterface| interface.name == network_interface).next().expect("Failed get Inteface");
  println!("Inteface:{}",interface.name);
}


マシンにあるインターフェースのうち、引数で指定したネットワークインターフェースと一致するものを取得します。
処理自体はコメントに書いたとおりです。

受信処理

extern crate pnet;

use pnet::datalink::{self, NetworkInterface};
use pnet::datalink::Channel::Ethernet;
use pnet::packet::ethernet::{EtherTypes, EthernetPacket};
use pnet::packet::ipv4::Ipv4Packet;
use pnet::packet::tcp::TcpPacket;
use pnet::packet::udp::UdpPacket;
use pnet::packet::ip::IpNextHeaderProtocols;
use pnet::packet::Packet;
use std::env;

fn capture_packet(packet: &EthernetPacket) {
  // イーサネットの上位のプロトコルを確認
  match packet.get_ethertype() {
    // Ipv4上のtcpまたはudpを表示
    EtherTypes::Ipv4 => {
      let ipv4 = Ipv4Packet::new(packet.payload());
      if let Some(ipv4) = ipv4 {
        match ipv4.get_next_level_protocol() {
          IpNextHeaderProtocols::Tcp => {
            let tcp = TcpPacket::new(ipv4.payload());
            if let Some(tcp) = tcp {
              println!("TCP {}:{} -> {}:{}", ipv4.get_source(), tcp.get_source(), ipv4.get_destination(), tcp.get_destination());
            }
          }
          IpNextHeaderProtocols::Udp => {
            let udp = UdpPacket::new(ipv4.payload());
            if let Some(udp) = udp {
              println!("UDP {}:{} -> {}:{}", ipv4.get_source(), udp.get_source(), ipv4.get_destination(), udp.get_destination());
            }
          }
          _ => println!("not tcp"),
        }
      }
    }
    _ => println!("not ipv4"),
  }
}

fn main() {
  // インターフェース名を引き数で取得
  let network_interface = env::args().nth(1).unwrap();

  // すべてのネットワークインターフェースを取得
  let interfaces = datalink::interfaces();

  // インターフェースを取得
  let interface = interfaces.into_iter().filter(|interface: &NetworkInterface| interface.name == network_interface).next().expect("Failed get Inteface");
  println!("Inteface:{}",interface.name);

  // 送受信に使うソケット的なものを取得
  let (mut tx, mut rx) = match datalink::channel(&interface, Default::default()) {
    Ok(Ethernet(tx, rx)) => (tx, rx),
    Ok(_) => panic!("not ethernet"),
    Err(e) => {
      panic!("error ocrrued {}", e);
    }
  };

  println!("Sniffing...");
  // ループの中でパケットを受信する
  loop {
    match rx.next() {
      Ok(packet) => {
        let packet = EthernetPacket::new(packet).unwrap();
        capture_packet(&packet);
      }
      Err(e) => {
        panic!("error occured {}",e);
      }
    }
  }
}


こちらが実際の受信処理です。
パケットを受信している部分はrx.next()です。
そしてmatch構文で正常に受信できたかを確認します。
受信できた場合capture_packet()にデータを渡し、パケットを解析しています。

これをビルドします。

cargo build


このプログラムはrawソケットを使います。
root権限が必要ですのでこのように実行します。

sudo ./target/debug/pnet_sample wlp2s0


引数のwlp2s0がインターフェース名です。
これは僕の場合の話ですので、人によって変わります。
インターフェース名はifconfigで確認できます。

最後に


こういう系のプログラムはc言語でしかつくったことがないのですが、
Rustだと割と短く簡単につくることができました。
今回はipアドレスとポート番号を表示するだけですが、
ここにいろいろ情報が載っていますので本格的なものも作れそうです。
https://docs.rs/pnet/0.12.0/pnet/packet/index.html