今回はAndroidに焦点を当てて、広告ブロックの話をしようと思います。
予め断っておきますが筆者はスマホよりPCをメインで使っていて、広告が出るスマホアプリをそう何種類も使っているわけではないため、この記事で紹介する方法の有効性について長期的・広範な実験はしていません。例えば消費電力などの面で問題がある可能性はあります。また後半の内容については慣れていないと設定がクソめんどくさいです。あくまで一応動いたというマニア向け実証実験としてお読みください。
NextDNSとは
NextDNSは広告ブロック(ドメインブロック)機能をもつセキュアなDNSサービスで、独自のDNSエントリの設定なども可能です。
月30万リクエストの制限がありますが、無料プランもあります。
類似サービスとしては、AdGuard(の機能であるAdGuard DNS)や後述のControl Dなどがあります。
そこそこ昔からあるサービスで、基本的な設定方法とかは他サイトにあると思います。
アプリごとに有効/無効を切り替え
NextDNSはただのDNSなので、Android自体に設定すると全アプリが対象になり、除外設定はできません。銀行など一部アプリはNextDNS有効だと不具合が出ることがあるらしく、また広告が出ないor気にならないアプリにもNextDNSを使うと月間30万の無料枠を圧迫することになります(いずれにせよ大半を占めるのはWebブラウザかとは思いますが)。そこで、アプリごとに有効/無効を切り替えたくなります。
記事の後のほう(トラフィック転送)ではもっと複雑な方法を使うのですが、アプリごとに切り替えだけであれば実は結構簡単な方法があります。それはRethink DNSというアプリを使う方法です。
このアプリはVPNとして動作し、NextDNSなどの任意のDNSを指定した上で、VPNを適用したい(またはしたくない)アプリを選ぶことができます。ちなみにRethink DNSアプリ自体にも広告ブロック機能(公開されているフィルタを適用する)はあります。
(追記)Rethink DNS以外だとpersonalDNSfilterというのもあるようです。日本だとおそらくこちらのほうが有名らしいです。
NextDNS自体にも昔は公式アプリがあったというような情報も見ましたが今はないようです。またNextDNS Managerという非公式(オープンソース)のアプリもあるようですがこれにアプリごとの切り替え機能があるかは不明です。
Control Dと、SNI Proxyによるトラフィック転送機能
Control DはNextDNSと似ていますが後発のサービスで、トラフィック転送機能というのがあります。これは、各Webサービスで使われているドメイン(Twitterだったらx.comやt.coなど)に関するDNSリクエストに対して、実際のIPではなくControl Dが管理しているプロキシサーバーのIPを応答し、そのプロキシサーバーがTwitterなどとの間を仲介することで、別の国などからTwitterにアクセスしているように見せかける、というものです。
もちろんプロキシサーバー側ではどのドメインに対するアクセスなのかを見分ける必要がありますが、これはHTTPならhostヘッダ、HTTPSならSNIを見ることで可能です。
目的は違いますが、やっていることとしてはよくあるリバースプロキシそのままです。SNIを使っているところに注目してSNI proxyとか呼ぶこともあるようです。ソフトウェアとしてはNginxやdlundquist/sniproxyなどがあります。
この記事ではControl Dみたいなことをやりたいので、自宅サーバーでNginxを動かしてSNI proxyをやらせることにします。さらに、実際に別の国に転送して動作を検証するため、Proton VPNの無料版を登録して自宅鯖からWireguardでつなげておきました(無料版は6か国使えるんですがその中でWireguardが使えたのは日本とオランダとアメリカのサーバーだけだったのでとりあえずオランダにしてみました)。
自宅サーバーがない場合も同様のことをするのは可能ですが、やや煩雑なので最後の方に書きます。
仕事用プロファイルにTailscaleを入れる
あとはNextDNSで自宅のプロキシサーバーのIPを応答するよう設定すれば完成…といきたいところですが、実際にはそんなサーバーを全世界に公開したら何をされるかわかったものではありません。そこで、TailscaleのようなVPN経由で自宅サーバーにアクセスしたくなります。
ここで大きな問題になるのが、原則としてAndroidではPC用のOSと違ってVPNを同時に1個までしか動かせないということです。しかしこれを回避する方法があり、それは仕事用プロファイル機能を使う方法です。
これはAndroidの内部に仮想的に2つ目の環境を作ってくれるもので、VPN含めて各種アプリを独立して2つ入れられるようになります。さらにネットワーク環境は共有されているので相互通信は可能です。
欠点として、仕事用プロファイルは1つしか作れません。つまり同じアプリは2個までしか入れられません。既に仕事用プロファイルを使っていてこれ以上いじれないといった場合はこの記事の方法をそのまま使うのは難しくなりますが、最後の方に対応策を書いておきます。
で、仕事用プロファイルを作る方法についてですが、Shelterというアプリを使うのがとりあえずは良さそうだったのでそれをF-Droid(なんかこういう変な便利ツールがたくさん入ってる自由なPlay Storeみたいなやつ)から入れました。
Tailscaleもインストールしておきましょう。元のプロファイルにもTailscaleを入れているかもしれませんが、別デバイスとして登録されます。
さらにこのTailscaleのネットワーク(tailnet)上で自宅サーバーが例えば192.168.1.1としてアクセスできるように設定しておきます(別にサブネット割り当てじゃなくて普通の100から始まるtailscaleのアドレスでも構いません)。
自宅サーバー見えない問題
あとはこのTailscale経由のIPをNextDNSに設定すれば今度こそ完成…と思うかもしれませんが、大きな問題があります。
2つのプロファイル間では、ネットワークが共有とはいえ、(VPNを独立に入れられることからもわかるように)ルーティングなどは別なので、メインのプロファイルの側でTailscale上の自宅鯖のIPを使っても通信が通りません。
となると思いつくのはポート転送ですが、特権ポートである80番や443番をリッスンするにはroot化が必要になります。DNSでポート番号まで指定できればいいのですが、それもできません。
このままではメイン側からSNIプロキシにアクセスさせる設定ができません。
SocksTunとTermuxを入れる
しょうがないので、仕事用側でポート転送したうえで、メイン側で透過SOCKSプロキシを使って強制的にアクセス先ポートを書き換えることにします。
透過SOCKSプロキシクライアントに関しては透過プロキシを用いて特定アプリケーションのTCP・UDP通信をSOCKS5経由にする方法(Windows・Linux(iptables TPROXY)・Androidなど) - turgenev’s blogに詳しく書いたのですが、AndroidだとSocksTunというアプリがあります。また、アプリごとに有効/無効を切り替えられます。F-Droidからインストールできます。(ちなみに、実はRethinkDNSでも同等の設定は可能です)
SOCKSプロキシサーバーについては、勝手に中身を書き換えるような機能が付いてるアプリがあるならそれでいいのですが、まあそんな変なものは無さそうだった(そもそも普通のSOCKSでさえロクにない)ので、自分でなんとかするしかありません。
Androidアプリ開発なんてやったことなくてダルそうですが、幸い、Termuxを入れればいろんな言語が普通に動かせるようになります。これもF-Droidで入れましょう。
SOCKSプロキシによるポートの書き換え
SOCKSプロキシとしては、DNSも扱うため、UDP Associateに対応したものが必要です。今回は過去にも自分でいじったことがあって必要十分な機能があるGo製のthings-go/go-socks5を使うことにしました。変更後のものはge9/go-socks5/tree/mitmに置いてあります。
具体的には、例えばNextDNSでexample.comを192.0.2.103(このIPアドレス範囲はテスト用に予約されていて、実際には使われない)に解決するよう設定した上で、TCPの192.0.2.103:80(HTTP)を127.0.0.1:19103、192.0.2.103:443(HTTPS)を127.0.0.1:19203に書き換える、といった感じにしました。(ポート番号は適当ですが、エフェメラルポートとして使われない(多分)32767以下のほうがわかりやすいと思います)
この127.0.0.1:19103とかをどうするかは後にします。
トラフィック転送先を複数(例えばオランダに加えてアメリカとか)設ける場合は、example2.comを192.0.2.102に解決するようにしたうえで、192.0.2.102:80を127.0.0.1:19102、192.0.2.102:443を127.0.0.1:19202に書き換えるといったようにIPとポートをそれぞれ変えます。
go-socks5はSOCKS5のソフトウェアというよりはライブラリなのでコマンドラインオプションなどはろくにありませんが、そのかわり_example/main.goにちょっとした使用例が入っているのでこれを使います(go run _example/main.go)。ただしもともとのmain.goでは":10800"とワイルドカードアドレスをlistenしていて、グローバルIPが普通に降ってくるスマホでこれをやるのは当然危険なので自分のforkではlocalhost:10800に変えてあります。
DNSのポートの書き換え
さらに、SocksTunでは普通のUDP53番のDNSしか指定できずNextDNSが使えないので、同様の発想で、DNS通信の書き換えもSOCKSプロキシで行うことにしてしまいます。具体的には、SocksTunではDNSを8.8.8.8(実際には使わないので何でもよい)に設定し、go-socks5側で8.8.8.8:53へのUDP通信の行き先を127.0.0.1:10053とかに変えます。
そして、この127.0.0.1:10053でDNSプロキシを動かし、来たDNSリクエストをNextDNSに投げることとします(もちろんgo-socks5を変えてDNS over HTTPSとかをできるようにしてもいいわけですがそれは無駄に手間がかかる)。
コマンドとしては、一番有名そうなhttps://github.com/AdguardTeam/dnsproxyだと
dnsproxy -l 127.0.0.1 -p 10053 -u https://dns.nextdns.io/xxxxxx/my-android --http3
gostだと
gost -L dns://localhost:10053/https://dns.nextdns.io/xxxxxx/my-android
のような感じです。(単なるDNSなのでSOCKSより危険性は低いかと思いますが、localhostにしておくのを忘れずに)
トラフィック転送以外の部分はこれで完成しているので、go-socks5とSocksTunもあわせて動かせば、対象アプリで正しく通信(+広告ブロック)ができるはずです。
仕事用プロファイル側でポート転送
メイン側でやることは終わったので、仕事用プロファイル側に戻ります。
こちらではTailscale経由で自宅鯖のSNIプロキシが見える状態まで来ていました。
あと必要なのは、先ほどの設定に合うように、ポート転送を使って、127.0.0.1:19103のようなポートで(メイン側から)このSNIプロキシが見えるようにすることです。
で、ただのポート転送なんですが、これも意外と厄介です。
Termuxが入れられれば簡単にできますが、Termuxはどうやらメインのプロファイルでないと動かないようです。そうでなくてもたかがポート転送のためにTermuxを持ち出すのは牛刀をもって鶏を割くような感があります(それ言ったらSOCKSもそうですが…)。
なのでアプリを探すしかありません。見つかったのはPort ForwarderとFwd: port forwarderでした。
前者は、ちゃんと動きましたが、なぜかリッスンするアドレスの候補にlocalhost(ループバックデバイス)が出てこないという意味わからない欠点があります。かといって0.0.0.0でリッスンするわけにもいかないので、TailscaleかSocksTunのIPを使うことになりますが、TailscaleのIPはデバイスによって異なるのでコードにベタ書きするのは避けたく、SocksTunは(対象アプリを追加・削除するために)そこそこの頻度で再起動することがあり、そのたびにポート転送をやり直すのは不便です。
後者は長らく更新されておらず、Play Storeのものは自分のAndroid 15ではもう動かなかったのですが、幸いソースがGitHubで公開されているのでそのforkをたどって(途中、広告を消しましたみたいなコミットが入っているのが趣深い…)なんとかビルドして動かすところまでこぎつけました。ついでにいくつか微修正しました。自分のリポジトリはこちらです。apkも置いておきました。こちらはlocalhostをリッスンでき、TCPだけでなくUDPに対応していて、設定のImport/Exportもできるのでやや高機能です。ただ、まだ古いコードが残っているので、「起動時にポート転送を開始する」みたいなオプションとかは多分チェック入れても動かない状態だと思います。気が向いたらそのうち直すかもしれません。
SNIプロキシとポート転送の具体的な設定
ではSNIプロキシへのポート転送の具体的な設定に移ります。
記事の最初のほうでNginxでSNIプロキシをやると言いつつその設定を書いていなかったのでまずはそれを書きます。
load_module /usr/lib/nginx/modules/ngx_stream_module.so;
events {
worker_connections 1024;
}
pid /tmp/nginx-sni.pid;
http {
access_log off;
server {
listen 19102;
location / {
resolver 127.0.0.53;
set $target http://$host;
proxy_pass $target;
proxy_set_header Host $host;
}
proxy_bind 10.0.0.1;
}
server {
listen 19103;
location / {
resolver 127.0.0.53;
set $target http://$host;
proxy_pass $target;
proxy_set_header Host $host;
}
proxy_bind 10.0.0.2;
}
}
stream {
access_log off;
server {
listen 19202;
resolver 127.0.0.53;
proxy_pass $ssl_preread_server_name:443;
ssl_preread on;
proxy_bind 10.0.0.1;
}
server {
listen 19203;
resolver 127.0.0.53;
proxy_pass $ssl_preread_server_name:443;
ssl_preread on;
proxy_bind 10.0.0.2;
}
}
こんな感じでHTTPとHTTPSで同等の設定をします。特権ポートを使わないのでsudoがなくても動きます。bind addressを指定していてこれでトラフィックの送信先を決めています。つまり、例えば10.0.0.2はオランダ経由のVPNのアドレスということです。
書いてから思ったのですが汎用のサーバーであるNginxだとこうやって無駄に長くなるので気軽に試すなら普通のSNI proxy専用のもののほうがいいと思います。ameshkov/sniproxyなんかはbind addressじゃなくてforward proxyとして別のHTTPやSOCKS5のプロキシを指定できたりもするみたいです。
- sniproxyを試してみたのですが、DNS応答を書き換える部分と一緒くたになったソフトウェアで、上記のnginxと同じように使うにはどうすればいいかわかりませんでした。gostならgost -L sni://:19203 -F http://user:password@proxy.example.com:3128 みたいな感じでできました。やはりgostは便利ですね。NginxやHAProxyでも上流のHTTP/SOCKSプロキシを使うような機能がカスタムの外部モジュール(パッチ)とかfeature requestとかで出ていたりはするんですが安定版リリースでそのまま動くようなものは無さそうです。
これを動かせば、仕事用プロファイルから192.168.1.1:19103とかでSNIプロキシが見えている状態になるはずです。
あとはさっきのポート転送アプリで、
127.0.0.1:19103 → 192.168.1.1:19103
127.0.0.1:19203 → 192.168.1.1:19203
みたいなポート転送を設定して有効化します。これでようやく完成です。
実際にNextDNSでicanhazip.comとかugtop.com(←IPアドレスを教えてくれる)のIPを192.0.2.103に設定してみたりして、オランダのIPが返ってくるかどうか確かめましょう。Chromeだとうまくいかないかもしれないので次を読んでください。
Chromeが勝手にdns.googleを使う問題への対策
Google Chromeは、何も指定していなくても勝手にdns.googleとDNS over TLSで通信してドメイン解決をしようとすることがあり、このせいでNextDNSが使われず意図した通りにトラフィックが転送されないことがありました。そこでNextDNSでdns.google自体を拒否リストに入れると、(数分後に)問題は解決しました。
WebRTCについて
基本、だいたいの通信はドメイン解決から始まりますが、Discordの通話のようなWebRTC系のアプリは音声サーバーの(ドメインではなく)IPをやりとりしてUDP通信をすることがあるようです。そうなるとドメインだけで全ての通信の行先を制御するのは難しいかもしれません(とはいえ全く試していません)。
他にもこういった例があるかもしれません。
それと余談として、UDPが通らない環境でDiscordとかを使いたい場合は、(今回の記事の設定を前提とすると)仕事用プロファイル側に(も)Discordを入れておけばTailscale経由でいつでも通話ができるようになります(TailscaleではExit Nodeを有効にしておいてください)。
AndroidのVPNの自由度
ここまで、SOCKS5を使って無理やりポートを書き換えてアクセスを振り分けるというかなり邪道なやり方を説明してきました。
ところで、アプリごとに別のSOCKS5サーバーを指定できるようにすれば(SocksTunの実装を変えれば)いいのでは?そうすればもうちょっと簡単にルーティングできるのでは?と思った方もいるかもしれません。
自分もそう思ったのですが、AndroidのVPNの仕様上、各アプリをVPNの適用対象にするかどうか決めることはできても、来たパケットがどのアプリ由来のものか判別することはできないようです。
(訂正)できるかもしれません。Is there a way to see which app sent data to Android VPN Service - Stack Overflow
(さらに追記)なんならRethink DNSがまさに↑(リンク先にて言及されている)のgetConnectionOwnerUid()関数を使っているようです。GitHub - celzero/rethink-app: DNS over HTTPS / DNS over Tor / DNSCrypt client, WireGuard proxifier, firewall, and connection tracker for Android. 調査不足で失礼しました。
透過的なトラフィック転送ではなく普通にプロキシとして使用する
この記事では特にプロキシ設定などがない任意のアプリの通信を透過的に(強制的に)転送する方法を紹介しましたが、プロキシ設定があるアプリであれば、この記事のようにポート転送を使うことでメインのプロファイル側からでも自宅鯖(あるいはその先の任意のVPN)経由で通信ができます。このようなアプリはSocksTunの対象にする必要もありません。
例えばFirefoxなんかはAndroidアプリでもProxy SwitchyOmegaのような拡張機能が入れられるのでドメインごとにプロキシの設定が可能です。
自宅サーバーが無いまたはTailscaleが使えない場合
この記事では自宅サーバーにTailscaleで接続する前提で説明しましたが、これが満たせない場合もあります。自宅サーバーがないならTailscaleを使う意味はないですし、仕事用プロファイルにTailscaleを入れられなければ自宅サーバーは使いづらくなるので、この2つへの対策をまとめて説明します。
まず、前述の通りSNIプロキシ自体は(特権ポートを使わなければ)root権限を必要としないのでAndroidで動かす自体は何も難しくありません。なので残る問題としてはオランダとかへのVPNをどうやって張るの?というところですが、wireproxyのようなソフトウェアを使えばWireguardの暗号化を施したSOCKS5プロキシとして動作させるのはroot化なしで普通に可能です(あるいは自宅サーバーまでwireproxyを張るという手もあります)。あとはこのSOCKS5を指定できる前述のameshkov/sniproxyのようなSNIプロキシを使えばいいです。Android内でSNIプロキシまで動かすとなると電池を使いそうな気がしますが、そのかわりTailscaleとポート転送は不要になります。自宅サーバーを使わないことで、自宅サーバーまでの通信環境に縛られなくて済むというメリットもあります。
ただし、UDPがブロックされているような環境だと当然Wireguardサーバーに接続できなくなってしまいます。Tailscaleはそういう環境でもTCP443番にフォールバックしてくれるからこそ価値があるというわけです。
ちなみに、Tailscale自体にもWireproxy的な動作であるUserspace networking modeというのがあるのですが、これはAndroidだとサポートされていません(そもそもtailscaleコマンドがない)。
TailscaleでNextDNSを使う案について
記事では触れませんでしたがTailscaleの中でNextDNSをドメイン解決に使用するよう設定することも可能です。Tailscaleアプリにもsplit tunnelingは実装されているのでReThinkDNSと同様に一部アプリだけ対象にでき、かつポート転送を使わなくても普通にTailnetにある自宅鯖のSNIプロキシにつながります。
と、いいことばかりのように思えますが、Exit Node(基本そこまで使いたくはない)を使わずにTailscaleを有効にしていると、キャプティブポータルのあるフリーWi-Fiに接続したときに(接続を完了するまで)インターネットが使えなくなるという問題(おそらくTailscaleというよりはAndroidのVPN自体の問題)があります。
というのと自分があまりTailscale DNSの趣旨をよくわかっていないのかもしれませんが、TailscaleでNextDNSを指定すると全てのTailscaleデバイスがNextDNSを使うようになる(?)(参考: FR: Configure nextDNS on a per device basis using ACLs · Issue #11951 · tailscale/tailscale · GitHub)というのがなんかコレジャナイ感があります。
追記①Ad-Shield対策
一部の手の込んだ広告はドメインブロックだけだと検出されて閲覧を妨害するスクリプトが実行されます。この記事の後に書いた【広告ブロックDNS】特定ドメインへのTCP接続を途中で切断してAd-Shield対策 - turgenev’s blogと組み合わせるとこういったサイトへの対処が可能です。
追記②仕事用プロファイルの不具合?
しばらく実際に自分のAndroidスマホでこの設定をして運用していたのですが、明らかにバッテリーが今までにない勢いで減る事象が時折発生するようになりました。使用量を見た感じ、仕事用プロファイル側のTailscaleが使っていることが多く、場合によりポート転送アプリが10%を超えるようなこともありました。トラフィック転送を適用しているTwitterがバックグラウンドで起動していなければある程度抑制できるようにも見えましたがそれでも時折発生しているような気もしました。
原因がわからないのですが、とりあえず電池が減るのは困るので仕事用プロファイルは削除し、やむをえず「自宅サーバーが無いまたはTailscaleが使えない場合」のように設定し、UDPが通らない環境でも動くように自宅のTCP443番にLet's Encrypt付きのパスワードありのHTTPプロキシを建ててトラフィック転送をするように切り替えました。
まとめ
長くなりましたが以上です。結構端折ったのでわからないところがあればコメントお願いします。