Building SOCKS5-based "lightweight" exit node for Tailscale network
Tailscaleで出口ノードを構成するとすべてのトラフィックが出口ノードを経由します。 公共のWiFi環境などでは便利ですが、自宅などの環境では必要なトラフィックのみtailnet内部の出口ノードを経由させたいことがあります。
これは出口ノードに求められている機能が主に2つあり、要件によってはどちらか1つで事足りるためです。
- 機能1: 端末からのインターネット向け通信を安全なゲートウェイを経由させることで、WiFi環境などにおける中間者から秘匿する
- 機能2: 端末の物理ネットワークに寄らず、インターネット側に対して、常に同じソースIPアドレスからの通信にさせる
例えば、出口ノードが東京にあり、端末が海外の自宅にある場合、秘匿すべき中間者を想定する必要がないため機能1は必要としていません。 そのため、機能2のためだけに出口ノードを構成してしまうとすべてのトラフィックが東京経由になってしまい、体験が悪化してしまいます。
今回はアプリケーション(主にブラウザ)側が宛先に応じて出口ノードを選択する機能を有していることを前提に、機能2だけを充足するための手法を検討しました。
例となるシナリオ
tailnet内の端末が、Tailscaleの出口ノードを構成せずにtailnet内部で動作するSOCKS5プロキシを利用することで、ソースIPアドレスベースでのアクセス制御が実装されているSaaSにアクセス可能にする
Youtubeなどの一般的なサービスは出口ノードを経由せず、直接アクセスさせる。
要件
要件1: tailnet内部でのみSOCKS5プロキシを動作させる
プロキシが動作しているネットワークや当然ながらインターネット側からの要求は一切受け付けない。tailnet内部に絞ることで、SOCKS5での認証は付けずに、tailnet内部のソースIPアドレスでのアクセス制御で認証とする。
要件2: 内部ネットワークへのプロキシを拒否する
内部ネットワークへのアクセスはTailscale自体で達成するべきなので、内部ネットワークへのプロキシはすべて拒否し、インターネット側へのみプロキシする。
サーバ実装
今回はRaspberry Pi 5ですでに構成済みの出口ノードにSOCKS5プロキシ機能を追加しました。
インストール
今回は2024年11月現在でもきちんとメンテナンスされている dante
を使いました。
% sudo apt install dante-server
サーバ設定
設定ファイルは /etc/danted.conf
にあります。
Danteの設定における注意事項:
- アクセス制御では、マッチするとそこで評価を終了する。何もマッチしない場合は block となる。
- tailnetを表わすIPv4アドレスレンジとして
100.64.0.0/10
(CGNAT)、 IPv6アドレスレンジとしてfd7a:115c:a1e0::/48
(ULA) を使用。ここは環境ごとに変わらない。 - プロキシ元・先の組み合わせはIPv4/IPv6の混合が可能(IPv4でプロキシサーバに接続して、IPv6の宛先に中継させるなど)なので、ブロックする先に対してIPv4/IPv6両方のプロキシ元を想定する必要がある(つまりブロック先1つに対してIPv4/IPv6で設定が2つ必要)
設定ファイルにデフォルト値がすでに存在しているので、要件に応じて修正した箇所を説明します。
# [要件1]
# tailscale0 インターフェースにbindすることで、
# Tailscale network経由のパケットのみがSOCKS5プロキシに到達可能になる。
internal: tailscale0 port = 1080
# [要件2]
# プロキシ時は eth0 から送出することでループバックやtailnetへ戻せないようにする
external: eth0
# [要件1]
# tailnet経由なのでSOCKS5上でのクライアントの認証は不要
clientmethod: none
socksmethod: none
# [要件2]
# SOCKS5プロキシサーバ自体へのアクセス制御
# tailscale0 インターフェスにbindしているので厳密には必要ないが、念のためtailnetのIPv4/IPv6アドレス帯のみを許可している
client pass {
from: 100.64.0.0/10 port 1-65535 to: 0.0.0.0/0
log: error
}
client pass {
from: fd7a:115c:a1e0::/48 port 1-65535 to: ::/0
log: error
}
# [要件2]
# 以後はプロキシ元・先に対するアクセス制御になる。
# IPv4では、内部ネットワークへのプロキシを block するルールを並べ、最後にすべてを pass させる。
# IPv6では 2000::/3 がグローバルユニキャストアドレス (インターネットで使えるアドレス) なので、2000::/3を pass するルールを書いて、
# それ以外をデフォルト block に寄せている。
socks block {
from: 0.0.0.0/0 to: lo
log: connect error
}
socks block {
from: ::/0 to: lo
log: connect error
}
socks block {
from: 0.0.0.0/0 to: tailscale0
log: connect error
}
socks block {
from: ::/0 to: tailscale0
log: connect error
}
socks block {
from: 0.0.0.0/0 to: 10.16.0.0/8
log: connect error
}
socks block {
from: ::/0 to: 10.16.0.0/8
log: connect error
}
socks block {
from: 0.0.0.0/0 to: 172.16.0.0/12
log: connect error
}
socks block {
from: ::/0 to: 172.16.0.0/12
log: connect error
}
socks block {
from: 0.0.0.0/0 to: 192.168.0.0/16
log: connect error
}
socks block {
from: ::0/0 to: 192.168.0.0/16
log: connect error
}
socks block {
from: 0.0.0.0/0 to: 100.64.0.0/10
log: connect error
}
socks block {
from: ::0/0 to: 100.64.0.0/10
log: connect error
}
# IPv4 インターネットへのプロキシを許可
socks pass {
from: 100.64.0.0/10 to: 0.0.0.0/0
protocol: tcp udp
}
socks pass {
from: fd7a:115c:a1e0::/48 to: 0.0.0.0/0
protocol: tcp udp
}
# IPv6 インターネットへのプロキシを許可
socks pass {
from: 100.64.0.0/10 to: 2000::/3
protocol: tcp udp
}
socks pass {
from: fd7a:115c:a1e0::/48 to: 2000::/3
protocol: tcp udp
}
UPDATE(2024-11-24): インターフェース名を使っているため、再起動するとタイミングによってはインターフェースの設定が完了しておらず、起動に失敗するようです。 systemdの設定を以下にすることで改善できます。
% sudo systemctl edit danted
# dantedを起動させる前に特定のインターフェスの設定とtailscaledの起動を待つ
[Unit]
After=systemd-networkd-wait-online.service tailscaled.service
% sudo systemctl edit systemd-networkd-wait-online.service
# tailscale0の設定完了を待つoneshotサービス
[Service]
ExecStart=
ExecStart=/lib/systemd/systemd-networkd-wait-online -i tailscale0 -4 -6
% sudo systemctl enable systemd-networkd-wait-online.service
念のため再起動して動作を確認してください。
クライアント実装
curl
でSOCKS5プロキシを指定すると動作を確認できます。
# 名前解決は端末側で実施する。
# -4 と -6はURLのホスト名に対する名前解決の結果に作用する。
# tailnetが提供するホスト名はAレコード(IPv4アドレス)しか返さないので、結果的にSOCKS5サーバにはIPv4が常に使われ、
# SOCKS5サーバにIPv4/IPv6到達性があるかどうかで結果が変化する
% curl -v -4 -x socks5://socks5:1080 https://ifconfig.co
% curl -v -6 -x socks5://socks5:1080 https://ifconfig.co
curl
でプライベートIPアドレスにアクセスしようとすると失敗する動作が確認できます。
% curl -v -x socks5://socks5:1080 http://192.168.1.1
...
curl: (97) Can't complete SOCKS5 connection to 192.168.1.1. (2)
ChromeではSwitchyOmegaという拡張機能が有名です。URLやホスト名ベースでSOCKS5プロキシを自動的に切り替えることができます。 これにより、特定のサービスURLだけをtailnet内のSOCKS5を指定することで、Tailscaleで出口ノードを構成せずともインターネット側からソースIPアドレスを出口ノードに合わせることができます。