特定アプリケーションのTCP・UDP通信を透過的なSocks5プロキシ経由にする方法(Windows・Linux・Androidなど)

概要

ネットワーク上の制約を回避するためにプロキシやVPNのような類のものを使用するにあたって、(必ずしもそれ自体ではプロキシなどに対応していない)特定のアプリケーションを対象に、またTCPだけでなくUDPにも対応したいということがあると思います。

特に「個別アプリケーション対象」「UDP対応」の2条件を満たすのは結構難しいので、そこを中心にやり方を紹介しようと思います。

SOCKS5プロキシ

SOCKSというのはHTTPプロキシなどと同じくプロキシの種類の1つで、ssh -D(ダイナミックポートフォワード)で使用されていることでも知られています。TCPポート上で動作するプロトコルです。

このSOCKSプロトコルのバージョン5(=SOCKS5)から、UDP Associateという、UDP通信をプロキシするための仕組みが追加されました。UDP Associateの基本的な仕組みは、UDP通信を仲介するための(プロキシサーバー自身の)ポートをクライアント側に通知し、クライアント側がそこに送信したUDPパケットをプロキシサーバーがかわりに本来の宛先に送信するといった感じになっているようです。したがって、クライアント側からはプロキシサーバーのTCPポートだけでなく、おそらくエフェメラルポート範囲全て(厳密にはよく理解できていません)UDPポートが見えている必要があります。ここが状況によっては結構ハードルが高い部分かと思いますが、ここに関してはVPNでなんとかすることを想定しています。

注意すべき点として、SOCKS5プロキシとして利用可能なものすべてがこのUDP Associateをサポートしているわけではなく、例えば上記のssh -Dでは使えません。

この記事ではUDP Associateまでサポートしている代表的なSOCKS5プロキシとしてdanteを主に使用します。他には3proxyというのもあり、こちらはWindows版のバイナリが提供されています。

3proxyはAndroid(termux)でもそのままコンパイルが通り、TCP/UDPともにちゃんと動きます。danteは、./configureに--build=armを付けます。makeするとまずbacktrace_symbols()が見つからないというエラーがでて、pkg search backtraceとかするとlibandroid-execinfoが出てくるのでそれを入れるとよいです(libandroid-execinfoを入れた後はmake distcleanして./configureからやり直す必要がありそうです)。次にsockd/mother_util.cでwait3関数がないというエラーが出るのでこのへんに書いてあるようにwaitpid(-1, &status, WNOHANG)と書き換えるとコンパイルが通ります。しかしそのままだとBad system callで即落ちます(Android 14で発生、8で発生せず)。printfデバッグの結果、sockd/privileges.cにあるif文のsetegid(sockscf.uid.unprivileged_gid)で落ちているようだったのでこのif文全体をコメントアウトしたら(この記事で扱う範囲では正しく)動くようになりました(その直後にあるよく似た式seteuid(sockscf.uid.unprivileged_uid)はそのままでも大丈夫だった)。 

dante・3proxyの設定

/etc/dante.confの内容(最低限の内容のみ載せています)は以下のような感じです。

internal: eno1 port=3129
external: eno1
internal: eno2 port=3129
external: eno2
external.rotation: same-same
udp.connectdst: no
socksmethod: none
clientmethod: none
client pass {
  from: 0.0.0.0/0  to: 0.0.0.0/0
}
socks pass {
    from: 0.0.0.0/0 to: 0.0.0.0/0
  protocol: tcp udp
    command: bind connect udpassociate bindreply udpreply
}

「udpassociate」という文字列があるのがわかります。見ての通りセキュリティは何もないので、必要に応じて追加してください。

外部アドレス(出ていくために使用するアドレス)の設定などについて詳しくはIPoE/PPPoE併用時(など)に一つの端末から同時に複数の接続経路を利用する - turgenev’s blogを参照してください。3proxyの設定ファイル例もここに載っています。むしろ3proxyのほうが簡単かもしれません。

udp.connectdst: noについては後述しています。

TCPに関するsocks5の動作確認だけならcurlなどで簡単にできるので、SOCKS5の設定に慣れていなければこの段階でやっておくのを推奨します。

今回は、192.168.31.2:3129というポートにおいてSOCKS5サーバーが稼働しているとして話を進めます。

また、重要な注意点として、UDP AssociateではUDP通信に使用するアドレスとポートがSOCKS5サーバーからクライアント側に通知されますが、これはあくまでSOCKS5サーバーから見たときのアドレスとポートです。NATが介在するなどして、SOCKS5サーバーがリッスンしていないアドレスにおいてSOCKS5が利用可能になっているような場合、クライアント側は誤ったIPにむけてUDPパケットを送信してしまい、UDP Associateはうまく動作しません。(ちゃんとtcpdumpで外向きパケットを監視していればよかったのですが)ここで2日ほどハマったので注意してください。ただし実は(今回紹介する)Windowsでの方法ではIPが一致していなくても動きます(さすがにポートまで変わると動かなさそうですが)。

socks5プロキシ自体には管理者・root権限は不要です。

SOCKS5を透過プロキシとして利用するための設定: Windowsの場合

SOCKS5サーバーを単体で動かすだけでは、まだ透過的なUDP通信はできません。任意のUDP通信をキャッチしてSOCKS5プロキシとやりとりを行う部分にはまた別のソフトウェアが必要になります。

Windowsの場合は、ProxiFyreというのが利用可能です。2023年に公開された新しいOSSですが、かなり良い感じです。

インストールのしかたはREADMEの通りで、「Windows Packet Filter (WinpkFilter)」というのを入れてから、ProxiFyre本体を解凍するだけです。設定ファイルも簡単なのでほとんど迷うところはありません。

ProxiFyreが起動している間は、設定ファイルに記載された文字列に一致する実行ファイルからの通信はSOCKS5プロキシを経由するようになります。ProxiFyreの起動前から実行されていたプログラムでもプロキシ経由になります。ProxiFyreはWindowsのサービス(≒デーモン)として動作することもできます。

初回起動時にダイアログが出るかと思いますが、ProxiFyre.exeをファイアウォールの許可設定に追加するのを忘れないようにしてください。そうしないと正しく動きません。

あと、3proxyを認証無しで使うと動かない不具合があります(報告済み: #27)(←に載せた3proxyの行をhavepass = 0に変えればとりあえず動く)修正されました。

SOCKS5を透過プロキシとして利用するための設定: Linuxの場合

Linuxの場合は、Windowsの場合よりむしろ難しく、ソフトウェア自体の設定に加えてLinux側でnetfilter(iptables/nftables)関連の設定を行う必要があります。というよりはnetfilterで設定ができるからこそWindowsでのProxiFyreのような全部やってくれるソフトが無いということかもしれません。

ソフトウェアとしては(試した限り)redsockshev-socks5-tproxyが利用可能です。redsocksのほうは数年間コミットがありませんが、semigodking/redsocksというメジャーなforkがあり、こちらではshadowsocksのサポートなどいくつかの拡張が追加されているようです。基本的にはforkのほうをおすすめします。

(古いほうの)redsocksであれば、以下のようなredsocks.confを用意します。

base {
  log_debug = on;
    log_info = on;
    log = stderr;
    daemon = off;
    redirector = iptables;
}

redsocks {
    local_ip = 127.0.0.1;
    local_port = 22222;
    ip = 192.168.31.2;
    port = 3129;
    type = socks5;
}

redudp {
    local_ip = 127.0.0.1;
    local_port = 22224;
    ip = 192.168.31.2;
    port = 3129;
    udp_timeout = 10;
}
 

udp_timeoutはもっと短く2秒くらいでも良さそうです(どうせクライアント側がそんなに長時間待ってくれないので)。semigodking/redsocksでは少し文法が変わって、

    local_ip = 127.0.0.1;
    local_port = 22222;
    ip = 192.168.31.2;
    port = 3129;

のところが

    bind = "127.0.0.1:22222";
  relay = "192.168.91.12:3129";

となります。

hev-socks5-tproxyでは以下のように設定すると同等の内容になります。

misc:
  log-file: stderr
    log-level: debug

socks5:
  # Socks5 server port
  port: 3129
  # Socks5 server address
  address: 192.168.31.2
  # Socks5 UDP relay mode (tcp|udp)
  udp: 'udp'
  # Socks5 handshake using pipeline mode

tcp:
  # TCP port
  port: 22222
  # TCP address
  address: '::'

udp:
  # UDP port
  port: 22224
  # UDP address
address: '::'

Socks5サーバーのアドレスはまとめて一箇所で指定されています。機能が少ない分、シンプルではあります。「Socks5 UDP relay mode (tcp|udp)」については、ここを変えるとUDP over TCPでやってくれるのかなと思いますが、試した限りでは'udp'でないと動きませんでした。

これで、127.0.0.1の22222番でTCP用、22224番でUDP用の透過プロキシが動作するようになります。どちらもデバッグ出力を有効にしていますが、うまく動いたらもちろん無効にしていいです。

透過プロキシはおそらく生のパケットを生成する都合上、起動にroot権限が必要になります。ただ、hev-socks5-tproxyのREADMEを見ると、どうやらsetcapというコマンドで個別に権限を与えておけばプログラム自体をrootで起動する必要はないようです。

Linuxの場合: iptablesやrouteの設定

では、上記の127.0.0.1上のポートにTCP/UDPの通信を流し込むためにiptablesやipコマンドでの設定を行います。

特に、不特定多数の宛先を対象としたUDPでの透過プロキシには、iptablesのTPROXYというターゲットを使用する必要があり、これはmangleテーブルのPREROUTINGチェインでしか使用できないという特徴があります。つまり、他のコンピュータから転送されてきたパケットにしか適用できないということです。実はそのコンピュータ自体からのパケットにも適用する方法はあるのですが、説明の都合上後回しにします。

また、TCPではTPROXYを使うよりもっと簡単な方法がありますが、semigodking/redsocksとhev-socks5-tproxyはTPROXYでも動かせるので、ついでに設定してみることにします。Support TPROXY for TCP connections · Issue #97 · darkk/redsocks · GitHubの通りオリジナルのredsocksでは動きませんでした。

ということで、以下の内容をテストするために、また別のコンピュータ(場合によってはスマホなどでも可)を用意する必要があります。手元になければVPN経由、あるいはLinuxで一つのパケットに2回(複数回)NATをかけるための2つの方法 - turgenev’s blogで紹介したようにnetnsやダミー的なtunデバイスを利用するのでも構いません。ここでは、その別のコンピュータのIPアドレスが192.168.1.90であるとします。

また、確認用のコマンドとして今回は主にSTUNプロトコルのクライアント(具体的にはStuntman)を使用したため、宛先ポート範囲を3478:3479に限定しています。疎通確認の方法は何でもいいのですが、雑に指定するとプロキシ用の通信がプロキシに向かうような循環が生じて通信できなくなる場合があるのである程度限定的にやっていくのが良いです。まあ今回の例ではそもそも別の端末なので限定しなくても循環は起こりませんが。ちなみに、stunclient自体は特にUDPの疎通確認ツールとして手軽に使いやすく、アドレス・ポートの情報も得られるのでかなりおすすめです(詳細はNATタイプ、ポートセービングIPマスカレード、UDPホールパンチング、STUN - turgenev’s blogにも書きました)。

まず、以下のようなiptablesコマンドを実行します。

sudo iptables -t mangle -A PREROUTING -p tcp -s 192.168.1.90 --dport 3478:3479 -j TPROXY --on-ip 127.0.0.1 --on-port 22222
sudo iptables -t mangle -A PREROUTING -p udp -s 192.168.1.90 --dport 3478:3479 -j TPROXY --on-ip 127.0.0.1 --on-port 22224

先ほどの透過プロキシのアドレスを指定しています。送信元アドレスも192.168.1.90に限定しています。

一見これだけでも動きそうですが、実はこのままだと192.168.1.90からの通信が外部インターフェイスに向かってFORWARDされてしまい(?)うまくTPROXYが適用されません。あまり理解できていませんが、TPROXYに関するmanページなどにもそういう感じのことが書かれています。

どうすればいいかというと、TPROXYを適用したいパケットはループバックインターフェイス(lo)に向かうようにルーティングを設定する必要があります。

まず、いつものようにrt_tablesなどを編集して新しいルーティングテーブル(ここでは100とします)を追加します。

ここに次のようにしてrouteを追加します。

sudo ip route add local default dev lo table 100

さらに192.168.1.90からの通信がこのテーブルを使用するようにします。

sudo ip rule add from 192.168.1.90 lookup 100

これで、TPROXYが正しく機能するようになるはずです。

外部インターフェイスからloに転送しているのでip_forwardを有効にする必要がありそうですが、試したかぎり無効(=0)のままでも動くようです。あと多分最近の普通のLinuxなら大丈夫だと思いますが、TPROXYに関するモジュールがロードされている(lsmodでxt_TPROXYが表示されればOK?)必要があります。

ファイアウォールに関しては、ufwが有効だとufw-not-localというチェインに引っかかって、INPUTである(自分自身に送られてきている)にもかかわらず自分自身以外を宛先としている怪しいパケットとしてDROPされてしまうので、sudo iptables -I ufw-not-local -s 192.168.1.90 -j ACCEPTとかやっておく必要があります。普通にufw (route) allow 192.168.1.90としても効果がないので注意が必要です。他のファイアウォールでは確認していませんが同様の問題が発生する可能性があるのでまずは無効にしてテストすることを勧めます。route_localnetやrp_filterはデフォルトのまま(それぞれ0、2)で問題ありません

また、この例ではip ruleもiptablesも192.168.1.90から来ているという条件を指定していて(もっと言えばip ruleのほうで3478-3479に限定していないのもかなり雑)、二重管理っぽいきらいがありますが、実際には--tproxy-markを使って付けたfwmarkをもとにloにルーティングすることも可能です。例えば以下のようにします。

sudo iptables -t mangle -A PREROUTING -p tcp -s 192.168.1.90 --dport 3478:3479 -j TPROXY --on-ip 127.0.0.1 --on-port 22222 --tproxy-mark 0x1
sudo iptables -t mangle -A PREROUTING -p udp -s 192.168.1.90 --dport 3478:3479 -j TPROXY --on-ip 127.0.0.1 --on-port 22224 --tproxy-mark 0x1
sudo ip rule add fwmark 0x1 lookup 100

ruleのほうは常時有効にしておいてiptablesのほうをon/offすれば挙動を変えられるので、こっちのほうがテストしやすいかもしれません。

正常動作時(この例ではUDP)のtcpdumpは以下の通りです。わかりやすいように3proxyを127.0.0.3で動かしています。loに向かうようにしているものの、tcpdumpiptablesより外側(つまり全体としてかなり外側)でパケットの様子を見ているのでenp3s0から入ったことになっています。これでちゃんと22224番に送られているのですが、22224番はどこにも現れないのが厄介なところです。パケットがtproxy側に届いているかを判断するには(あくまで筆者の知る限り、ですが)先ほどのようにtproxy側でデバッグ出力を有効にするしかありません。2行目以降しばらくはhev-socks5-tproxy(127.0.0.1:51066)と3proxyの間のTCP接続でUDP associateリクエストが交換され、3proxyのポート(今回なら127.0.0.3:39410)が通知されています。hev-socks5-tproxyはUDPで(127.0.0.1:44277から)そこにUDPパケットを送り、3proxyが外部インターフェイス(192.168.1.9)からそれを出します。(その次のTCPのackはあまり気にしなくて良さそうです。)帰ってきた応答は今までとそっくりそのまま逆のルートで192.168.1.90に返されています。

09:18:24.316021 enp3s0 In  IP 192.168.1.90.42874 > 3.132.228.249.3479: UDP, length 28
09:18:24.316771 lo    In  IP 127.0.0.1.51066 > 127.0.0.3.3130: Flags [S], seq 3104424580, win 65495, options [mss 65495,sackOK,TS val 591900081 ecr 0,nop,wscale 7], length 0
09:18:24.316810 lo    In  IP 127.0.0.3.3130 > 127.0.0.1.51066: Flags [S.], seq 1895898628, ack 3104424581, win 65483, options [mss 65495,sackOK,TS val 1315171637 ecr 591900081,nop,wscale 7], length 0
09:18:24.316842 lo    In  IP 127.0.0.1.51066 > 127.0.0.3.3130: Flags [.], ack 1, win 512, options [nop,nop,TS val 591900081 ecr 1315171637], length 0
09:18:24.316970 lo    In  IP 127.0.0.1.51066 > 127.0.0.3.3130: Flags [P.], seq 1:4, ack 1, win 512, options [nop,nop,TS val 591900081 ecr 1315171637], length 3
09:18:24.317006 lo    In  IP 127.0.0.3.3130 > 127.0.0.1.51066: Flags [.], ack 4, win 512, options [nop,nop,TS val 1315171637 ecr 591900081], length 0
09:18:24.317145 lo    In  IP 127.0.0.3.3130 > 127.0.0.1.51066: Flags [P.], seq 1:3, ack 4, win 512, options [nop,nop,TS val 1315171637 ecr 591900081], length 2
09:18:24.317176 lo    In  IP 127.0.0.1.51066 > 127.0.0.3.3130: Flags [.], ack 3, win 512, options [nop,nop,TS val 591900081 ecr 1315171637], length 0
09:18:24.317295 lo    In  IP 127.0.0.1.51066 > 127.0.0.3.3130: Flags [P.], seq 4:14, ack 3, win 512, options [nop,nop,TS val 591900081 ecr 1315171637], length 10
09:18:24.317469 lo    In  IP 127.0.0.3.3130 > 127.0.0.1.51066: Flags [P.], seq 3:13, ack 14, win 512, options [nop,nop,TS val 1315171637 ecr 591900081], length 10
09:18:24.317803 lo    In  IP 127.0.0.1.44277 > 127.0.0.3.39410: UDP, length 38
09:18:24.317938 enp3s0 Out IP 192.168.1.9.49280 > 3.132.228.249.3479: UDP, length 28
09:18:24.358632 lo    In  IP 127.0.0.1.51066 > 127.0.0.3.3130: Flags [.], ack 13, win 512, options [nop,nop,TS val 591900123 ecr 1315171637], length 0
09:18:24.489589 enp3s0 In  IP 3.132.228.249.3479 > 192.168.1.9.49280: UDP, length 68
09:18:24.489823 lo    In  IP 127.0.0.3.39410 > 127.0.0.1.44277: UDP, length 78
09:18:24.490063 enp3s0 Out IP 3.132.228.249.3479 > 192.168.1.90.42874: UDP, length 68

Linuxの場合: PC自体の通信にTPROXYを使用する

前述の通り、TPROXYはPREROUTINGチェインでしか使えないため、そのままではPC自体で生成された通信(OUTPUTチェインなどで処理される)には適用できません。

しかし、実際には、先ほどと同様にループバックデバイス(lo)を使って外向きの通信を強制的に自分自身に送り付けることで、自分自身から送られてきたパケットにPREROUTINGチェインを適用することができます。

ただし、PC自体の通信をloに向けるとなると場合によっては前述のような循環が発生してしまうので、アドレス指定やmarkを使用するなどして回避する必要があります。

ここでは最も簡単な設定例として、loに(他のデバイスでも構いませんが)適当なプライベートアドレスを割り当てて、そこから出ていくものだけをloに向けるように設定します。結果的に先ほどとほとんど同じようになります。

まずアドレスの割り当てを行います。

sudo ip addr add 192.168.5.5 dev lo

そしてこの192.168.5.5を対象に同じようなip ruleとiptablesのルールを設定します。

sudo ip rule add from 192.168.5.5 lookup 100
sudo iptables -t mangle -A PREROUTING -p tcp -s 192.168.5.5 --dport 3478:3479 -j TPROXY --on-ip 127.0.0.1 --on-port 22222
sudo iptables -t mangle -A PREROUTING -p udp -s 192.168.5.5 --dport 3478:3479 -j TPROXY --on-ip 127.0.0.1 --on-port 22224

結局アドレス以外何も変わっていませんね。こちらも別にポートを3478-3479に限定するのはやらなくてもいいです。これで192.168.5.5から出ていくパケットがTPROXYを使うようになります。

こちらも正常動作時のパケットキャプチャを載せておきます。

10:13:38.897424 lo    In  IP 192.168.5.5.48202 > 3.132.228.249.3479: UDP, length 28
10:13:38.897566 lo    In  IP 127.0.0.1.60698 > 127.0.0.3.3130: Flags [S], seq 188947714, win 65495, options [mss 65495,sackOK,TS val 841486027 ecr 0,nop,wscale 7], length 0
10:13:38.897596 lo    In  IP 127.0.0.3.3130 > 127.0.0.1.60698: Flags [S.], seq 1277796748, ack 188947715, win 65483, options [mss 65495,sackOK,TS val 3344603128 ecr 841486027,nop,wscale 7], length 0
10:13:38.897623 lo    In  IP 127.0.0.1.60698 > 127.0.0.3.3130: Flags [.], ack 1, win 512, options [nop,nop,TS val 841486028 ecr 3344603128], length 0
10:13:38.897722 lo    In  IP 127.0.0.1.60698 > 127.0.0.3.3130: Flags [P.], seq 1:4, ack 1, win 512, options [nop,nop,TS val 841486028 ecr 3344603128], length 3
10:13:38.897739 lo    In  IP 127.0.0.3.3130 > 127.0.0.1.60698: Flags [.], ack 4, win 512, options [nop,nop,TS val 3344603128 ecr 841486028], length 0
10:13:38.897955 lo    In  IP 127.0.0.3.3130 > 127.0.0.1.60698: Flags [P.], seq 1:3, ack 4, win 512, options [nop,nop,TS val 3344603128 ecr 841486028], length 2
10:13:38.897977 lo    In  IP 127.0.0.1.60698 > 127.0.0.3.3130: Flags [.], ack 3, win 512, options [nop,nop,TS val 841486028 ecr 3344603128], length 0
10:13:38.898047 lo    In  IP 127.0.0.1.60698 > 127.0.0.3.3130: Flags [P.], seq 4:14, ack 3, win 512, options [nop,nop,TS val 841486028 ecr 3344603128], length 10
10:13:38.898129 lo    In  IP 127.0.0.3.3130 > 127.0.0.1.60698: Flags [P.], seq 3:13, ack 14, win 512, options [nop,nop,TS val 3344603128 ecr 841486028], length 10
10:13:38.898253 lo    In  IP 127.0.0.1.49384 > 127.0.0.3.53298: UDP, length 38
10:13:38.898317 enp1s0 Out IP 192.168.1.13.52394 > 3.132.228.249.3479: UDP, length 28
10:13:38.939622 lo    In  IP 127.0.0.1.60698 > 127.0.0.3.3130: Flags [.], ack 13, win 512, options [nop,nop,TS val 841486070 ecr 3344603128], length 0
10:13:39.061935 enp1s0 In  IP 3.132.228.249.3479 > 192.168.1.13.52394: UDP, length 68
10:13:39.062134 lo    In  IP 127.0.0.3.53298 > 127.0.0.1.49384: UDP, length 78
10:13:39.062243 lo    In  IP 3.132.228.249.3479 > 192.168.5.5.48202: UDP, length 68

基本的には先ほどと同様の流れで動いています。やはりtproxyの22224番ポートは出力に現れません。

Linuxの場合: 特定のアプリケーションにTPROXYを使用させる

当初の目的は特定のアプリケーションにTPROXYを使用させることでした。これは、cgroupというものを使って、特定のプロセス(子プロセス含む)が生成したパケットのみが対象になるように今までのip ruleやiptablesルールを設定することで実現できます。

cgroupに関する部分はこれだけでそれなりの長さになってしまうので別の記事に分けました。

cgroupのnet_clsを使って、特定のプロセス(ツリー)に対するiptables/nftablesルールを設定する - turgenev’s blog

これができたら、以下のようにして該当cgroupのパケットにまずmarkをつけて、

sudo iptables -t mangle -A OUTPUT -p udp ! -d 127.0.0.0/8 -m cgroup --cgroup 1 -j MARK --set-mark 0x123

それからip ruleを追加して、

sudo ip rule add fwmark 0x123 lookup 100

最後にTPROXYルールを適用すればよいです。

sudo iptables -t mangle -A PREROUTING -p udp -m mark --mark 0x123 -j TPROXY --on-ip 127.0.0.1 --on-port 22224

UDPだけ載せましたがTCPも同様です。

SOCKS5を透過プロキシとして利用するための設定: Androidなど、その他

hev-socks5-tproxyの作者がhev-socks5-tunnelというのも作っており、iOSやWSLなど含め*nix系にはだいたい対応しているようです。AndroidだとVPNアプリとしてSocksTunもあり、手元で使ってみましたがアプリごとにプロキシの有効/無効切り替えもできてとても良い感じでした。またhev-socks5-serverと組み合わせるときはUDP over TCPができます。

UDP AssociateのNATタイプ

NATタイプ(EIM/APDFなどの用語)に関してはNATタイプ、ポートセービングIPマスカレード、UDPホールパンチング、STUN - turgenev’s blogをご覧ください。

UDP Associateでは、クライアント側のUDPポートとプロキシ側のUDPポートがマッピングされるような動作をすることになるため、実質的にどのようなNATタイプとして機能するのか気になります。また、SOCKS5プロキシと透過プロキシソフトウェアのそれぞれでNAT動作を考える必要があります。

まずMappingに関しては、ここで紹介した全てのソフトウェアでEIMになります。まあ普通に実装する限りはそうなるだろうという感じです。一方、Filteringに関しては、3proxyとProxiFyreとsemigodking/redsocksとhev-socks5-tproxyはEIFですが、danteとredsocksはAPDFになります。この2つは当初の通信先と一致しないUDPパケットのみを通す実装になっているからです。詳しく原因を見ていきます。

まずdanteに関してですが、最終的な目的地へのUDP通信を行う際に、connect関数を使用しているのが原因です。UDPには接続の概念がないので普通はconnect関数はあまり使わないのですが、使うことは可能で、そうすると単一の宛先としか通信できなくなります(他のところからパケットが来るとICMP port unreachableが返る)。ソースファイルとしては主にsockd/dante_udp.cが関係していて、connectに関するコードの付近に「Should we connect to the destination?」から始まるコメントが書いてあり、方針を決めかねている様子がうかがえます。さらによくコードを読むと、lib/config.cで定義されているsockscf.udpconnectdstという変数を1から0に変更することで、connectが使用されなくなることがわかります。実際、手元でこの変更をしてからビルドすることでdanteの動作がEIM/EIFになることを確認しました。追記: manを読み直したら、confの中にudp.connectdst:noと書いておくことで同じ挙動が実現できることがわかりました。

redsocksに関しては、redudp.cのredudp_pkt_from_socks()で、get_destaddr関数の値との比較を行い、アドレスまたはポートが一致しなければ「Socks5 server relayed packet from unexpected address」というメッセージを表示してパケットを破棄していることがわかります。semigodking/redsocksでは(ここで別のファイルに移動された上で)ここでそのコードが削除されているため、EIM/EIFになります。

もしこのSOCKS5を利用してUDPホールパンチングを行うのであれば、余計なフィルタリングのないEIM/EIF(Full Cone NAT)が望ましいかと思います。一方で、クライアント-サーバー型の普通のUDP通信(DNSなど)が利用できれば十分というのであればEIM/APDFでも問題はありません。

EIM/EIFとEIM/APDFを組み合わせれば、全体としては制約が厳しいEIM/APDFと同等の動作をすることになります。また、当然ですが、SOCKS5プロキシと透過プロキシに加えて、SOCKS5が稼働しているサーバーのNAT環境も影響します。

TCPのNATタイプ

TCPに関してはホールパンチングでは普通使わないのでNATタイプが問題になることはありませんが、一応調べてみると、Mappingの時点でConnection Dependent Mappingになります。SOCKS5だけでなくHTTPプロキシでも同じでした。おそらく、新規接続の際に、前回のポートを使いまわすようなことは特に考えていないのではないかと思います。しかるべく実装すればEIMにはできそうです。Filteringに関しては、EIF(他の場所からの接続も受け入れる)にしようとするとリバースプロキシの役目も果たすということになるのでかなり大がかりな変更(SOCKS5の範囲を逸脱するような)が必要になる気がします。まあ必要ないのでいいでしょう。

MTUに関して

通常のUDP通信にプロキシが介在することになるため、MTU関連で問題が起きないか心配になります。しかし、透過プロキシ側でもSOCKS5側でも一旦UDPソケットを介しているので、そこで再構築と分割が行われるため、(効率はよくないかもしれませんが)プロキシが原因で通信ができなくなることはありません。

VPNによるSplit Tunneling

ここまでSOCKS5を用いた方法について解説しましたが、VPNソフトウェア側がアプリケーションごとのスプリットトンネリング(一部の通信だけを選択的にVPNに通すこと)に対応している場合もあります。TunnelBearは対応しているようです。WireguardはTunnlToを使うとできそうです(ProxiFyreの作者がかかわっている?)(というかこれができるなら一般のVPNに対してもできそうな気がする…)。OpenVPNは(少なくともWindowsでは)無理そうです。

TCP限定の方法

TCPに関してはUDPよりも容易で、様々な方法・ソフトウェアがあります。

まずiptablesを使う場合はTPROXYではなくREDIRECTターゲットを使うこともできます。redsocksのREADMEにもあります。REDIRECTもlocalhostにむけて行われるようなので、透過プロキシ側の設定ファイルはそのままで使えるはずです。

ちなみにTPROXY instead of REDIRECT for TCP traffic · Issue #133 · semigodking/redsocks · GitHubによるとTPROXYはDNATと併用できるメリットがあるようです。

tsocksや、その後継(?)のtorsocksは、tsocks firefoxのようなコマンドで起動することでアプリケーションがsocksを使用するようにしてくれるものですが、UDPには対応していないようです。proxychains(-ng)も(こちらはHTTPプロキシなどにも対応していますが)同様です。iptablesルールを設定する必要がなく、root権限が不要な場合が多い(AppleのSystem Integrity Protectionとか、別の障壁に引っかかる可能性はありますが)ので、用途がTCPならこっちを使った方が楽です。ただし、go製のプログラムなど、カーネルの機能を直接呼んでいるプログラムにはLD_PRELOADが効かないので使えません。

OSやブラウザのプロキシ設定について

少し本題とはそれますが、OSやブラウザにもプロキシを設定する機能は標準で備わっており、多くの場合はSOCKSにも対応しています。しかしこれらは基本的にTCPに関してしか機能せず、DNSやWebRTC(Discordなど)のようなUDP通信はプロキシを経由しません。特にインターネット匿名化技術であるTorの界隈ではUDP通信で生のIPが漏洩するとして注意が呼びかけられているのをよく見かけます。なお、UDPをプロキシすることはできないかわりにUDPをブロックすることができる場合はあります(ChromeならWebRTC Network Limiterとか?)。またFirefoxではDNSをSOCKS側に任せるというオプションがあり、この場合ブラウザ側はDNS解決を行わずドメイン名をそのままSOCKS側に投げるようです(How does SOCK 5 proxy-ing of DNS work in browsers? - Stack Overflow)(DNSリクエストがSOCKSによってプロキシされるわけではない)。

単一のホストとのUDP通信をプロキシする場合

UDP通信の相手が一つ(8.8.8.8:53だけとか)しかない場合は、TPROXYを使わなくてももっと簡単にできるようです。redsocksとかにやり方が載っていると思います。これについては解説しません。

UDP Associateの現実的な有効性について

UDP Associateは技術としては面白く、使用感も悪くないですが、UDP Associateが真に有効な場面というのはそれほど多くないような気もします。

まず、最初に述べたように、UDP Associateを使うためにはプロキシサーバーのUDPポートが手元から全部見えている必要があり、そのためには自宅サーバーやVPSなど、かなり自由に扱えるコンピュータが必要になります。

LinuxではPolicy Based Routingが強力なので、TPROXYのルールを設定するくらいだったら同じ条件でパケットにマークをつけてそれに従って(LAN内のPCやVPN経由のVPSに)ルーティングすれば済みます。

Windowsではルーティングが貧弱なので、特定のアプリケーションのUDP通信をプロキシ経由にしたければ、ここで紹介した以外の方法を筆者は知りません。

従って例えば、「UDP通信を使用するWindows用のゲームソフトを使いたいが、自宅ではUDP通信がブロックされているのでVPSを契約しており、VPSの通信量を節約するためにゲーム用の通信だけをVPSに流したい」といった状況であれば、この記事の方法が真に有効である可能性はあります。ちなみに筆者自身の状況としては、使っているVPNUDPがブロックされていてDiscordの通話ができず、Discordの通話サーバーは大量にあるので宛先ベースでのルーティングでも対処が難しく、プロキシを使って何とかならないかと考えていた感じでした。

また、SOCKS5プロキシを起動するにはrootは不要なので、root化されていないAndroidのキャリア回線(大抵はEIM/EIF)をPC側で使いたいという場合もこの記事の手法が有効です。

UDP AssociateのためのUDPホールパンチング

UDP Associateが使えるためにはSOCKS5プロキシ側のUDPのポートが全部見えていなければいけないと言いましたが、UDPホールパンチングが使えれば、ポートが開放されていなくても必要に応じてP2PUDP接続の経路を確立することができます。SOCKS5と透過プロキシの両方を頑張って拡張すればできそうです。

ただ、これも真に有効な場面があるかと言われると…という感じです。

ちなみにsemigodking/redsocksのREADMEにはFull Cone NAT Traversalのサポートがあるというようなことが書いてありますが、これは単にredsocksがFull Cone NATとして動作するというだけで、UDP Associateでホールパンチングしてくれるという話ではないと思います。

試していないもの

Hysteria 2 SOCKS5も透過プロキシ部分も全部入っていそうでかなり強そうですが、設定方法がよくわからなかった(TLSまたはacmeを設定しろとか)のでやめました。TPROXY関連など多くのドキュメントがあります。

  • 追記: 改めてちゃんと調べてみたら、サーバー側(出口)とクライアント側をペアで用意してその間は暗号化されたQUICで通信して、そのクライアント側がSOCKSとして動作できるという話のようです。従って普通のSOCKS5プロキシとして使うには不適ですが、本記事の内容は応用できます。

Win2Socks - Transparent Proxy for Windows UDPをサポートしていると明記されていますが、有料なので試していません。

Sockscap Tutorial | Sockscap32 Download これはWindows用で、特定アプリケーションをSOCKSに通してくれるようですが雰囲気的にあまりUDPをサポートしていそうに見えないので試していません。

Force an application's traffic through a SOCKS proxy - Unix & Linux Stack Exchange proxyboundというものが紹介されています。

socks - What's the difference between torify, usewithtor, tsocks and torsocks? - Tor Stack Exchange torifyというものが紹介されています。

特定プログラムを対象に、(SOCKS経由ではなく)bindするIPを指定できるというソフトウェアもありました。今回の目的に合うかは不明です。

ForceBindIP | r1ch.net

GitHub - falahati/NetworkAdapterSelector: A simple solution to let you force bind a program to a specific network adapter

関連文献

iptables - What is the purpose of TPROXY, how should you use it and what happens internally? - Server Fault TPROXYとip routeに関する説明。

redsocks/doc/balabit-TPROXY-README.txt at master · semigodking/redsocks · GitHub semigodking/redsocksにあるTPROXYの説明。

TProxy - Hysteria 2 TPROXYの詳しい設定例がある。

IPTables TPROXY - proxy input and output · GitHub OUTPUTパケットにTPROXYを適用する設定例。markの付け方が工夫されている。

networking - How to send all internet traffic to a SOCKS5 proxy server in local network? - Android Enthusiasts Stack Exchange 英語。網羅的で非常に詳しい回答がついている。

SOCKS非対応のアプリケーションをSOCKS経由にする - ふなWiki 非常に古い。

EdgeRouter X に redsocks を導入して透過的にプロキシーを経由させる – ちとくのホームページ redsocksのdnstc、dnsu2tの機能について書いてある。