Tauri、React、Node.js、GridDB によるデスクトップ WiFi ネットワーク・モニターの構築

はじめに

この記事では、Node.jsTauri、および React のような技術を複雑に織り交ぜながら、デスクトップ WiFi ネットワーク・モニターを構築するプロセスを掘り下げていきます。私たちのアーキテクチャ決定の中心は、ストレージ・ニーズに GridDB を使用することに重点を置いていることです。

しかし、なぜ GridDB なのでしょうか?それは WiFi ネットワーク・モニタリングの複雑さをナビゲートする中で、信頼性が高く効率的なストレージ・メカニズムを選択することが最も重要になるからです。ユニークな機能と最適化されたパフォーマンス特性を持つ GridDB は、理想的な候補として浮上しました。Node.js の多用途性、React の豊富なユーザー・インターフェース機能、Tauri のクロスプラットフォームの利点と組み合わせることで、効果的なネットワーク・モニタリングへの全体的なアプローチを提示することを目指しています。

ソースコード

プロジェクトのセットアップにはいくつかのステップがあります。

準備

このソースコードは、Windows 11 と WSL2 上の Ubuntu 20.04 でテストされ、Windows 上の Npcap であるネイティブ・パケット・キャプチャに大きく依存しています。

インストールする前に Windows システムに WiFi デバイスがあることを確認してください。このプロジェクトは1つの WiFi デバイスでのみテストされていることに注意してください。

Npcap のインストール

Npcap は Microsoft Windows 用のパケットキャプチャと送信ライブラリです。後で cap のような node.js npm パッケージと一緒に使ってパケットキャプチャを実行できるように、まずこのソフトウェアをインストールする必要があります。

Npcap をダウンロードするには、同社のサイトにアクセスし、手間をかけずにインストールするために適切なインストーラの種類を選択してください。インストール時には、すべてのオプションをデフォルトのままにしておきます。

Windows 上でソースコードをクローンする

この GitHub リポジトリからソースコードをクローンします。

異なるオペレーティング・システム上で動作する2つのサーバーがあります。1つ目は、Windows 上で動作するフォワーダーのように動作する Node.js サーバーで、Node.js フォワーダー と呼んでいます。

Node.js フォワーダーは WiFi ネットワークトラフィックをキャプチャし、Linux 上の Node.js サーバーに送信します。Linux 上の Node.js サーバーは、キャプチャしたパケットをデータベースに保存し、Tauri デスクトップ・アプリケーションにデータを提供します。

Node.js フォワーダー

git clone git@github.com:junwatu/bearsakura-netwatch.git

server ディレクトリに移動し、すべての依存関係をインストールします。

cd server
npm install

このプロジェクトでパケットをキャプチャするには、node-gyp を必要とする cap npm パッケージを使ってネイティブ・モジュールをビルドします。このネイティブ・モジュールは Node.js と Npcap の橋渡しとなります。npm install を実行すると、ネイティブモジュールが自動的に作成されます。確実に動作させるには、グローバルにインストールすればよいです。

npm install -g node-gyp

しかし、node-gyp 自体には Python や C++ コンパイラのような依存関係があり、正しく動作させるためにはシステムにインストールする必要があります。node-gyp の Windows 向けガイドを参照してください。

env の設定

.env ファイルを設定し、必要であればポートを変更します。これらはデフォルト値です。

PACKET_PORT=5000
DATABASE_SERVER_PORT=4000
  • PACKET_PORT は、Windows 上の Node.js フォワーダーがリッスンするポートです。必要に応じて変更してください。
  • DATABASE_SERVER_PORT=4000 は、Linux 上の Node.js サーバーがリッスンするポートです。

Node.js フォワーダーの実行

このコマンドは Windows 上で Node.js フォワーダーを実行します。

npm run dev

Linux でソースコードをクローンする

このGitHub リポジトリからソースコードをクローンします。server-db ディレクトリに移動し、すべての依存関係をインストールします。

cd server-db
npm install

Linux サーバーの env を設定する

.env ファイルを設定し、必要に応じてポートを変更します。これはデフォルト値です。

DATABASE_SERVER_PORT=4000

のポートが Node.js Forwarder.env 設定と一致していることを確認します。例えば、.env 設定が DATABASE_SERVER_PORT=3000 の場合、このポートも 3000 でなければなりません。

Linux 上でサーバーを実行する

このコマンドは Linux 上でサーバーを実行します。

npm run dev

デスクトップモニターを実行する

デスクトップのバイナリファイルをこちらからダウンロードします。インストーラーを実行し、インストール・ディレクトリに移動します。

config.json ファイルを開きます。


{
    "api": {
        "base_url": "http://localhost:4000"
    }
}

.env の設定 DATABASE_SERVER_PORT=4000base_url のポートが一致していることを確認します。

例えば、.env 設定が DATABASE_SERVER_PORT=3000 の場合、base_urlhttp://localhost:3000 となります。

デスクトップアプリケーションを起動し、BearWacth.exe をダブルクリックします。

システム・アーキテクチャ

Desktop WiFi Network Monitor のアーキテクチャの中で、Npcap は Windows OS に最適化された強力なパケットキャプチャモジュールとして際立っています。このユーティリティは、WiFi ネットワークパケットを継続的に取得し、その後、専用の Node.js Forwarder に送ります。このフォワーダーはその後、GridDB データベースにデータを保存する Node.js Server にパケットを送信します。また、Node.js Server は、データをデスクトップ・ダッシュボードに表示する Desktop Dashboard Monitor にデータを提供します。

ネットワーク・トラフィックのキャプチャ

Node.js を使用してネットワーク・トラフィックをキャプチャするには、通常、libpcap (Unix 系システム) や WinPcap/Npcap (Windows 系システム) のようなシステムレベルのライブラリとインターフェースするネイティブ・モジュールを使用します。この投稿では Windows OSNpcap を使用します。

Node.js と GridDB によるバックエンド開発

Node.js

Node.js® はオープンソースのクロスプラットフォーム JavaScript 実行環境です。Windows インストーラーはこちらからダウンロードしてください。ここでは Node.js LTS v18.18.0 を使用します。Windows に Node.js をインストールする方法はたくさんありますが、彼らのドキュメントを参照してください。chocolatey のようなサードパーティの Windows パッケージインストーラを使用したり、手動インストールを使用することもできます。

Node.js のインストールは、ターミナルで以下のコマンドを実行することで確認できます。

node --version

GridDB

GridDB は、時系列データに特化した拡張性の高い NoSQL データベースです。独自のアーキテクチャに基づき、インメモリとディスクベースのストレージの両方を提供し、最適化されたパフォーマンスとデータの耐久性を保証します。そのアーキテクチャは、大量のデータを処理するように設計されており、IoT、遠隔測定、および時系列データが重要なあらゆるアプリケーションに適した選択肢となっています。コア機能だけでなく、GridDB は自動パーティショニングや堅牢なフェイルオーバー・メカニズムなどの高度な機能を誇り、データの一貫性と高可用性を保証します。

インストール情報については、GridDB 公式ウェブサイトを参照してください。

Node.js によるパケット・キャプチャ

パケット・キャプチャのコードは非常に簡単です。startCapturing(ipAddress) 関数がパケットキャプチャを開始します。WiFi インターフェースを検出し、パケットのキャプチャを開始します。getPackets() 関数はキャプチャしたパケットを返します。

import pkg from 'cap';
const { Cap, decoders } = pkg;

const PROTOCOL = decoders.PROTOCOL;

let packets = [];

function startCapturing(ipAddress) {
    const c = new Cap();
    const device = Cap.findDevice(ipAddress);
    const filter = 'ip';
    const bufSize = 10 * 1024 * 1024;
    const buffer = Buffer.alloc(65535);

    const devices = Cap.deviceList();
    const wifiDevice = devices.find(device => {
        const description = device.description.toLowerCase();
        return description.includes('wireless') || description.includes('wi-fi');
    });

    if (!wifiDevice) {
        console.error('No Wi-Fi device found!');
        process.exit(1);
    }

    const wifiInterfaceName = wifiDevice.name;
    const linkType = c.open(wifiInterfaceName, filter, bufSize, buffer);

    c.on('packet', function(nbytes, trunc) {
        const ret = decoders.Ethernet(buffer);

        if (ret.info.type === 2048) {
            const decodedIP = decoders.IPV4(buffer, ret.offset);
            const srcaddr = decodedIP.info.srcaddr;
            const dstaddr = decodedIP.info.dstaddr;

            let packetInfo = {
                length: nbytes,
                srcaddr: srcaddr,
                dstaddr: dstaddr
            };

            if (decodedIP.info.protocol === PROTOCOL.IP.TCP) {
                const decodedTCP = decoders.TCP(buffer, decodedIP.offset);
                packetInfo.protocol = 'TCP';
                packetInfo.srcport = decodedTCP.info.srcport;
                packetInfo.dstport = decodedTCP.info.dstport;
            } else if (decodedIP.info.protocol === PROTOCOL.IP.UDP) {
                const decodedUDP = decoders.UDP(buffer, decodedIP.offset);
                packetInfo.protocol = 'UDP';
                packetInfo.srcport = decodedUDP.info.srcport;
                packetInfo.dstport = decodedUDP.info.dstport;
            }

            packets.push(packetInfo);

            // Limit the storage to the last 100 entries (or any other number)
            if (packets.length > 100) packets.shift();
        }
    });
}

function getPackets() {
    return packets;
}

export { startCapturing, getPackets };

このパケットは http://localhost:5000/packets でホストされます(ポートとホストは .env での設定に依存します)。この Node.js サーバーはフォワーダーのように動作します。Express.js 上で動作し、キャプチャしたパケットを取得するために startCapturing モジュールを使用します。


import 'dotenv/config';
import express from 'express';
import bodyParser from 'body-parser';
import axios from 'axios';
import * as packetCapturer from './packetCapturer.js';

const app = express();
app.use(bodyParser.json());

const PORT = process.env.PACKET_PORT || 3000;
const HOST = process.env.PACKET_IP_ADDRESS || 'localhost';
const SERVER_DB_HOST = process.env.DATABASE_SERVER || 'localhost';
const SERVER_DB_PORT = process.env.DATABASE_SERVER_PORT || 4000;

packetCapturer.startCapturing(process.env.PACKET_IP_ADDRESS);

setInterval(async () => {
    const packets = packetCapturer.getPackets();
    try {
        await axios.post(`http://${SERVER_DB_HOST}:${SERVER_DB_PORT}/save-packets`, packets);
        console.log('Packets sent successfully');
    } catch (error) {
        console.error('Error sending packets:', error);
    }
}, 5000);  // Adjust the interval to your needs


app.get('/packets', (req, res) => {
    res.json(packetCapturer.getPackets());
});

app.listen(PORT, HOST, () => {
    console.log(`Server started on http://${HOST}:${PORT}`);
});

ブラウザで http://localhost:5000/packets を開くと、Linux 上の Node.js サーバーに送信する前のパケットを確認できます。このフォワーダは Linux 上の Node.js サーバに 5 秒ごとにパケットを送信します。

Node.js サーバー

Node.js サーバーは Linux 上で動作するシンプルな Node.js サーバーであり、キャプチャされたパケットを受け取り、GridDB データベースに格納します。データベースサーバーは http://localhost:4000 で動作します(ポートは .env の設定に依存します)。

import 'dotenv/config';
import cors from 'cors';
import express from 'express';
import bodyParser from 'body-parser';
import* as griddb from './griddbservice.js'

const app = express();
app.use(cors());
app.use(bodyParser.json());

const PORT = process.env.DATABASE_SERVER_PORT || 4000;
const HOST = process.env.DATABASE_SERVER || 'localhost';

app.get(
    '/info', (req, res) = > { res.json({message : 'database server'}); });

app.post(
    '/save-packets', async(req, res) = > {
      const packets = req.body;

      for (let packet of packets) {
        const {length, srcaddr, dstaddr, protocol, srcport, dstport} = packet;
        await griddb.saveData(
            {length, srcaddr, dstaddr, protocol, srcport, dstport});
      }

      res.json({message : 'saved'});
    });

app.get('/get-all-packets', async (req, res) => {
  const packets = await griddb.getAllData();
  res.json({packets});
})

app.listen(PORT, HOST, () => {
    console.log(`Server started on http://${HOST}:${PORT}`);
});

パケットデータは /save-packets エンドポイントの POST メソッドで受信します。saveData 関数はデータを GridDB データベースに保存します。

await griddb.saveData({length, srcaddr, dstaddr, protocol, srcport, dstport});

getAllData 関数は GridDB データベースから全てのデータを取得します。このデータはデスクトップアプリケーションに送信されます。

app.get('/get-all-packets', async (req, res) => {
  const packets = await griddb.getAllData();
  res.json({packets});
})

ブラウザで http://localhost:4000/get-all-packets を開けば、パケットデータがデータベースに保存されていることを確認できます。

Tauri と React によるフロントエンド開発

Tauri

Tauri は、Web 技術を使って小さく、安全で、高速なアプリケーションを構築するためのツールキットです。Tauri は Electron と競合するもので、Web フロントエンドでデスクトップアプリケーションを作成するための、よりスリムでパフォーマンスの高いソリューションを提供することを目指しています。Electron がそうであるように、Chromium インスタンス全体をバンドルすることに伴う肥大化や潜在的なセキュリティ問題を減らしつつ、開発者が使い慣れたウェブ技術を使用できるようにすることが核心的なアイデアです。

Tauri は Rust というメモリ・セーフな言語で構築されており、そのコアは非常に軽量なウェブビュー・レンダリング・エンジンです。これにより、Electron と比較して、バイナリサイズを大幅に小さくし、リソースの使用量を削減することができます。また、必要なパーミッションを最小化し、ウェブコンテンツをシステムから分離することで、強力なセキュリティモデルを提供しています。

Tauri が開発された背景には、サイズ、スピード、セキュリティに関する Electron の一般的な批判に対処する一方で、Web テクノロジーが提供する開発の容易さとクロスプラットフォーム機能を維持するという動機がありました。

React

React は、ユーザーインターフェイスを構築するための JavaScript ライブラリで、Facebook と個々の開発者や企業のコミュニティによって保守されています。複雑でインタラクティブな UI を効率的かつ柔軟に開発するために作られました。React の Virtual DOM は、レンダリングをさらに最適化し、アプリのパフォーマンスを向上させます。React の宣言的な性質はコードを単純化し、デバッグと管理を容易にします。コンポーネントベースのアーキテクチャにより、開発者は自身の状態を管理するカプセル化されたコンポーネントを構築することができ、それらを組み合わせて複雑な UI を作ることができます。React はまた、データの変更に応じて効率的に更新・レンダリングできるウェブ・アプリケーションを作成する能力を開発者に与えます。

Tauri と React

Tauri と React のコードは desktop フォルダにあります。ビルドするには Rust と node.js をインストールする必要があります。詳しくは Tauri のインストールガイドを参照してください。

デスクトップ WiFi モニターの主な UI は以下の通りです。

import React, { useState, useEffect } from 'react'
import PacketVisualization from './packet-visualization'
import { invoke } from "@tauri-apps/api/tauri";

import "./App.css";

function App() {
  const [packetData, setPacketData] = useState(null)
  const [apiBaseUrl, setApiBaseUrl] = useState(null)

  useEffect(() => {
    async function fetchPacketData() {
      const config = await invoke('read_config');
      const apiBaseUrlConfig = await invoke('get_api_base_url', { config: config });
      const response = await fetch(`${apiBaseUrlConfig}/get-all-packets`)
      const data = await response.json()

      setPacketData(data)
      setApiBaseUrl(apiBaseUrlConfig)
    }
  
    fetchPacketData()
    
    // fetch every 5 seconds
    const intervalId = setInterval(fetchPacketData, 5000)  
    // cleanup interval on component unmount
    return () => clearInterval(intervalId)  
  }, [])
  

  return (
    <div className="container">
      {packetData && <packetvisualization data={packetData}></packetvisualization>}
      <p className='packet-server'>
        Packet Server: {apiBaseUrl}
      </p>
    </div>
  );
}

export default App;

このコードは config.json ファイルを読み込む Rust コードを呼び出します。

const config = await invoke('read_config');
const apiBaseUrlConfig = await invoke('get_api_base_url', { config: config });
const response = await fetch(`${apiBaseUrlConfig}/get-all-packets`)

JavaScript のコードは config.json ファイルを読み込んで apiBaseUrl の設定を取得する Rust のコードを呼び出します。apiBaseUrl の設定は config.json ファイルの base_url の設定です。


{
    "api": {
        "base_url": "http://localhost:4000"
    }
}

これは config.json ファイルを読み込む Rust のコードです。


// Prevents additional console window on Windows in release, DO NOT REMOVE!!
#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]

use std::fs;
use serde_json::Value;

// Learn more about Tauri commands at https://tauri.app/v1/guides/features/command
#[tauri::command]
fn greet(name: &str) -> String {
    format!("Hello, {}! You've been greeted from Rust!", name)
}

#[tauri::command]
fn read_config() -> Result<String, String> {
  let path = "config.json";
  let config = fs::read_to_string(path).map_err(|e| e.to_string())?;
  Ok(config)
}

#[tauri::command]
fn get_api_base_url(config: String) -> Result<String, String> {
  let parsed_config: Value = serde_json::from_str(&config).map_err(|e| e.to_string())?;
  let base_url = parsed_config
    .get("api")
    .and_then(|api| api.get("base_url"))
    .and_then(|base_url| base_url.as_str())
    .ok_or("Missing 'api.base_url' setting")?;
  Ok(base_url.to_string())
}

fn main() {
    tauri::Builder::default()
        .invoke_handler(tauri::generate_handler![greet, read_config, get_api_base_url])
        .run(tauri::generate_context!())
        .expect("error while running tauri application");
}

JavaScript から呼び出したいカスタムメソッドはすべて tauri::Builder::default().invoke_handler メソッドに登録しなければなりません。例えば、JavaScript から read_config メソッドを呼び出したいので、tauri::Builder::default().invoke_handler メソッドに登録する必要があります。

将来の拡張

デスクトップ WiFi モニターは概念実証 (PoC) であり、改善できる点はたくさんあります。今後機能拡張することが望ましい点をいくつか列挙しておきます。

  • 異なるマシンでの動作
  • より良いパケット可視化
  • より良いセットアッププロセスとデプロイメント

ブログの内容について疑問や質問がある場合は Q&A サイトである Stack Overflow に質問を投稿しましょう。 GridDB 開発者やエンジニアから速やかな回答が得られるようにするためにも "griddb" タグをつけることをお忘れなく。 https://stackoverflow.com/questions/ask?tags=griddb

Leave a Reply

Your email address will not be published. Required fields are marked *