Go
proxy

プロキシとの戦いに疲れたのでgoで透過プロキシを作ってみた

背景

Qiitaのproxyタグを見てもわかるように、多くのエンジニアがプロキシサーバとの戦いで日々消耗し続けている。とはいえ、主にセキュリティ上の理由でプロキシサーバの利用は必要なことも分かる。
となると、プロキシサーバを意識しなくても(設定しなくても)自動的にプロキシサーバを経由してくれるようになればうれしいよね? これは透過プロキシと言って昔からある考え方で、QiitaにもProxy環境下での仮想GW構築というすばらしい記事がある。

しかし、上記記事では透過プロキシにredsocks、DNSサーバにpdsndと複数のツールを組み合わせて実現されており、それぞれインストールや設定するのが個人的には面倒というのと、記事中で述べられているとおり、プロキシ経由で53/tcpに接続できない場合に対応できないという制約もあった。

というわけで、今回go言語の勉強も兼ねて自分好みの透過プロキシを自作してみた。といってもフルスクラッチではなく、既にある有用なライブラリを組み合わせるだけで実装できていて、書いたのはGlueコードがメインだったりします(ライブラリの作者の方々に感謝)。

透過プロキシの実装方式

調べるとよく出てくる透過プロキシの実装方式として、Linux縛りにはなるがiptablesを使った方法がある。NAT をやめて、透過 SOCKS プロキシを導入した などで詳しく解説されているので参考にすると良い。今回はこの方式で実装している。

作ったやつ

github.com/wadahiro/go-transproxy

設定ファイルなし・起動オプションのみで動作し、インストールもtransproxy1というシングルバイナリを配置するだけとシンプルなツール。ビルド済みのバイナリも用意

transproxyコマンドを実行するとデフォルトで以下の4つの透過プロキシが起動するとともに、iptablesの設定も自動で行うようになっている。iptablesにより、流れる通信は自動的に透過プロキシを通り、アップストリームのプロキシサーバを経由するようになる。

  1. HTTP(80/tcp)用透過プロキシ
  2. HTTPS(443/tcp)用透過プロキシ
  3. DNS(53/tcp, 53/udp)用透過プロキシ
  4. TCP(22/tcp)用透過プロキシ ※デフォルトではSSH(22/tcp)用だがオプションでポートの変更・追加可能

また、利用するアップストリームのプロキシサーバの制限レベルや内部のDNS環境が多少異なっても使えるようにいくつか起動オプション(後述)を用意しておいた。

基本的な使い方

アップストリームのプロキシ設定は、よく使われる環境変数(http_proxy, https_proxy, no_proxy)をそのまま利用するようにしている。
環境変数設定後は、以下のように起動オプションでプライベートドメイン解決用のDNSサーバとパブリックドメイン解決用のDNSサーバを指定するのみ。

# 利用するアップストリームのプロキシサーバ設定
export http_proxy=http://foo:bar@yourproxy.example.org:3128
export https_proxy=http://foo:bar@yourproxy.example.org:3128

# プロキシ除外設定がIP、CIDR、ドメイン後方一致で設定可能(ただし現状、制限がいくつかあり)
export no_proxy=example.org,192.168.0.0/24,172.16.0.0/16

# 内部DNS、外部DNSを指定してtransproxyをroot権限で起動
sudo -E transproxy -private-dns 192.168.0.100 -public-dns 8.8.8.8

なお、transproxyはiptablesのNATテーブルにルール追加を行うため、root権限が必要になるので sudo を使う。

transproxyにより透過プロキシが起動したら、あとはクライアントがこのサーバをゲートウェイとして利用するようにすればよい。そうするとクライアント側ではプロキシ設定は一切不要な状態で、プロキシサーバの先のネットワーク(主にインターネット)へのアクセスが可能になる。

使いみち

Dockerコンテナのビルドや実行で使う

自分はこのユースケースでよく使っている。
Dockerをインストールしているマシンでtransproxyを起動しておけば、Dockerコンテナからみるとデフォルトゲートウェイに自動的になる(Dockerコンテナのビルド時も)。なので、Dockerコンテナの起動やビルドオプションで環境変数http_proxyを渡したりせずとも、コンテナからはプロキシサーバを自動的に経由するようになり非常に便利。

透過プロキシを組み込んだデフォルトゲートウェイを別途たてて利用する

自分のPCからの通信を全て透過プロキシ経由としたいとか、複数人で透過プロキシを共有したい場合は
透過プロキシを組み込んだデフォルトゲートウェイを構築して利用することになる。

一番簡単な方法は、transproxyをデフォルトゲートウェイのルーターにインストールして動作させればよい。ただし、デフォルトゲートウェイを好き勝手にいじれない環境の方が多いかもしれない。その場合、選択肢としては以下の2つがあると思う。

  1. 用意されているサブネット内にさらに独自にサブネットを切って独自にデフォルトゲートウェイを新たに構築
  2. 用意されているサブネット内にオレオレゲートウェイを構築

1はサブネットを独自に管理しないといけないしちょっと大掛かりになるので、ライトにできる2の方法について書いておく。

やることは下記の2つ+オプションで1つ。

  1. 同一サブネット内にオレオレゲートウェイ(Linuxルーター)を構築してtransproxyをインストール&起動
  2. クライアントのデフォルトゲートウェイ設定を変更してオレオレゲートウェイに向ける
  3. (オプション) クライアントのスタティックルートの設定を変更してプライベートLAN向けの通信は 本物ゲートウェイ に向かせる

こうすると、本物ゲートウェイの前にオレオレゲートウェイが挟まり、クライアントからの通信は

           --> オレオレゲートウェイ(透過プロキシ) --> 本物ゲートウェイ -->
クライアント                                                            ...
           <-- オレオレゲートウェイ(透過プロキシ) <-- 本物ゲートウェイ <--

のような通信経路となり透過プロキシを通すことができる。

最後のオプションは、クライアントに対して別セグメントからアクセスする必要がある場合(サーバとして公開する場合など)に設定が必要となる。本物ゲートウェイはクライアントのMACアドレスをARPで知っているため、別セグメントからのパケットは本物ゲートウェイから直接クライアントに配送される。一方、クライアントからの応答はデフォルトゲートにオレオレゲートウェイを設定しているため、下記のようにオレオレゲートウェイを経由して返ることになる。

                   --> 本物ゲートウェイ ------------------------->
別セグメントのマシン                                               クライアント
                   <-- 本物ゲートウェイ <-- オレオレゲートウェイ <--

このように、往路・復路で異なる経路(非対称ルーティング: Asymmetric Routing)となると、通信機器によってはパケットがDROPされ別セグメントのマシンに応答が返ってこなくなる。
そこで、クライアントのスタティックルート設定でプライベートLAN向けの通信は 本物ゲートウェイ を通るように明示的に設定しておけば、

                   --> 本物ゲートウェイ ------------------------->
別セグメントのマシン                                               クライアント
                   <-- 本物ゲートウェイ <-------------------------

と非対称ではなくなり、外部から接続することも可能になる。

オレオレゲートウェイ(Linuxルーター)構築のポイント

ルーターとして機能させるため、IPフォワード許可の設定を入れておく。加えて、ICMPリダイレクトを送信しないようにも設定しておく。ルーターがICMPリダイレクトを送信してしまうと、クライアントがオレオレゲートウェイを経由せずに元の本物ゲートウェイ経由で外部セグメントに出るようになってしまうことがあるため。

/etc/sysctl.conf
net.ipv4.ip_forward = 1
net.ipv6.conf.all.forwarding = 1
net.ipv4.conf.eth0.send_redirects = 0
net.ipv4.conf.default.send_redirects = 0
net.ipv4.conf.all.send_redirects = 0

また、iptablesの設定&保存をしておく。

sudo iptables -t nat -A POSTROUTING -j MASQUERADE
sudo service iptables save

後はtransproxyを起動すればOK。
なお、AWS上で構築する場合は、EC2の設定で 送信元/送信先の変更チェック を入れておく必要あり。

起動オプションの使用例

最後に、transproxyの利用環境に合わせた起動オプションの使用例を紹介しておく。

DNSでパブリックドメイン、プライベートドメインの名前解決ができる場合

とっても幸せな環境。
たとえばAWSダイレクトコネクト環境だと、デフォルトのDNSで普通に外部の名前解決ができるっぽい。このような環境の場合、名前解決のためにDNS用の透過プロキシは不要なので、

sudo -E transproxy

のみで利用できる。

また、AWSダイレクトコネクト環境で、プライベートドメインを解決するために社内の既存のDNSサーバを使わないといけない場合は、下記のように内部DNSと外部DNSを指定しつつ、-dns-over-tcp-disabled オプションを指定することで、外部DNSもアップストリームのプロキシを経由しないようにできるようにしていおいた。

sudo -E transproxy -private-dns 192.168.0.100 -public-dns 172.16.0.2 -dns-over-tcp-disabled

DNSで外部の名前解決ができない場合

プロキシサーバを使用しないとインターネットに出れない環境の人はこれが普通だと思う。

プロキシ経由で53/tcpに接続できる場合

そこそこいい環境。この場合は、

sudo -E transproxy -private-dns 192.168.0.100 -public-dns 8.8.8.8

のように基本的な使い方でOK。外部DNSへのアクセスは、

クライアント --(tcp or udp)--> ゲートェイ(透過プロキシ) --(tcp)--> アップストリームのプロキシ --(tcp)--> DNSサーバ

のような通信経路で透過プロキシを通る際にUDPであってもDNS over TCPとなる。

プロキシ経由で53/tcpに接続できない場合

非常に辛い環境。だけど幸いなことに、GoogleがDNS over HTTPSなサービスを提供してくれている。これを利用して、HTTPSさえ許可されていれば透過的にDNSも解決できるようにした。

注意点として、利用先の外部DNSは選べないので、Googleを信用する必要あり。以下のように -dns-over-https-enabled オプションをつければ、外部DNSへの通信はGoogleのサービスを使うようになる。

sudo -E transproxy -private-dns 192.168.0.100 -dns-over-https-enabled

外部DNSへのアクセスは、

クライアント --(tcp or udp)--> ゲートェイ(透過プロキシ) --(https)--> アップストリームのプロキシ --(https)--> DNSサーバ

のような通信経路となる。

プロキシ経由で任意ポート/tcpに接続できる場合

(利用者にとって)大変よい環境。トンネリングでやりたい放題なので厳しめの所では許可されていないことが多いかも。
この場合、例えばSSHなんかも透過プロキシで利用できる。デフォルトで22番ポートを対象としているのでオプション指定は不要だが、明示的に指定する場合は下記のようになる。

sudo -E transproxy -private-dns 192.168.0.100 -public-dns 8.8.8.8 -tcp-proxy-dports 22

TCPで他のポート(例えば5000)にも外部アクセスする場合は、下記のようにカンマ区切りで複数設定可能としている。

sudo -E transproxy -private-dns 192.168.0.100 -public-dns 8.8.8.8 -tcp-proxy-dports 22,5000

  1. v0.3よりコマンド名がgo-transproxyからtransproxyに変わりました。