Building SOCKS5-based "lightweight" exit node for Tailscale network

Tailscaleで出口ノードを構成するとすべてのトラフィックが出口ノードを経由します。 公共のWiFi環境などでは便利ですが、自宅などの環境では必要なトラフィックのみtailnet内部の出口ノードを経由させたいことがあります。

これは出口ノードに求められている機能が主に2つあり、要件によってはどちらか1つで事足りるためです。

例えば、出口ノードが東京にあり、端末が海外の自宅にある場合、秘匿すべき中間者を想定する必要がないため機能1は必要としていません。 そのため、機能2のためだけに出口ノードを構成してしまうとすべてのトラフィックが東京経由になってしまい、体験が悪化してしまいます。

今回はアプリケーション(主にブラウザ)側が宛先に応じて出口ノードを選択する機能を有していることを前提に、機能2だけを充足するための手法を検討しました。

例となるシナリオ

tailnet内の端末が、Tailscaleの出口ノードを構成せずにtailnet内部で動作するSOCKS5プロキシを利用することで、ソースIPアドレスベースでのアクセス制御が実装されているSaaSにアクセス可能にする

Dante over Tailscale

Youtubeなどの一般的なサービスは出口ノードを経由せず、直接アクセスさせる。

要件

サーバ実装

今回はRaspberry Pi 5ですでに構成済みの出口ノードにSOCKS5プロキシ機能を追加しました。

インストール

今回は2024年11月現在でもきちんとメンテナンスされている dante を使いました。

% sudo apt install dante-server

サーバ設定

設定ファイルは /etc/danted.conf にあります。

Danteの設定における注意事項:

設定ファイルにデフォルト値がすでに存在しているので、要件に応じて修正した箇所を説明します。

# [要件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アドレスを出口ノードに合わせることができます。