12
0

More than 1 year has passed since last update.

Rustでネットワークインターフェイス一覧を取得し、宛先IPから送信元IPを選択する

Last updated at Posted at 2022-12-04

この記事はフューチャーアドベントカレンダー4日目の記事です。

TCP/IPの通信において、ネットワークインターフェイス一覧を取得し、宛先IPアドレスが指定されたときに送信元IPアドレスを選択する処理のRust実装を紹介します。

車輪の再発明の過程でスポーク1本を開発したぐらいの内容ですが、読んでいただけますと幸いです。

背景

先日、小野輝也さんが著した「Rustで始めるTCP自作入門」を読みました。この本は、タイトルの通り、Rustを用いて簡易的なTCPを実装するという内容の本です。
普段アプリケーションを開発していて、下のレイヤーはOSのプロトコル・スタックに任せることがほとんどですが、自作していくことでTCPが提供する機能や、基本的な仕組みの理解を深められ、勉強になりました。

読んでいて、本とは別の実装をしてみようと考えた部分があったため、紹介します。

送信元IPの選択

この本の実装ではソケットを送信元IP, 宛先IP, 送信元ポート, 宛先ポートの組(4タプル)で識別しており、ソケット作成時に4タプルを設定して管理します。
しかし、TCPクライアントが4タプルを作成する段階では、送信元IPはクライアントが指定する宛先IPほど簡単には決められません。
デバイスは通常、複数のネットワークインターフェイスを持っており、IPも複数あります。宛先IPによって、送信元IPは変化します。
例えば、宛先が127.0.0.1:8080であれば、送信元IPは127.0.0.1になりますし、グローバルIP宛であればデフォルトゲートウェイが設定されているインターフェイスのIPが送信元IPになります。

通常アプリは送信元IPを意識する必要はありません。例えば、ソケット通信をする際、宛先だけを指定してconnectシステムコールすれば、カーネルがよしなに送信元IPが設定され、パケットが送信されます。
しかし、今回はアプリで宛先IPから送信元IPを選択します。

本の実装ではシェルコマンドip route get <dst_ip>を呼び出し、結果をパースして送信元IPを取得しています。
送信元IPを選択する処理はTCP提供機能とは関係がなく、内容を簡潔にするという観点からこの方法で全く問題ないと思います。

ただ、シェルコマンドを用いない場合はどのような実装になるのだろうと思い、実装してみました。

実行環境

本実装は以下の環境で実行しました。

  • OS: Ubuntu 22.04.1
    • ネイティブ動作しています
  • cargo: 1.65.0

本実装はLinuxのみサポートしており、他の環境では動作しません。

実装

ここにコードを掲載しております。

以下のサードパーティライブラリを使用しました。

ライブラリ バージョン 役割
anyhow 1.0 エラー管理crate
nix 0.26.1 Linuxなどの*nix系プラットフォームのAPIをsafetyに扱うためのcrate
libc提供機能を扱うcrateにlibc crate があり、こちらはユーザにunsafeな操作を求められることが多いが、nixはunsafeな操作を行わずに利用できる

また、簡潔な実装のため、以下の制約を設けています

  • IPv4のみ対応する
  • 一つのインターフェイスに複数のIPv4アドレスが割り当てられている場合を考慮しない

処理の流れ

ネットワークインターフェイス一覧が与えられている場合

デバイスが持つ全ネットワークインターフェイスについて、以下の情報が与えられているとします。

  • IPアドレス
  • サブネットマスク
  • デフォルトゲートウェイの有無

この場合、宛先IPから送信元インターフェイスを選択し、送信元IPを選択することは難しくありません。以下の手順で探せば良いです。

  1. IPアドレスとサブネットマスクからインターフェイスのネットワークアドレスを計算する
    1. ネットワークドレスはIPアドレスとサブネットマスクの論理積で計算できる
  2. ネットワークアドレスからインターフェイスが宛先ホストと同じネットワークに属していた場合、インターフェイスに割り当てられたIPを返す
  3. いずれのインターフェイスが属するネットワークに宛先ホストが属さない場合、デフォルトゲートウェイと同じネットワークに属しているインターフェイスの値を返す

上記処理を実装すると以下のようになります。

use std::env;
use std::fs::File;
use std::net::Ipv4Addr;
use std::io::{BufReader, BufRead};

use anyhow::{Context, Result, bail};
use nix::ifaddrs;

// 指定したIPアドレスにパケットを送る場合、送信元IPがどれになるかを選択するCLIアプリです
fn main() {
    let args: Vec<String> = env::args().collect();
    let dest_ip: Ipv4Addr = args[1].parse().unwrap();

    match get_source_addr_to(dest_ip) {
        Ok(i) => println!("{}", i),
        Err(e) => println!("{}",e),
    }
}

#[derive(Clone)]
struct NetworkInterface {
    name: String,
    address: Ipv4Addr,
    netmask: Ipv4Addr
}

// 宛先IPを引数にとり、送信元になるIPを返します
fn get_source_addr_to(dest_ip: Ipv4Addr) -> Result<Ipv4Addr>{
    let interfaces = get_network_interfaces()?;
    match interfaces.iter()
            .find(|i| is_reachable_address(i, dest_ip)) {
        Some(i) => Ok(i.address),
        None => match find_default_interface(&interfaces) {
            Some(i) => Ok(i.address),
            None => bail!("no interface can reach the destination"),
        },
    }
}

// 宛先IPと、インターフェイスが持つIPが同じネットワークに属しているかどうかを返します
fn is_reachable_address(interface: &NetworkInterface, dest_ip: Ipv4Addr) -> bool {
    let candidate_ip = u32::from_be_bytes(interface.address.octets());
    let netmask = u32::from_be_bytes(interface.netmask.octets());
    let target_ip = u32::from_be_bytes(dest_ip.octets());
    
    candidate_ip & netmask == target_ip & netmask
}

// デバイスが持つすべてのIPv4ネットワークインターフェイスを返します
fn get_network_interfaces() -> Result<Vec<NetworkInterface>> {
    todo!();
}

// デフォルトゲートウェイが設定されているインターフェイスを返します
fn find_default_interface(interfaces: &Vec<NetworkInterface>) -> Option<NetworkInterface> {
    todo!();
}

ネットワークインターフェイスやデフォルトゲートウェイの情報はOSが管理しています。
ということで、OSからネットワークインターフェイスとデフォルトゲートウェイの情報を取得できれば、目的は達成できます。

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

ネットワークインターフェイス情報の取得はlibcで提供されており、getifaddrs関数が利用できます。今回はlibcなどの*nix系プラットフォームが提供するAPIを扱いやすくしたnixcrateを使用します。
nixgetifaddrsInterfaceAddress構造体イテレータを返します。InterfaceAddress`構造体の定義は以下の通りです。

pub struct InterfaceAddress {
    /// Name of the network interface
    pub interface_name: String,
    /// Flags as from `SIOCGIFFLAGS` ioctl
    pub flags: InterfaceFlags,
    /// Network address of this interface
    pub address: Option<SockaddrStorage>,
    /// Netmask of this interface
    pub netmask: Option<SockaddrStorage>,
    /// Broadcast address of this interface, if applicable
    pub broadcast: Option<SockaddrStorage>,
    /// Point-to-point destination address
    pub destination: Option<SockaddrStorage>,
}

このうち、interface_name address netmaskを今回は利用します。

ここで注意したいのはgetifaddrsで返ってくる情報はIPに限らず、また、層によって別エントリで格納されるということです。
例えば、nixのドキュメントにある以下のコードを実行してみます。

// https://docs.rs/nix/latest/nix/ifaddrs/fn.getifaddrs.html
let addrs = nix::ifaddrs::getifaddrs().unwrap();
for ifaddr in addrs {
    match ifaddr.address {
        Some(address) => {
        println!("interface {} address {}",
                ifaddr.interface_name, address);
        },
        None => {
        println!("interface {} with unsupported address family",
                ifaddr.interface_name);
        }
    }
}

実行結果を抜粋すると以下のようになります(一部結果を加工しています)。

interface lo address 00:00:00:00:00:00
interface enp4s0 address xx:xx:xx:xx:xx:xx
interface wlp3s0 address xx:xx:xx:xx:xx:xx
interface virbr0 address 52:54:00:2c:d0:ab
...
interface lo address 127.0.0.1:0
interface wlp3s0 address 192.168.11.64:0
interface virbr0 address 192.168.122.1:0
...
interface lo address [::1]:0
interface wlp3s0 address [2400:xxxx:xxxx:xxxx:xxxx:xxxx:xxxx:xxxx]:0
interface wlp3s0 address [2400:xxxx:xxxx:xxxx:xxxx:xxxx:xxxx:xxxx]:0

このように同じインターフェイスでも、データリンク層(MACアドレス)とネットワーク層(IPアドレス)でエントリが分かれており、アドレスごとにもエントリが分かれています。

どのエントリなのかはOption<SockaddrStorage>型のaddressをダウンキャストして確認できます。ダウンキャストするメソッドはas_xxxで提供されており、IPv4ソケットアドレスへのダウンキャストはas_sockaddr_inメソッドを実行することでOption<SockaddrIn>を返します。

すべてのネットワークインターフェイスのIPv4に関するエントリを取得する処理は以下になります。

//...

impl NetworkInterface {
    pub fn new(interface: InterfaceAddress) -> Option<Self> {
        Some(NetworkInterface{
            name: interface.interface_name,
            address: Ipv4Addr::from(interface.address?.as_sockaddr_in()?.ip()),
            netmask: Ipv4Addr::from(interface.netmask?.as_sockaddr_in()?.ip()),
        })
    }
}

//...

// デバイスが持つすべてのIPv4ネットワークインターフェイスを返します
fn get_network_interfaces() -> Result<Vec<NetworkInterface>> {
    // 複数IPがある場合は別エントリになってしまうが、動作する
    Ok(getifaddrs().context("failed to get network interfaces")?
        .filter_map(|i| NetworkInterface::new(i))
        .collect::<Vec<_>>())
}

デフォルトゲートウェイの取得

getifaddrsにはデフォルトゲートウェイの情報が含まれていないため、別の方法で取得します。今回は簡便な方法で取得しました(netlinkソケットを利用してカーネルと通信して取得することもできます)。

/proc/net/routeには、ルーティング情報が記載されています。内容を抜粋すると以下の通りになります。

$ cat /proc/net/route
Iface	Destination	Gateway 	Flags	...                                                     
wlp3s0	00000000	010BA8C0	0003	...  
virbr0	0000FEA9	00000000	0001	...  
docker0	000011AC	00000000	0001	...
wlp3s0	000BA8C0	00000000	0001	...  
virbr1	0064A8C0	00000000	0001	...            

よってこの内容からGateway属性に00000000以外が設定されているインターフェイスを探せば良くなります
実装は以下のとおりです

const PROC_NET_ROUTE_PATH: &str = "/proc/net/route";

//...

// デフォルトゲートウェイが設定されているインターフェイスを1つ返します
fn find_default_interface(interfaces: &Vec<NetworkInterface>) -> Option<NetworkInterface> {
    let default_interface_name = find_default_interface_name()?;
    interfaces.iter().find_map(|i| if i.name == default_interface_name {
       Some(i.clone()) 
    } else {
        None
    })
}

// デフォルトゲートウェイが設定されているインターフェイスの名前を1つ返します
fn find_default_interface_name() -> Option<String> {
    BufReader::new(File::open(PROC_NET_ROUTE_PATH).ok()?).lines()
        .find_map(|l| {
            let entry = l.ok()?;
            let fields: Vec<&str> = entry.split("\t").collect();
            if fields[2] != "Gateway " && fields[2] != "00000000" {
                Some(fields[0].to_string())
            } else {
                None
            }
        })
}

「最初から/proc/net/routeを見れば良いじゃん」となりそうですが、/proc/net/routeにはインターフェイスが持つIPアドレスはわからないため、結局はgetifaddrsで取得するなどは必要になると考えています。

動作確認

実行環境デバイスが持つインターフェイスの一部を記載します。wlp3s0にはデフォルトゲートウェイが設定されています。

interface lo address 127.0.0.1/8
interface wlp3s0 address 192.168.11.64/24
interface virbr0 address 192.168.122.1/24

実行結果は以下の通りです。私の環境では正常に動作していることを確認できました。

$ cargo run 127.0.0.3
127.0.0.1

$ cargo run 192.168.11.100
192.168.11.64

$ cargo run 192.168.122.20
192.168.122.1

$ cargo run 8.8.8.8
192.168.11.64

また、「Rustで始めるTCP自作入門」の実装で送信元IPを選択する処理を本実装に置き換えてみましたが、動作できました。

まとめ

Rustで宛先IPから送信元IP(ネットワークインターフェイス)を選択する処理を実装しました。
「Rustで始めるTCP自作入門」での実装もそうですが、普段使っているものが、どのような動きをしているのか片鱗を見るというのは面白く、今後も時間があればこのような学習も行っていきたいと考えています。

明日は@famipapamart さんの担当です。

12
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
12
0