MastodonでTorインスタンスと連合ネットワークを組む方法、2.4.5版


MastodonインスタンスがTor内インスタンスと連合ネットワークを組むのに必要なサポートが2.4.0から組み入れられました。その機能を導入するための手順を解説したいと思います。

* 改訂にあたり、2.4.0には脆弱性が存在するため2.4.5を対象とするように書き換えました。

# Clearnet側

Clearnet側がTor連合に必要なのはTorに繋がるHTTPプロクシだけです。それをTorとPrivoxyを組み合わせて作成します。Mastodonインスタンスは既に動いているものとします。また、パッケージ導入のコマンドはDebian系を仮定します。

まずはPrivoxyとTorをインストールします。

```sh
apt install privoxy tor
```

次に「Tor内(`.onion`のURL)へはTorのSOCKSプロクシへフォワードし、それ以外は直接アクセスする」という設定のため、以下を`/etc/privoxy/config`へ追記します

```text:/etc/privoxy/config
forward-socks5t .onion 127.0.0.1:9050 .
```

Privoxyを再起動したら、このプロクシが機能しているかチェックします。まず、clearnet側へのアクセスがTorを経由して**いない**ことを確認します。

```sh
curl -Ss --proxy http://127.0.0.1:8118 https://check.torproject.org/ |sed -n '/<h1/,/<\/h1>/p'
```
```text
  <h1 class="off">
    
      Sorry. You are not using Tor.
    
  </h1>
```

次にTorサイトへのアクセスを確認。

```sh
curl -Ss --proxy http://127.0.0.1:8118 http://nq5jmc5rsyo4fiph.onion/
```  
```text                     
<html><body>You are being <a href="http://nq5jmc5rsyo4fiph.onion/about">redirected</a>.</body></html>
```

こうして出来たHTTPプロクシをMastodonに使わせるため`.env.production`に以下を追記します。

```text:.env.production
http_proxy=http://127.0.0.1:8118
ALLOW_ACCESS_TO_HIDDEN_SERVICE=true
```

これでsidekiqとpumaの再起動をするとTorインスタンスと連合を組むことが出来ます。試しにユーザー検索欄から実際に`@root@nq5jmc5rsyo4fiph.onion`を検索してみましょう。木之本桜とGentooロゴのアバターのユーザー情報が取得されるはずです。

## Cloudflareを利用している場合の注意点
Cloudflareをキャッシュや攻撃防御、etc...に利用中の場合、Torからの接続を介したサーバ間通信で403が出る可能性があります。これはCloudflare側で自動的に危険な接続と判断されてBOT回避ページをCloudflareが差し込んでいる為です。[^1]

CloudflareにはWhitelistに追加することによって接続を信頼する方法があるので、手順を以下にざっくりと記します。なお、Cloudflareの操作方法はそれなりに理解しているものと仮定しています。

![JqkGFa6Woj12U9ZD.png](https://qiita-image-store.s3.amazonaws.com/0/92870/b42bf364-6f83-660c-7fd6-b662b25e7442.png)

ドメインのOverviewからFirewallを選択。

![vaedRKThE1wnnzYK.png](https://qiita-image-store.s3.amazonaws.com/0/92870/1e60af2f-63e3-6724-9e61-b4e61e646a12.png)

下の方にあるIP Firewallに

![8GIIRPAxCR9ZZFYZ.png](https://qiita-image-store.s3.amazonaws.com/0/92870/71d00762-1ce3-b89f-aea1-a4fd8c9d76ed.png)

`t1`と入力、するとドロップダウンに`Tor`が出てくるのでそれを選択。

![v0Mb1fAVQhR0Bt8z.png](https://qiita-image-store.s3.amazonaws.com/0/92870/6f38dd02-3372-c60f-0404-e066a7f9c2a5.png)

標準では`Block`になっているが、今回はWhitelist化するので`Block`の部分をクリック。

![TMwKkJO8jQzC2MOa.png](https://qiita-image-store.s3.amazonaws.com/0/92870/c0d4e949-c4d0-d93c-881c-eedc7225b272.png)

出てきたドロップダウンから`Whitelist`を選択。

![FveYswYCJipFYWgP.png](https://qiita-image-store.s3.amazonaws.com/0/92870/48174462-76b0-4560-f3ab-f178055e4ece.png)

最後に`Add`をクリック。

![5NLCgrSbcDsbgxT4.png](https://qiita-image-store.s3.amazonaws.com/0/92870/91ab1510-499b-ea2c-30a7-a2774736067f.png)

するとルールリストのValue(接続元)に`Tor (T1)`、Action(動作)に`Whitelist`が選択されたものが追加される。

以上でTorネットワークからの接続が拒否されることがなくなるでしょう。

[^1]: [Does Cloudflare block Tor? – Cloudflare Support](https://support.cloudflare.com/hc/en-us/articles/203306930-Does-Cloudflare-block-Tor-)



# Tor側

MastodonインスタンスをTorネットワーク内に立てる為に必要なこと。

## 押さえておきたいこと

MastodonにはHTTPプロクシ機能が実装されましたが、Tor内で動かすにあたってその機能を使ってはいけません。HTTPプロクシが使われるのはMastodon自身のソース部分のみであり、Mastodonが依存しているGemがそれを使うとは限りません。もしそれらGemがプロクシを経由しない通信を発生させた場合、そこから情報が漏洩することになります。

Mastodonに限らず、能動的に外部にアクセスするプログラムをTor内で動かす場合は透過プロクシ環境を構築することが必須です。それを怠ったためにサーバー特定に繋がり、権力の餌食となっていった事例が存在します[^3]。

他に、Tor内のサイトは基本的にHTTPSを使わないというのがあります。

[^3]: https://ezoeryou.github.io/blog/article/2017-10-08-australian-police-run-cp.html

## 手順

流れは以下のとおりです。

1. Torをインストールする
2. インスタンスのホスト名を生成する
3. Torとiptablesの組み合わせによる透過プロクシ環境を構築する。
4. Tor向けに特化したMastodonのインストール・設定を行う。

### 透過プロクシ環境構築・ホスト名生成

透過プロクシの通信の流れは次の下手なポンチ絵のような感じになります。

これはTorインスタンスから別のTorサイトへアクセスする様子。
![tor-to-tor.png](https://qiita-image-store.s3.amazonaws.com/0/91658/625ef22f-e40e-a97e-ae89-1ccad1002751.png)


これはTorインスタンスからClearnetサイトへアクセスする様子。
![tor-to-clearnet.png](https://qiita-image-store.s3.amazonaws.com/0/91658/e1385504-b141-7a4b-a6b8-00fe0621a31a.png)


まずTorとiptables-persistentをインストールします。

```sh
apt install tor iptables-persistent
```

次にTorでアクセスを受ける為とホスト名生成の為のディレクティブを追記します。

```text:/etc/tor/torrc
TransPort 127.0.0.1:9040
DNSPort 127.0.0.1:5353
VirtualAddrNetwork 127.192.0.0/10
TransPort [::1]:9040
DNSPort [::1]:5353
VirtualAddrNetworkIPv6 [fc00:70a::]/32
AutomapHostsOnResolve 1

HiddenServiceDir /var/lib/tor/mastodon
HiddenServiceVersion 3
HiddenServicePort 80 [::1]:80
```

こうしてTorを再起動すると、`/var/lib/tor/mastodon/hostname`に`***.onion`のような新しいホスト名が生成されるのでメモっておきます。

次に、iptablesを使って特定ユーザー(mastodon)から発信された通信をTorに投げるルールを設定します。


まずnatテーブルとfilterテーブルにTORIFYチェーンを作成。

```sh
iptables -N TORIFY
iptables -t nat -N TORIFY
```

取り敢えずlocalhost宛はTorに投げないし拒否もしないようにします。

```sh
iptables -t nat -A TORIFY -d 127.0.0.0/8 -j ACCEPT
iptables -A TORIFY -d 127.0.0.0/8 -j ACCEPT
```
『UDP:53宛の通信を、ローカルのTorがDNSPortとして受け付けている5353へ、TCP全てをTransPortの9040へ投げる。以上のnatテーブルをすり抜けてきたものは拒否』というルールをTORIFYチェーンの末尾に追加します。

```sh
iptables -t nat -A TORIFY -p udp --dport 53 -j REDIRECT --to-ports 5353
iptables -t nat -A TORIFY -p tcp -j REDIRECT --to-ports 9040
iptables -A TORIFY -m state --state RELATED,ESTABLISHED -j ACCEPT
iptables -A TORIFY -j REJECT --reject-with icmp-net-prohibited
```

DB等、Mastodonの動作に必要なローカルネットワークに載っているサービスをTorに投げないようにするルールをTORIFYの「先頭に」追加(-Aではなく-I)します。また、filterテーブルのTORIFYチェーンにも同じ宛先を追加します。以下はポスグレが192.168.16.2:5432にある場合の例です。redisやMTAに対しても同じ要領で繰り返します。

```sh
iptables -t nat -I TORIFY -p tcp -d 192.168.16.2 --dport 5432  -j ACCEPT
iptables -I TORIFY -p tcp -d 192.168.16.2 --dport 5432  -j ACCEPT
```

* このように、DB等がローカルホストではなく他のホストに散らばっている場合、透過プロクシの例外を設定していくのが面倒くさいので、Mastodonと同一のホストで動かすこと、出来れば通信にUNIXドメインソケットを使う事がおすすめです。

以上のTORIFYチェーンに、ユーザーがmastodonだった時に放り込むようにします。

```sh
iptables -A OUTPUT -m owner --uid-owner mastodon -j TORIFY
iptables -t nat -A OUTPUT -m owner --uid-owner mastodon -j TORIFY
```

ここまででIPv4接続に対する透過プロクシ環境が完成しました。これと同じ要領でIPv6用のiptablesの設定を進めていきます。

```sh
# チェーン作成
ip6tables -N TORIFY
ip6tables -t nat -N TORIFY
# localhostは許可。転送無し
ip6tables -t nat -A TORIFY -d ::1 -j ACCEPT
ip6tables -A TORIFY -d ::1 -j ACCEPT
# DNSリクエストとTCPはTorへ
ip6tables -t nat -A TORIFY -p udp --dport 53 -j REDIRECT --to-ports 5353
ip6tables -t nat -A TORIFY -p tcp -j REDIRECT --to-ports 9040
# 確立済みを除いてそれ以外の送出は拒否
ip6tables -A TORIFY -m state --state RELATED,ESTABLISHED -j ACCEPT
ip6tables -A TORIFY -j REJECT --reject-with icmp6-adm-prohibited
# Torへ転送しないもの。ポスグレが[fd00::5432:1]:5432にある場合
ip6tables -t nat -I TORIFY -p tcp -d fd00::5432:1 --dport 5432  -j ACCEPT
ip6tables -I TORIFY -p tcp -d fd00::5432:1 --dport 5432  -j ACCEPT
# mastodonユーザーの時はTORIFYへ
ip6tables -A OUTPUT -m owner --uid-owner mastodon -j TORIFY
ip6tables -t nat -A OUTPUT -m owner --uid-owner mastodon -j TORIFY
```

以上で透過プロクシ環境が構築されました[^2]。mastodonユーザーに切り替えて、プロクシ使用に関するオプションを指定していないプログラムがTor経由のアクセスを出来ているか、ポスグレ等各種デーモンに接続できるかを確認しましょう

```sh
su mastodon
curl -Ss https://check.torproject.org |sed -n "/<h1/,/<\\/h1/p"
psql -h fd00::5432:1 -U hoge
```

```shell-session
  <h1 class="not">
    
      Congratulations. This browser is configured to use Tor.
    
  </h1>
```

問題が無さそうだったら、iptablesの設定を永続化します。

```sh
iptables-save >/etc/iptables/rules.v4
ip6tables-save >/etc/iptables/rules.v6
```

[^2]: ところでTor自体は外への接続にIPv6を使うことが出来るのですが、TorのDNS機能を使うと何故かAAAAを返してこないのでIPv6が使われないかのような動作をします。そのため、他のキャッシュDNSを組み合わせて`.onion`だけはTorに、それ以外を`1.1.1.1`にフォワードするという運用方法もあります。

### Tor用インスタンス設定・改変

ここから大体[Production Guide](https://github.com/tootsuite/documentation/blob/master/Running-Mastodon/Production-guide.md)に則ってインストールを進めていくことになります。まっさらな状態からインストールするという場合、既に透過プロクシの影響により`git clone`や`bundle`が遅くなっているはずですが驚かないように。以下、clearnetにインスタンスを立てる時と手順が変わる所だけを述べていきます。

#### Mastodon本体

Tor内のサイトはHTTPSが基本的には使われないのですが、素のMastodonはHTTPSを無効化する設定がどこかのバージョンから削除され、HTTPアクセスをどうやっても強制的にリダイレクトしてしまうので、それを無効化するパッチを当てます。

```diff
diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb
index 29ba6cad6..df22cf32e 100644
--- a/app/controllers/application_controller.rb
+++ b/app/controllers/application_controller.rb
@@ -33,7 +33,7 @@ class ApplicationController < ActionController::Base
   private
 
   def https_enabled?
-    Rails.env.production?
+    false
   end
 
   def store_current_location
diff --git a/config/initializers/ostatus.rb b/config/initializers/ostatus.rb
index 5773b7290..93961fd19 100644
--- a/config/initializers/ostatus.rb
+++ b/config/initializers/ostatus.rb
@@ -7,7 +7,7 @@ web_host = ENV.fetch('WEB_DOMAIN') { host }
 alternate_domains = ENV.fetch('ALTERNATE_DOMAINS') { '' }
 
 Rails.application.configure do
-  https    = Rails.env.production? || ENV['LOCAL_HTTPS'] == 'true'
+  https    = false
 
   config.x.local_domain = host
   config.x.web_domain   = web_host
diff --git a/config/initializers/session_store.rb b/config/initializers/session_store.rb
index 3dc0edd6f..85983d259 100644
--- a/config/initializers/session_store.rb
+++ b/config/initializers/session_store.rb
@@ -1,3 +1,3 @@
 # Be sure to restart your server when you modify this file.
 
-Rails.application.config.session_store :cookie_store, key: '_mastodon_session', secure: (Rails.env.production? || ENV['LOCAL_HTTPS'] == 'true')
+Rails.application.config.session_store :cookie_store, key: '_mastodon_session'
```

`.env.production`編集時、Torアクセスを許可するための環境変数をさらに追加します。もちろん`LOCAL_DOMAIN`に先程生成された.onionアドレスを指定すること。

```text
ALLOW_ACCESS_TO_HIDDEN_SERVICE=true
```

* 2.4.0では`HIDDEN_SERVICE_VIA_TRANSPARENT_PROXY=true`が必要でしたが、2.4系列の最新では必要が無くなりました。以前にこのガイドに沿ってインストールした方は削除しても構いません。

#### nginx

HTTPSを使わなくなったので、それに関する設定を削除し、443番から80番で(というかHiddenServicePortで指定したポートで)listenするようにします。また、`server_name`ディレクティブを.onionアドレスにします。

以上の手続きを経てnginx・Mastodon各種サービスを再起動させたら、Torインスタンスの完成です。ユーザー検索で`@root@nq5jmc5rsyo4fiph.onion`の情報取得は出来たでしょうか。

### 代替手段: socksifyによるプロクシ使用方法

透過プロクシを構築する代わりに、socksify(1)を使う方法もあります。あるソフトウェアをsocksify(1)経由で起動させると、通信に関するライブラリコールをフックすることにより元来プロクシに対応していないソフトウェアでもプロクシを通過させるようにすることが出来ます。

メリットは、対象プログラムだけTorプロクシを使わせるように出来るため、作業時に他のサーバーとのやり取りがやりやすいこと、透過プロクシ設定と確認にrootと一般ユーザーを行き来する必要がなく管理が楽であることが挙げられます。デメリットは、環境によってはセグフォを起こす可能性があり、試してみるまでわからないこと、IPv6に対応していないことが挙げられます。

#### socksifyインストール・設定

socksifyをインストールします。このプログラムはDanteというソフトウェアの一部です。

```sh
apt install dante-client
```

socksifyでどのプロクシにアクセスするかなどの設定を行います。

```text:/etc/dante.conf
## 名前解決をSOCKSの先でさせるようにする
resolveprotocol: fake

## bindは素通しさせる
route {
    from: 0.0.0.0/0 to: 0.0.0.0/0 via: direct
    command: bind
}

## データベースなど、サービスに必要なローカルネットへのアクセスを
## SOCKS接続から除外させるためのルールを列挙していく。
## ポスグレが192.168.16.2:5432にある時
route {
    from: 0.0.0.0/0 to: 192.168.16.2/32 port = 5432 via: direct
}
## redisが127.0.0.1:6379にある時
route {
    from: 0.0.0.0/0 to: 127.0.0.1/32 port = 6379 via: direct
}
## その他MTAなどがあればそれも同じように

## 列挙が面倒臭ければネットワークの範囲を指定して除外させるという設定も可
#route {
#    from: 0.0.0.0/0 to: 127.0.0.0/8 via: direct
#}
#route {
#    from: 0.0.0.0/0 to: 192.168.16.0/24 via: direct
#}

## 残りの外向きの通信はTorに投げる
route {
    from: 0.0.0.0/0 to: 0.0.0.0/0 via: 127.0.0.1 port = 9050
    proxyprotocol: socks_v5
    method: none
}
```

#### Mastodon本体

透過プロクシの時と同じようにHTTPSの無効化をします。まずは同じパッチを本体に当てて下さい。

次に、プライベートアドレスかのチェックをする機能を完全に抑えるパッチを当てます。socksifyを通した通信を行うと名前解決先のIPアドレスがいつも0.0.0.xになるのですが、これがMastodon内部で行っているプライベートアドレスかのチェックに引っ掛かって例外を投げてしまうためです。

```patch:disable-private-address-check.patch
diff --git a/app/lib/request.rb b/app/lib/request.rb
index 397614fac..8227cb03d 100644
--- a/app/lib/request.rb
+++ b/app/lib/request.rb
@@ -101,7 +101,7 @@ class Request
   end
 
   def use_proxy?
-    Rails.configuration.x.http_client_proxy.present?
+    true
   end
 
   def block_hidden_service?
```

`.env.production`に以下を追加します。`http_proxy`は指定しません。

```text
ALLOW_ACCESS_TO_HIDDEN_SERVICE=true
```

#### サービス起動スクリプト

pumaとsidekiqの起動に、socksifyのラッパースクリプトを経由させるようにします。以下はサービス起動をsystemdユニットで行っている場合の例です。

```text:mastodon-web.service
ExecStart=/usr/bin/socksify /home/mastodon/live/bin/bundle exec puma -C config/puma.rb -b tcp://127.0.0.1:3000
```

あとは同じようにnginxの設定を行い、各種サービスを再起動させて完成です。……セグフォしなければ。

# 外部リンク

* [Darknet連合対応インスタンスの一覧](https://ja.mstdn.wiki/Darknet%E9%80%A3%E5%90%88%E5%AF%BE%E5%BF%9C%E3%82%A4%E3%83%B3%E3%82%B9%E3%82%BF%E3%83%B3%E3%82%B9%E3%81%AE%E4%B8%80%E8%A6%A7) — Tor対応が出来たら追加してみましょう。
* [Example of building Tor instance](https://gist.github.com/hcmiya/40f3810108c954b3a24017a78844e0b6) — この記事の前に書いたTorインスタンス構築法です。~~英語がダメなので~~最小限のコメントと必要なコマンドだけが列挙されているので最速でTorインスタンス立てたい人はこちら。但し、UNIXドメインソケットを積極的に使おうとしているのと、yarnのために野良リポジトリを導入したくないとの思いとでその周りで少々Production Guideとの差があります。