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

## はじめに

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

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

## ソースコード

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

### 準備

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

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

#### Npcap のインストール

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

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

[<img src="https://griddb.net/en/wp-content/uploads/2024/01/npcap-windows.png" alt="" width="499" height="388" class="aligncenter size-full wp-image-29893" />][4]

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

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

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

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

#### Node.js フォワーダー

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

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

<div class="clipboard">
  <pre><code class="language-sh">cd server
npm install</code></pre>
</div>

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

```sh
npm install -g node-gyp
```

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

#### `env` の設定

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

```sh
PACKET_PORT=5000
DATABASE_SERVER_PORT=4000
```

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

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

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

```sh
npm run dev
```

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

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

```sh
cd server-db
npm install
```

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

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

```sh
DATABASE_SERVER_PORT=4000
```

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

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

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

```sh
npm run dev
```

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

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

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

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

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

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

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

[<img src="https://griddb.net/en/wp-content/uploads/2024/01/desktop.png" alt="" width="852" height="632" class="aligncenter size-full wp-image-29891" />][8]

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

[<img src="https://griddb.net/en/wp-content/uploads/2024/01/system-arch.png" alt="" width="1357" height="802" class="aligncenter size-full wp-image-29890" />][9]

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 OS** と **Npcap** を使用します。

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

### Node.js

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

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

```sh
node --version
```

### GridDB

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

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

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

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

```javascript
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` モジュールを使用します。

```javascript
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` の設定に依存します）。

```javascript
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 データベースに保存します。

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

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

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

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

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

### React

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

### Tauri と React

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

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

```javascript
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 (
    &lt;div className="container">
      {packetData && &lt;packetvisualization data={packetData}>&lt;/packetvisualization>}
      &lt;p className='packet-server'>
        Packet Server: {apiBaseUrl}
      &lt;/p>
    &lt;/div>
  );
}

export default App;
```

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

```javascript
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` の設定です。

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

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

```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&lt;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&lt;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) であり、改善できる点はたくさんあります。今後機能拡張することが望ましい点をいくつか列挙しておきます。

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

 [1]: https://npcap.com/
 [2]: https://github.com/mscdex/cap
 [3]: https://npcap.com/#download
 [4]: https://griddb.net/en/wp-content/uploads/2024/01/npcap-windows.png
 [5]: https://github.com/junwatu/bearsakura-netwatch
 [6]: https://www.npmjs.com/package/node-gyp#on-windows
 [7]: https://github.com/junwatu/bearsakura-netwatch/releases/download/v0.1.0/BearWatch_0.1.0_x64-setup.exe
 [8]: https://griddb.net/en/wp-content/uploads/2024/01/desktop.png
 [9]: https://griddb.net/en/wp-content/uploads/2024/01/system-arch.png
 [10]: https://nodejs.org/en/download
 [11]: https://nodejs.dev/en/
 [12]: https://chocolatey.org/
 [13]: https://docs.griddb.net
 [14]: https://tauri.app/
 [15]: https://www.electronjs.org/
 [16]: https://www.rust-lang.org/
 [17]: https://react.dev/
 [18]: https://reactjs.org/docs/faq-internals.html
 [19]: https://tauri.app/v1/guides/getting-started/prerequisites