http proxy を使わないと外に出れない環境で、事実上 proxy 設定を入れることができない場合、transparent proxy (intercepting proxy) を設置することで解決できる。通称 tproxy。その構築メモ。tproxy とは直接は関係ないけれど、ここでは MASQUERADE の機能も同時に入れている。
raspi2にarchlinux入れる。USB NIC追加する(eth1
)。eth0
が uplink とする。
とりあえず満たしたい要件は dual-stack masquerade, http tproxy の二つ。
sysctl
/etc/sysctl.d/nat.conf
に以下の内容を保存する。これは ipv6 forwarding が有効になった時に、uplink から貰った経路を使い続けるため。
net.ipv6.conf.eth0.accept_ra=2
systemd-networkd
/etc/systemd/network/eth.network/eth0.network
に IPForward=yes
を追加する。ちなみにこれは sysctl より後に実行されて設定を上書きすることに注意。「sysctlに設定したからオッケー」とはならない。また uplink から受け取った DNS サーバを使いたいので UseDomains=yes
を追加する。
eth1 に static アドレス割り当てする。マニュアルを見ながら、同様に /etc/systemd/network/eth1.network
を作る。内容はこんな感じ。IPMasqueradeオプションもあるけど、iptablesで他のものを設定するついでがあるので、ここではやらなかった。
[Match]
Name=eth1
[Network]
Address=192.168.0.1/24
Address=fd00::1/64
IPForward=yes
systemd-resolved
FallbackDNS=
という行を追加しておく。さもないとマニュアルにある通り組み込みのDNSサーバが参照されてしまうから。そしてなぜか組み込みのDNS サーバは google のものだったりする。uplink proxy server を名前で登録する場合には、引けない名前を引きに行って timeout するとか起こるので、明示的に無効化しておく。
dnsmasq
IPv4, IPv6 dual stack で対応してくれる。eth1
に割り当てた IP アドレスと対応をとり、同一アドレス帯になるようにすること。
dhcp-range=192.168.0.50,192.168.0.150,12h
dhcp-range=fd00::, ra-stateless
systemctl enable dnsmasq
で起動登録する。
iptables
通常の MASQUERADE の設定にさらに、squid の設定を参考に TPROXY の設定を入れる。どちらも tproxy 的には dev lo
port 3128 で待ち受けるようにした。
iptables -t nat -A POSTROUTING -o eth0 -j MASQUERADE
iptables -A FORWARD -i eth0 -o eth1 -m state --state RELATED,ESTABLISHED -j ACCEPT
iptables -A FORWARD -i eth1 -o eth0 -j ACCEPT
ip6tables -t nat -A POSTROUTING -o eth0 -j MASQUERADE
ip6tables -A FORWARD -i eth0 -o eth1 -m state --state RELATED,ESTABLISHED -j ACCEPT
ip6tables -A FORWARD -i eth1 -o eth0 -j ACCEPT
iptables -t mangle -N DIVERT
iptables -t mangle -A DIVERT -j MARK --set-mark 1
iptables -t mangle -A DIVERT -j ACCEPT
iptables -t mangle -A PREROUTING -d 10.0.0.0/8 -j ACCEPT
iptables -t mangle -A PREROUTING -d 172.16.0.0/12 -j ACCEPT
iptables -t mangle -A PREROUTING -d 192.168.0.0/16 -j ACCEPT
iptables -t mangle -A PREROUTING -p tcp -m socket -j DIVERT
iptables -t mangle -A PREROUTING -p tcp --dport 80 -j TPROXY \
--tproxy-mark 1 --on-port 3128
iptables -t mangle -A PREROUTING -p tcp --dport 443 -j TPROXY \
--tproxy-mark 1 --on-port 3128
iptables -t mangle -A PREROUTING -p tcp --dport 11371 -j TPROXY \
--tproxy-mark 1 --on-port 3128
ip6tables -t mangle -N DIVERT
ip6tables -t mangle -A DIVERT -j MARK --set-mark 1
ip6tables -t mangle -A DIVERT -j ACCEPT
ip6tables -t mangle -A PREROUTING -d fec0::/10 -j ACCEPT
ip6tables -t mangle -A PREROUTING -d fc00::/7 -j ACCEPT
ip6tables -t mangle -A PREROUTING -p tcp -m socket -j DIVERT
ip6tables -t mangle -A PREROUTING -p tcp --dport 80 -j TPROXY \
--tproxy-mark 1 --on-port 3128
ip6tables -t mangle -A PREROUTING -p tcp --dport 443 -j TPROXY \
--tproxy-mark 1 --on-port 3128
ip6tables -t mangle -A PREROUTING -p tcp --dport 11371 -j TPROXY \
--tproxy-mark 1 --on-port 3128
# 起動登録
iptables-save > /etc/iptables/iptables.rules
ip6tables-save > /etc/iptables/ip6tables.rules
systemctl enable iptables
systemctl enable ip6tables
https_tproxy
無かったので作った。
$ export GOPATH=$HOME
$ go get github.com/hkwi/https_tproxy
tproxy.service
squid の設定を参考にした際のiptables以外、ip rule, ip route や ebtables のエントリを入れる。起動用のファイルは次のように整えてみた。systemctl enable tproxy
で有効にする。
[Unit]
Description=tproxy
[Service]
Type=oneshot
ExecStart=/etc/tproxy
RemainAfterExit=yes
[Install]
WantedBy=multi-user.target
ebtables は Linux kernel で、DST が自分自身でないパケットを自分自身に向かうようにする(tproxy)ために必要。逆向きが必要ないのは、MASQUERADE しているため。
#!/bin/bash
ip -4 rule add fwmark 1 lookup 100
ip -4 route add local default dev lo table 100
ip -6 rule add fwmark 1 lookup 100
ip -6 route add local default dev lo table 100
ebtables -t broute -A BROUTING -i eth1 -p ipv6 \
--ip6-proto tcp --ip6-dport 80 -j redirect --redirect-target DROP
ebtables -t broute -A BROUTING -i eth1 -p ipv4 \
--ip-proto tcp --ip-dport 80 -j redirect --redirect-target DROP
ebtables -t broute -A BROUTING -i eth1 -p ipv6 \
--ip6-proto tcp --ip6-dport 443 -j redirect --redirect-target DROP
ebtables -t broute -A BROUTING -i eth1 -p ipv4 \
--ip-proto tcp --ip-dport 443 -j redirect --redirect-target DROP
ebtables -t broute -A BROUTING -i eth1 -p ipv6 \
--ip6-proto tcp --ip6-dport 11371 -j redirect --redirect-target DROP
ebtables -t broute -A BROUTING -i eth1 -p ipv4 \
--ip-proto tcp --ip-dport 11371 -j redirect --redirect-target DROP
GOMAXPROCS=4 /root/bin/https_tproxy -out=$HTTP_PROXY 2>&1 |& logger &
解説
本家本元の解説は Linux kernel source tree の Documentation/networking/tproxy.txt
にある。ポイントは、TCP socket を通常と同じように開いて扱うことができる+Listen しているポートはダミーで、本来の宛先を socket name として取得できる、というところでしょう。
squid でできるんじゃないか…というのは、http proxy 時に cache_peer 設定で parent cache server を指定できる、ということだと思いますが、これはダメでした。parent cache server に接続できなかった場合に、自分で外部に接続しに行こうとします。それから https の tproxy は実装がありません。ということで、私は自作に切り替えました。
tproxy にもデメリットはあります。プロクシ通信路でエラーがあった場合に、ブラウザがエラーを認識できないのが原因で 1) エラーをユーザに提示できない 2) ブラウザがリトライを試行しない というものがあります。
IPv6 には masquerade なんて無い!という意見があるかもしれません。が、実際現実問題として DHCPv6 でステートフルにアドレスが割り当てられていて、かつ、Prefix delegation が無かった時にどうするかというと、masquerade せざるを得ません。
port 11371 は hkp
で、gpg key server から鍵をダウンロードする際に使用されます。特に docker build の最中に key ダウンロードがある場合など、これも tproxy に入れておいたほうが簡単です。
追記:tproxy の部分は、今までに見つけた範囲では、次のプログラムでも実現できそうです。他にもありそうでしたら教えてください!