ローカルネットワーク内の機器を検索する (mDNS)

Recipe ID: net-015

ローカルネットワーク(LAN)内にあるプリンタ、NAS、IoT デバイス、あるいは他の Tauri アプリを見つける機能です。
IP アドレスが分からなくても、.local ドメインやサービスタイプ(例: _http._tcp.local.)を使って機器を特定できます。

Tauri (Rust) では mdns-sd クレートなどを使用することで、簡単にサービスディスカバリを実装できます。

実装手順

1. 依存関係の追加 (src-tauri/Cargo.toml)

[dependencies]
mdns-sd = "0.17"

2. Rust 実装 (スキャン処理)

mDNS のスキャンは時間がかかる(または永続的に監視する)処理なので、非同期タスクとして実行し、見つかったデバイスを順次フロントエンドにイベント通知するのが一般的です。

src-tauri/src/lib.rs

use tauri::{AppHandle, Emitter};
use mdns_sd::{ServiceDaemon, ServiceEvent};
use serde::Serialize;

#[derive(Serialize, Clone)]
struct MdnsDevice {
    name: String,
    ip: Vec<String>,
    port: u16,
}

// サービス検索を開始するコマンド
#[tauri::command]
fn scan_local_devices(app: AppHandle, service_type: String) -> Result<(), String> {
    // 例: service_type = "_http._tcp.local."
    
    // スキャンは別スレッドで行う
    std::thread::spawn(move || {
        // デーモンの作成
        let mdns = match ServiceDaemon::new() {
            Ok(d) => d,
            Err(e) => {
                eprintln!("Failed to create daemon: {}", e);
                return;
            }
        };

        // サービスのブラウズ開始
        let receiver = match mdns.browse(&service_type) {
            Ok(r) => r,
            Err(e) => {
                eprintln!("Failed to browse: {}", e);
                return;
            }
        };

        println!("Started scanning for: {}", service_type);

        // イベントループ
        while let Ok(event) = receiver.recv() {
            match event {
                // サービスが見つかった(IPアドレス等の詳細解決済み)
                ServiceEvent::ServiceResolved(info) => {
                    println!("Resolved: {:?}", info);
                    
                    let ips: Vec<String> = info.get_addresses()
                        .iter()
                        .map(|ip| ip.to_string())
                        .collect();

                    let device = MdnsDevice {
                        name: info.get_fullname().to_string(), // 一意な識別子として fullname を使用
                        ip: ips,
                        port: info.get_port(),
                    };

                    // フロントエンドに通知
                    let _ = app.emit("mdns-device-found", device);
                }
                ServiceEvent::ServiceRemoved(_service_type, fullname) => {
                    // デバイスが消えた場合 (簡易的に名前だけ送る)
                     let _ = app.emit("mdns-device-lost", fullname);
                }
                _ => {}
            }
        }
    });

    Ok(())
}

#[cfg_attr(mobile, tauri::mobile_entry_point)]
pub fn run() {
    tauri::Builder::default()
        .invoke_handler(tauri::generate_handler![scan_local_devices])
        .run(tauri::generate_context!())
        .expect("error while running tauri application");
}
注意: mdns-sdServiceInfoserde::Serialize を直接実装していない可能性があります。その場合は、必要なフィールド(IP、ポート、ホスト名など)だけを抽出した自前の構造体 (#[derive(Serialize)]) を定義し、それにデータを移し替えてから emit してください。

3. フロントエンド実装

import { invoke } from '@tauri-apps/api/core';
import { listen } from '@tauri-apps/api/event';

interface DiscoveredDevice {
    name: string;
    ip: string[];
    port: number;
}

let devices: DiscoveredDevice[] = [];

async function startMdnsScan() {
    // 1. デバイス発見イベントのリスナー登録
    await listen<DiscoveredDevice>('mdns-device-found', (event) => {
        const device = event.payload;
        console.log('Found:', device);
        
        // 重複チェックしてリストに追加
        if (!devices.find(d => d.name === device.name)) {
            devices.push(device);
            // ここでUIを更新する処理を呼び出す
            console.log("Updated Device List:", devices);
        }
    });

    // 2. デバイス消失イベントのリスナー登録 (オプション)
    await listen<string>('mdns-device-lost', (event) => {
        const name = event.payload;
        console.log('Lost:', name);
        
        devices = devices.filter(d => d.name !== name);
        console.log("Updated Device List:", devices);
    });

    // 3. スキャンの開始 (例: HTTPサーバー _http._tcp.local. を探す)
    try {
        await invoke('scan_local_devices', { serviceType: '_http._tcp.local.' });
    } catch (error) {
        console.error('Scan failed:', error);
    }
}

// 実行
startMdnsScan();

Tips とトラブルシューティング

1. ファイアウォール

mDNS は UDP ポート 5353 を使用します。 開発者のマシンやユーザーの環境によっては、OS のファイアウォールがパケットをブロックすることがあります。 Windows では、アプリ初回起動時に「このアプリの通信を許可しますか?」というダイアログが出ることがありますが、これを拒否すると検索できなくなります。

2. 自分自身のサービスを広告 (Advertise) する

逆に、「自分のアプリを他の端末から見つけてもらいたい」場合は、register メソッドを使ってサービスを登録します。
let mdns = ServiceDaemon::new().expect("Failed to create daemon");
let service_info = ServiceInfo::new(
    "_my-app._tcp.local.",
    "My Tauri App",
    "my-host.local.",
    "",
    12345, // Port
    &properties[..],
).unwrap();

mdns.register(service_info).expect("Failed to register our service");

3. IPv6 対応

最近のネットワークでは IPv6 アドレスが返ってくることがよくあります。IP アドレスを利用して通信を行う際は、IPv4/IPv6 のどちらを使うか、あるいは両方試す実装が必要になる場合があります。