LinuxでTPROXYを使ったTransparent Proxyの設定ガイド:クライアントごとの設定不要でTCP/UDP全トラフィックをリダイレクト

Network tutorial - IT technology blog
Network tutorial - IT technology blog

Quick Start: 5分でTPROXYを動かす

理論に入る前に、まずtransparent proxyをすぐに動かしてみましょう。環境:Ubuntu 22.04、kernel 5.15+、SquidをProxyデーモンとして使用。

ステップ1:TPROXYサポート付きSquidのインストール

sudo apt update && sudo apt install squid

# SquidがTPROXYをサポートしているか確認
squid -v | grep -i netfilter

出力に--enable-linux-netfilterが含まれている必要があります。Ubuntuのデフォルトパッケージにはすでにこのフラグが含まれています。

ステップ2:SquidのTPROXYポートリスニング設定

/etc/squid/squid.confに以下を追加:

# TPROXYポート(通常のプロキシポート3128とは異なる)
http_port 3129 tproxy

# テスト用に一時的にすべて許可
http_access allow all

ステップ3:カーネルルーティング + iptablesの設定

ファイル/usr/local/bin/tproxy_setup.shを作成:

#!/bin/bash
# fwmark=1のトラフィック用に専用ルーティングテーブルを作成
ip rule add fwmark 1 lookup 100 2>/dev/null || true
ip route add local default dev lo table 100 2>/dev/null || true

# カーネルモジュールをロード
modprobe xt_TPROXY

# 管理しやすいように専用チェインを作成
iptables -t mangle -N TPROXY_MARK 2>/dev/null || iptables -t mangle -F TPROXY_MARK

# LANからのTCPポート80・443をSquidにリダイレクト
iptables -t mangle -A TPROXY_MARK -p tcp -m multiport --dports 80,443 \
  -j TPROXY --tproxy-mark 0x1/0x1 --on-port 3129

# プロキシデーモンからのトラフィックはリダイレクトしない(ループ防止)
iptables -t mangle -A PREROUTING -m owner --uid-owner proxy -j RETURN

# LANからのトラフィックに適用(環境に合わせてサブネットを変更)
iptables -t mangle -A PREROUTING -s 192.168.1.0/24 -j TPROXY_MARK
sudo chmod +x /usr/local/bin/tproxy_setup.sh
sudo bash /usr/local/bin/tproxy_setup.sh
sudo systemctl restart squid

LAN内のクライアントからすぐにテストします — ブラウザにプロキシを設定せずに — 任意のHTTPページにアクセスして確認:

sudo tail -f /var/log/squid/access.log

クライアントのリクエストがログに表示されれば、TPROXYは正しく動作しています。

TPROXYが必要な理由を理解する

従来のプロキシの問題点

通常のプロキシには根本的な弱点があります:各デバイスの設定が必要なことです。200台のPCがある会社ではGPOやMDMで設定を配布する必要があります。スマートTV、ゲームコンソール、IoTデバイスはほとんどプロキシオプションを持っていません。IPカメラやネットワークプリンターはプロキシをサポートすることがほぼありません。

解決策はカーネルレベルにあります。トラフィックは内部ネットワークを出る前にゲートウェイでブロックされます — クライアントは知る必要も、設定する必要もありません。スマートTVは普通にウェブを閲覧しますが、すべてのリクエストはあなたのプロキシを経由します。

TPROXYと通常のNATリダイレクトの違い

通常のNATリダイレクト(REDIRECT target)を使うと、プロキシはパケットを受け取りますが自分自身のアドレスしか見えません — 本来の宛先情報は完全に失われます。まるで手紙を受け取っても差出人が誰に送るつもりだったか分からないようなものです。

TPROXYが決定的に異なる点:パケットをユーザー空間に配信する際、元の宛先アドレスがそのまま保持されます。プロキシはソケットオプションIP_TRANSPARENTを通じて本来の宛先を読み取り、クライアントのソースIPでインターネットに接続します。宛先サーバーはリクエストが192.168.1.100から来たと認識し、プロキシの存在を知りません。

パケット処理の完全なフロー

  1. クライアントが93.184.216.34:80にTCP SYNを送信
  2. パケットがインターフェースeth0に入り、mangleテーブルのPREROUTINGチェインを通過
  3. TPROXYルールがマッチし、パケットにfwmark=1をマーク、ポート3129に転送
  4. カーネルがローカルソケットを検索 — ポート3129でSquidがリスニング中であることを確認
  5. パケットがSquidに配信されるが、元の宛先アドレスはそのまま保持
  6. Squidがgetsockname()を通じて宛先アドレス93.184.216.34:80を読み取る
  7. Squidがインターネットへの接続を開き、ソースIPを192.168.1.100(クライアントのIP)にバインド

高度な設定

UDPとDNSの透過的な処理

UDPはハンドシェイクがないためTCPとメカニズムが異なりますが、TPROXYは対応しています。よくあるユースケースはDNSをインターセプトしてフィルタリングやクエリ全体のログ取得です:

# UDP透過性にはxt_socketモジュールが必要
modprobe xt_socket

# UDP DNSルールを追加
iptables -t mangle -A TPROXY_MARK -p udp --dport 53 \
  -j TPROXY --tproxy-mark 0x1/0x1 --on-port 5353

PythonでUDPプロキシを書くには特別なソケットオプションの設定が必要です:

import socket

sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
# マシンに属さないアドレスへのバインドを許可
sock.setsockopt(socket.SOL_IP, socket.IP_TRANSPARENT, 1)
# 各パケットの元の宛先アドレスを受信
sock.setsockopt(socket.SOL_IP, socket.IP_RECVORIGDSTADDR, 1)
sock.bind(('0.0.0.0', 5353))

nftablesへの移行(新しいカーネルへの推奨)

Debian 11とUbuntu 22.04+から、nftablesがデフォルトです。iptablesよりもシンプルな構文で、カーネルチームはiptablesの拡張よりもnftablesの開発を優先しています:

table ip tproxy_table {
    chain prerouting {
        type filter hook prerouting priority mangle; policy accept;
        
        # プロキシデーモンからのトラフィックをスキップ
        meta skuid proxy return
        
        # LANからのTCP 80/443をリダイレクト
        ip saddr 192.168.1.0/24 tcp dport { 80, 443 } \
            tproxy ip to 127.0.0.1:3129 meta mark set 0x1
    }

    chain divert {
        type filter hook prerouting priority mangle;
        
        # 確立済みソケットのトラフィックはTPROXYが不要
        meta l4proto tcp socket transparent 1 meta mark set 0x1 accept
    }
}
sudo nft -f /etc/nftables-tproxy.conf
# 確認
sudo nft list ruleset

HTTPSインスペクションのためのSSL Bump

エンタープライズ環境ではHTTPSも検査が必要になることがよくあります。SquidはSSL bumpで対応できます — ただし事前にすべてのクライアントにCA証明書を配布しておく必要があります。そうしないとブラウザが証明書エラーを表示します:

# まずCA証明書を作成
openssl req -new -newkey rsa:2048 -sha256 -days 365 -nodes -x509 \
  -keyout /etc/squid/ssl_cert/myCA.key \
  -out /etc/squid/ssl_cert/myCA.pem

# squid.conf
https_port 3130 tproxy ssl-bump \
  cert=/etc/squid/ssl_cert/myCA.pem \
  key=/etc/squid/ssl_cert/myCA.key

# SNIのみピーク、復号なし(プライバシーへの影響が少ない)
ssl_bump peek step1
ssl_bump splice all

トラブルシューティングと運用のヒント

トラフィックがリダイレクトされない場合のデバッグ

TPROXYが動作しない場合に使うチェックリスト:

# 1. パケットがルールに到達しているか確認
iptables -t mangle -L TPROXY_MARK -v -n

# 2. ルーティングテーブル100を確認
ip route show table 100
ip rule show

# 3. Squidが正しいポートにバインドしているか確認
ss -tlnp | grep 3129

# 4. SquidプロセスのCapabilityを確認
cat /proc/$(pgrep squid | head -1)/status | grep -i cap

最も一般的なエラー:SquidにCAP_NET_ADMINがない場合。systemdサービスに以下を追加して修正:

# /etc/systemd/system/squid.service.d/tproxy.conf
[Service]
AmbientCapabilities=CAP_NET_ADMIN CAP_NET_BIND_SERVICE

# リロードして再起動
sudo systemctl daemon-reload && sudo systemctl restart squid

複数VLANのセットアップ時のサブネット計算

複数のサブネットに同時にルールを追加する必要がある場合、toolcraft.app/ja/tools/developer/ip-subnet-calculatorをよく使っています — CIDRを入力するだけでネットワーク範囲、ブロードキャスト、ホスト数がすぐに表示されます。各VLANにルールを追加する際、手計算よりもはるかに便利です。

多数の接続でのパフォーマンス最適化

# コネクショントラッキングテーブルを拡張
sysctl -w net.netfilter.nf_conntrack_max=262144

# ソケットバッファを拡張
sysctl -w net.core.rmem_max=16777216
sysctl -w net.core.wmem_max=16777216

# TPROXYで処理済みのトラフィックのconntrackを無効化(オーバーヘッド削減)
iptables -t raw -A PREROUTING -p tcp -m multiport --dports 80,443 \
  -m mark --mark 0x1 -j NOTRACK

systemdでの再起動後の自動起動

# /etc/systemd/system/tproxy-setup.service
[Unit]
Description=TPROXY iptables セットアップ
Before=squid.service
After=network.target

[Service]
Type=oneshot
RemainAfterExit=yes
ExecStart=/usr/local/bin/tproxy_setup.sh
ExecStop=/usr/local/bin/tproxy_cleanup.sh

[Install]
WantedBy=multi-user.target
sudo systemctl enable --now tproxy-setup.service

設定が完了したら作業は終わりです。LAN内のすべてのデバイス — スマートTV、IPカメラ、IoTデバイス — がすべて透過的にプロキシを経由します。各マシンの設定を触る必要はありません。

Share: