🔒

Torの接続マネージャを作る with Tauri

2024/06/28に公開

はじめに

こんにちは。calloc134 です。
最近個人開発のネタを探しており、ちょうど作ってみたかったため、Tor の接続マネージャを作ってみました。
技術的にも色々なことを試すことができたため、ブログとしてまとめてみました。

完成形

まずはじめに完成形を紹介します。

ローディング画面は以下の通りです。


アプリケーションとしてはシンプルな一画面のみです。
Tor の接続に必要なブリッジ情報とプロキシ情報を、既存の Tor の設定ファイルから読み込んでいます。
これを適切に設定して、接続ボタンを押すと Tor に接続されます。

接続の様子はリアルタイムでログに出力されます。
また、接続の進行状況はステータスバーで確認できます。

技術スタック

今回のアプリケーションはバックエンドとフロントエンドに分割されます。

バックエンド

Python のライブラリを利用するため、Python でのバックエンド開発に絞られました。

利用したライブラリが以下の通りです。

FastAPI

https://fastapi.tiangolo.com/

Python の Web フレームワークで、API の開発をすることが可能です。
今回は API 作成のフレームワークにこれを利用しました。
SSE というデータ通信の仕組みを利用して、リアルタイムにユーザへレスポンスを通知することも可能です。今回はこの仕組みを利用して、Tor の接続状況をリアルタイムに通知しています。

Stem

https://stem.torproject.org/api/control.html
Tor の制御を行うライブラリです。今回は、Tor の接続状況をリアルタイムに取得するために利用しました。

control.Controller.from_port(port=self.torPort)
tor_controller.authenticate()
bootstrap_status = tor_controller.get_info("status/bootstrap-phase")

python-dbus

https://dbus.freedesktop.org/doc/dbus-python/
Tor の開始を制御するために利用しています。

dbus = SystemBus()
systemd = dbus.get_object('org.freedesktop.systemd1', '/org/freedesktop/systemd1')
self.manager = Interface(systemd, 'org.freedesktop.systemd1.Manager')
self.manager.RestartUnit('tor.service', 'replace')

また、今回はコードを見やすくするために、returns というライブラリを利用してエラーハンドリングを行っています。

https://qiita.com/sobacha/items/ed4f9b2a49badf129797

フロントエンド

React

https://react.dev/

フロントエンド構築に利用しました。

Tauri

https://tauri.app/

デスクトップアプリを作成する上で利用されているフレームワークです。
これを利用すると、後述する Vite を利用して開発することができるため、Electron を利用した場合よりも開発体験よく開発することが出来ました。

Vite

https://vitejs.dev/

フロントエンドの開発環境構築や本番環境のビルドを行うためのユーティリティです。

Tailwind CSS

https://tailwindcss.com/

CSS フレームワークです。今回はこれを利用してデザインを行いました。

shadcn/ui

https://ui.shadcn.com/

Tailwind CSS を利用してデザインを行う際に利用することの出来る UI コンポーネントライブラリです。
今回は一般的なテキストエリアやボタン、ステータスバーなどを利用しました。

aceternity ui

https://ui.aceternity.com/

同様に Tailwind CSS を利用してデザインを行う際に利用することの出来る UI コンポーネントライブラリです。
このライブラリは非常にデザインの整ったコンポーネントを多く提供しています。今回はオーロラ背景のコンポーネントを利用しました。

react-hot-toast

https://react-hot-toast.com/

通知を表示するためのライブラリです。今回は接続ボタンを押した際に通知を表示するために利用しました。

react-spinners

https://www.davidhu.io/react-spinners/

ローディングアニメーションを表示するためのライブラリです。

また、今回はコードを見やすくするために、neverthrow というライブラリを利用してエラーハンドリングを行っています。

コーディング

バックエンドとフロントエンドで分割して解説します。

バックエンド

今回は、以下のようなレイヤー区分としてコーディングを行いました。

- main.py
- entity
  - config.py
  - bridgeConfig.py
  - proxyConfig.py
- usecase
  - handle.py
- repository
  - torrcRepository.py
  - torControlRepository.py

リポジトリ層では、Tor の設定ファイルを読み込むための torrcRepository と、Tor の制御を行うための torControlRepository を作成しました。
ユースケース層ではユーザからのリクエストをハンドルするための handle を作成しています。
エンティティとしてユーザの送信した設定情報を保持するための configbridgeConfigproxyConfig を作成しています。

バリデーション

基本的にはエンティティ側でバリデーションを行います。
https://github.com/calloc134/torito-backend/blob/master/src/torito_prototype/entity/bridgeConfig.py

設定の記述が正しいかどうかを正規表現で判定しています。
エンティティ内では雑にエラーを投げてしまっています・・・
個人的には result 型に寄せたかったところです。

構成ファイル参照と保存・接続

https://github.com/calloc134/torito-backend/blob/master/src/torito_prototype/usecase/handle.py

構成ファイル参照部分では比較的うまくレイヤー区分を行うことができました。
一方、保存・接続部分では、SSE を利用してリアルタイムに接続状況を通知するために、レイヤー区分がうまく行えていない部分があります。

SSE を利用してリアルタイムに接続状況を通知する際は Python のジェネレータを利用することになるのですが、ジェネレータではエラーを投げてもユーザに通知されません。
そのため、すべての処理を try-except で囲み、エラーを検知したらエラーメッセージとしてユーザに通知するようにしています。
また、接続状況を非同期でユーザに通知するためにリポジトリを利用する必要があるのですが、そのレスポンスの送信をリポジトリ内で行ってしまっているため、レイヤー区分がうまく行えていないと感じています。
今後、リファクタリングの余地があると思われます。
(ただ、リファクタリングがどれだけ有効に機能するかはわからないため、現状ままでも問題ないかもしれません)

フロントエンド

今回は以下のようなレイヤー区分としてコーディングを行いました。

- index.html
- src
  - index.css
  - components
    - App.tsx
    - Loading.tsx
    - MainPanel.tsx
    - ui
      - aurora-background.tsx (aceternity ui)
      - button.tsx (shadcn/ui)
      - textarea.tsx (shadcn/ui)
      - checkbox.tsx (shadcn/ui)
      - progress.tsx (shadcn/ui)
  - context
    - ServerDataContext.tsx
    - ServerDataProvider.tsx
    - ServerDataContextType.ts
    - useServerData.ts
  - lib
    - utils.ts (shadcn/ui)
  - types
    - ServerData.ts
  - utils
    - fetchData.ts
    - mutationData.ts

コンポーネントディレクトリでは、MainPanelLoading を作成しました。
MainPanel はメインの画面を表示するコンポーネントで、Loading はローディングアニメーションを表示するコンポーネントです。
また、ui ディレクトリには、aceternity ui と shadcn/ui のコンポーネントを配置しています。

コンテキストディレクトリでは、利用するデータを提供するためのコンテキストを作成しています。

ライブラリディレクトリでは、fetch と mutation の処理を行うためのユーティリティを配置しています。

Suspense の利用 (フェッチ)

fetchDataでは、内部で Promise を throw することで、Suspense の利用を想定したデータフェッチを行っています。

Suspense を利用することで、コンテキストの型から undefined を除外することができることを確認でき、面白かったです。

https://github.com/calloc134/torito-frontend/blob/746c2b1148f60ec5ed2d3d9c52433e4e935521a1/src/utils/fetchData.ts#L4

Suspense の仕様を考えながら書いていたのですが、データを溜めておく場所としてグローバル変数は必要なんでしょうか・・・?
どうしてもこれ以外に方法が思いつかなかったのでこのような実装となっています。

関連するツイート
https://x.com/calloc134/status/1800765660278194505

接続状況を取得するための SSE 対応 (ミューテーション)

mutationDataでは、fetch API を利用して、SSE で接続状況を取得するための処理を行っています。

https://github.com/calloc134/torito-frontend/blob/746c2b1148f60ec5ed2d3d9c52433e4e935521a1/src/utils/mutationData.ts

引数としてコールバック関数を受け取るようにすることで、コンポーネント側で任意の処理を差し込みやすくなっています。

fetch API で SSE 対応をするやり方についてはこちらの記事を参考にしました。
https://medium.com/@david.richards.tech/sse-server-sent-events-using-a-post-request-without-eventsource-1c0bd6f14425

これを実装し終わった段階でちょうど以下の記事が出ていたため、偶然の一致を感じました・・・
https://zenn.dev/cybozu_frontend/articles/try-server-sent-events

バックエンドとフロントエンドの接続

今回通常の Web アプリケーションではなく、デスクトップアプリケーションを作成しているため、バックエンドとフロントエンドの接続方法が通常の Web アプリケーションとは異なります。

今回は、バックエンドとフロントエンドを別々に立ち上げ、バックエンドの API をフロントエンドから呼び出す形で接続を行いました。

最初に、Tauri は Rust と React によるデスクトップアプリケーションを作成するためのフレームワークであり、FastAPI をそのまま利用することはできません。

したがって、FastAPI を実行可能なバイナリに変換し、Tauri の Rust から呼び出す形で接続を行います。

この作業には以下のような手順が必要です。

  • Python ソースコードのビルド
  • Python バイナリの埋め込み

Python ソースコードのビルド

Python ソースコードをビルドするためには、PyInstaller というツールを利用します。

PyInstaller は、Python スクリプトを実行可能なバイナリに変換するツールです。
これを用いて Python スクリプトをバイナリに変換する CD を Github Actions で作成しました。

https://github.com/calloc134/torito-backend/blob/master/.github/workflows/bulid.yml

なお、開発環境では Manjaro Linux を利用していたのですが、環境の問題かうまく動作しませんでした。Github Actions でわざわざビルドしているのはそのためです。

Python バイナリの埋め込み

Tauri には、他のバイナリをサイドカーとして埋め込むための機能があり、これを利用することで、Python バイナリを Tauri のアプリケーションに埋め込むことができます。
https://tauri.app/v1/guides/building/sidecar/

この機能を利用することで、Python バイナリを Tauri のアプリケーションに埋め込むことができます。

また、FastAPI のサイドカーの呼び出しを Rust から行うために、以下のリポジトリを参考にさせていただきました。

https://github.com/dieharders/example-tauri-python-server-sidecar

フロントエンドのビルド

まずバックエンドのリポジトリからビルドされたバイナリを取得し、またフロントエンドのビルドに必要な依存関係をすべてインストールしてから、ビルドを行います。

こちらも手元の環境ではビルドがうまくいかなかったため、Github Actions でビルドを行いました。

https://github.com/calloc134/torito-frontend/blob/main/Makefile

https://github.com/calloc134/torito-frontend/blob/main/.github/workflows/build.yml

以上により、バックエンドとフロントエンドの接続を行い、デスクトップアプリケーションとしてのアプリケーションを完成させました。

余談ですが、今回のアプリケーションは Tauri 特有の機能を殆ど利用していないため、ビルド次第では Web アプリケーションとして動作させることも可能です。

おわりに

今回のアプリケーションは Tor の接続マネージャを単純に作成することを目的としていましたが、初めて利用したライブラリやフレームワークが多かったため、思ったよりも開発に時間がかかってしまいました。

また、バックエンドとフロントエンドの接続方法が通常の Web アプリケーションとは異なるため、その部分で苦労したこともありました。

しかし、モダンな UI でちゃんと動作するようなアプリケーションをしっかり形にすることができたため、個人的にはかなり満足しています。
また、新しい技術の実験場としても非常に有効でした。

今回の開発ではあまりテストコードを書けていません。せっかくレイヤ区分を行っているため、今後もし要望がある場合はしっかりとテストコードを記述していきたいと思います。

追記: 懸念点

今回のアプリケーションについて、セキュリティ的な懸念がいくつか存在しています。

localhost:3001 へ同一コンピュータ内部のアプリケーションがリクエスト可能

この問題は、Tauri がデフォルトで localhost:3001 に API サーバを立てるため、同一コンピュータ内部のアプリケーションがリクエスト可能であるという問題です。
これにより、任意のアプリケーションが torrc の内容を取得でき、また任意のプロキシやブリッジを指定できます。
FastAPI をバックエンドに利用していることにより、修正が難しいと考えられます。

Tor の ControlPort への認証サポート

Tor が ControlPort を公開するとき、認証のためのクッキーを要求するように設定することで、不正なアクセスを防ぐことができます。
このアプリケーションではこのクッキーを用いた認証をサポートしていないため、認証を行っている場合には利用できません。
今後、当機能を追加することを検討しています。

Tor の ControlPort として unix domain socket をサポートする

そもそも ControlPort を TCP で公開すること自体がセキュリティ的に問題があるため、unix domain socket を利用することが推奨されています。
このアプリケーションでは unix domain socket による Tor との通信をサポートしていないため、セキュリティ的に問題があると考えられます。
今後、当機能を追加することを検討しています。

GitHubで編集を提案

Discussion