How to get the original address after NAT

ポート フォワードの後に受信したソケットから、オリジナルの宛先 IP アドレスをユーザー モード側で取得する方法がようやく分かりました。Linux のカーネル デバッグまで持ち出しましたが、結論はとてもあっさりしたものでした。答えは、getsockopt 関数にオプションとして SO_ORIGINAL_DST を渡すというもので、結果としてオリジナルの宛先情報が入った struct sockaddr が返ってきます。これは超便利!

SO_ORIGINAL_DST というオプションは <linux/netfilter_ipv4.h> で定義されているのですが、getsockopt の説明ではなぜか触れられていません。キーワードさえ分かってしまえば検索できるのですが、何も知らないとここまで辿りつくのは難しい、と思います。この前見つけた IP_RECVORIGDSTADDR とか IP_ORIGDSTADDR とは一体なんだったのか、という疑問も残ります。

getsockopt(3): socket options – Linux man page
http://linux.die.net/man/3/getsockopt

以下簡単に、カーネル デバッグからどうやってたどり着いたかを書いておきます。

まず目をつけたのが、nf_nat_ipv4_manip_pkt 関数でした。この関数で iph という iphdr 構造体の変数の中の宛先、または送信元のアドレスを書き換えているように見えます。実際にデバッガーで確かめると、iptables で設定した値でオリジナルのアドレスを書き換えていました。アドレスを書き換えているところの少し前で、l4proto->manip_pkt() を呼び出しています。プロトコルが TCP の場合は、ここから tcp_manip_pkt が呼ばれ、TCP のポートが書き換えられています。l4proto というのは、Layer 4 プロトコルのことで、TCP や UDP が該当するということです。けっこう分かりやすいです。

ユーザーモード側から欲しい情報は、ここで変更されてしまう iph 側に入っている値ですが、nf_nat_ipv4_manip_pkt は一方的に値を上書きしてしまうロジックになっていて、変更前の値をどこかに保存することはありません。最初は iphdr 構造体にオリジナルの値が入っているのかと思いましたが、この構造体はパケット内の IP ヘッダーそのものであり、オリジナルのアドレスは持っていません。したがって、この関数が呼ばれる時点では、既にオリジナルのアドレスはどこかに退避されているはずです。

そこで目をつけたのは、オリジナルが入っているけれども上書きされてしまう iph ではなく、転送先の情報を持っている nf_conntrack_tuple 構造体の target という変数です。target がどこから生まれるかを辿れば、その場所には iptables で設定した情報があるわけで、オリジナルの情報も管理されているだろう、という推測です。コール スタックを 1 フレーム遡ると、nf_nat_packet の中で、nf_ct_invert_tuplepr(&target, &ct->tuplehash[!dir].tuple) という呼び出し箇所があり、ここで target が設定されています。nf_ct_invert_tuplepr は destination と source を入れ替えているだけのようなので、NAT で置き換える情報は、ct->tuplehash に入っていると分かります。ct は nf_conn 構造体で、名前から Netfilter の Connection 情報を保持しているような感じです。これは期待できます。

そこで nf_nat_packet において、ct->tuplehash の値を見てみました。貼り付けてもあまり意味がないのですが、出力はこんな感じでした。

(gdb) p ((struct nf_conn*)$rdi)->tuplehash[0]
$12 = {hnnode = {next = 0x80000001, pprev = 0xffffe8ffffc01158}, tuple = {src = {u3 = {all = {1174513856, 0, 0, 0},
        ip = 1174513856, ip6 = {1174513856, 0, 0, 0}, in = {s_addr = 1174513856}, in6 = {in6_u = {
            u6_addr8 = "\300\25001F", ’00’ <repeats 11 times>, u6_addr16 = {43200, 17921, 0, 0, 0, 0, 0, 0},
            u6_addr32 = {1174513856, 0, 0, 0}}}}, u = {all = 40191, tcp = {port = 40191}, udp = {port = 40191},
        icmp = {id = 40191}, dccp = {port = 40191}, sctp = {port = 40191}, gre = {key = 40191}}, l3num = 2}, dst = {
      u3 = {all = {2886347368, 0, 0, 0}, ip = 2886347368, ip6 = {2886347368, 0, 0, 0}, in = {s_addr = 2886347368},
        in6 = {in6_u = {u6_addr8 = "h*\n\254", ’00’ <repeats 11 times>, u6_addr16 = {10856, 44042, 0, 0, 0, 0, 0,
              0}, u6_addr32 = {2886347368, 0, 0, 0}}}}, u = {all = 47873, tcp = {port = 47873}, udp = {port = 47873},
        icmp = {type = 1 ’01’, code = 187 ‘\273’}, dccp = {port = 47873}, sctp = {port = 47873}, gre = {
          key = 47873}}, protonum = 6 ’06’, dir = 0 ’00’}}}
(gdb) p ((struct nf_conn*)$rdi)->tuplehash[1]
$13 = {hnnode = {next = 0x33d9 <irq_stack_union+13273>, pprev = 0xdcbddf4e}, tuple = {src = {u3 = {all = {335653056,
          0, 0, 0}, ip = 335653056, ip6 = {335653056, 0, 0, 0}, in = {s_addr = 335653056}, in6 = {in6_u = {
            u6_addr8 = "\300\2500124", ’00’ <repeats 11 times>, u6_addr16 = {43200, 5121, 0, 0, 0, 0, 0, 0},
            u6_addr32 = {335653056, 0, 0, 0}}}}, u = {all = 14860, tcp = {port = 14860}, udp = {port = 14860}, icmp = {
          id = 14860}, dccp = {port = 14860}, sctp = {port = 14860}, gre = {key = 14860}}, l3num = 2}, dst = {u3 = {
        all = {1174513856, 0, 0, 0}, ip = 1174513856, ip6 = {1174513856, 0, 0, 0}, in = {s_addr = 1174513856}, in6 = {
          in6_u = {u6_addr8 = "\300\25001F", ’00’ <repeats 11 times>, u6_addr16 = {43200, 17921, 0, 0, 0, 0, 0,
              0}, u6_addr32 = {1174513856, 0, 0, 0}}}}, u = {all = 40191, tcp = {port = 40191}, udp = {port = 40191},
        icmp = {type = 255 ‘\377’, code = 156 ‘\234’}, dccp = {port = 40191}, sctp = {port = 40191}, gre = {
          key = 40191}}, protonum = 6 ’06’, dir = 1 ’01’}}}

32bit 整数になっていますが、3 種類の IPv4 アドレスの情報があります。このうち、104.42.10.172 が ubuntu-web.cloudapp.net を示すオリジナル アドレスです。192.168.1.70 はクライアントのアドレス、192.168.1.20 は NAT を行っているルーター自身のアドレスです。

2886347368 = 104.42.10.172
1174513856 = 192.168.1.70
335653056 = 192.168.1.20

ということで、欲しい情報は ct->tuplehash に入っていることが確認できました。ユーザーモード側にはソケットのファイル デスクリプターしかないので、これをもとに nf_conn 情報までたどり着くことができれば、値を取ってくることができます。

肝心なその後の記憶があやふやなのですが、何かのきっかけで以下の定義を見つけ、getorigdst 関数の定義を見るとまさに tuplehash の情報を返していました。そこで SO_ORIGINAL_DST で検索し、getsockopt に SO_ORIGINAL_DST を渡すと NAT 前のアドレスが取れると分かりました。流れはそんな感じです。

static struct nf_sockopt_ops so_getorigdst = {
    .pf     = PF_INET,
    .get_optmin = SO_ORIGINAL_DST,
    .get_optmax = SO_ORIGINAL_DST+1,
    .get        = getorigdst,
    .owner      = THIS_MODULE,
};

以前の記事で、socat というツールを使って、NAT されたパケットを明示的に指定した宛先にリレーして、透過プロキシの動作を実現させました。socat を少し書き換えて、SO_ORIGINAL_DST で取得した宛先に自動的にパケットをリレーできるようにしたのがこちらです。これで、443/tcp ポートであれば、どのサーバーに対するリクエストもト中継することができるようになりました。

https://github.com/msmania/poodim/tree/poodim-dev

以前は、VyOS を使ったルーターの他に、Squid というプロキシ用のサーバーと合わせて 2 台からなるルーティングの環境を作りました。よりシンプルな構成として、ルーティングとポート フォワードを 1 台のマシンで兼用することももちろん可能です。そこで、VyOS 上にルーティングの機能をまとめた環境を作ってみましたので、その手順を紹介します。VyOS ではなく、Ubuntu Server の iptables の設定だけで同様の環境を作ることもできると思います。

最終的には、このような構成になります。

capture2

前回の構成では、VyOS 上で Source NAT という機能を使って eth0 から eth1 へのルーティングを実現しました。その名の通り、宛先アドレスはそのままに、送信元のアドレスをルーターの eth1 側のアドレスに変更します。ここで送信元のアドレスを変更しなくても、eth1 からパケットを送信してしまえば、宛先になるサーバーのアドレスにパケットは届くことは届くでしょう。しかし、サーバーからの応答における宛先のアドレスが内部ネットワーク、すなわちルーターの eth0 側にあるクライアントアドレスになるので、サーバーからの応答はおかしなところに送信されてしまいます。サーバーからの応答が、正しくルーターの eth1 に返ってくるようにするため、Source NAT を行ないます。

Source NAT に加えて、PBR (= Policy Based Routing) の設定を行ない、443/tcp ポート宛てのパケットは、特例として Source NAT が行なわれる前に Squid サーバーにルーティングされるように設定しました。これはあくまでも MAC アドレス レベルの変更で、イーサネット フレームのレイヤーより上位層は変更されません。Squid サーバー側では、このルーティングされてきた 443/tcp ポート宛のパケットのポートを 3130/tcp に変更しました。このとき同時に、IP アドレスも Squid サーバーのアドレスに書き換わっているため、Squid サーバーで動くプログラムが受信できました。

1 台でこれを実現する場合、443/tcp ポート宛のパケットに対しては宛先 IP アドレスを自分、TCP ポートを 3130/tcp に変更するようなルールを作ることができれば OK です。この機能は、宛先を書き換えるので Destination NAT と呼ばれます。

User Guide – VyOS
http://vyos.net/wiki/User_Guide

あとは VyOS のユーザー ガイドに沿って設定するだけです。まずは前回の PBR の設定を消します。全部消してから一気に commit しようとするとエラーになるので、 無難に 1 つずつ commit して save します。

$ configure
# delete interfaces ethernet eth0 policy route PROXYROUTE
# commit
# delete protocols static table 100
# commit
# delete policy route PROXYROUTE
# commit
# save
# exit
$

Destination NAT の設定を行ないます。eth0 だけでなく、外から 443/tcp 宛てのパケットが来た場合も、同じように自分自身の 3130/tcp ポートに転送するように設定しておきます。iptables の REDIRECT の設定とは異なり、translation address も明示的に設定する必要があります。最終的には同じようなコマンドが netfilter に届くのだと思いますが。

$ configure
# set nat destination rule 100 description ‘Port forward of packets from eth0’
# set nat destination rule 100 inbound-interface ‘eth0’
# set nat destination rule 100 protocol ‘tcp’
# set nat destination rule 100 destination port ‘443’
# set nat destination rule 100 translation port ‘3130’
# set nat destination rule 100 translation address ‘10.10.90.12’
# set nat destination rule 110 description ‘Port forward of packets from eth1’
# set nat destination rule 110 inbound-interface ‘eth1’
# set nat destination rule 110 protocol ‘tcp’
# set nat destination rule 110 destination port ‘443’
# set nat destination rule 110 translation port ‘3130’
# set nat destination rule 110 translation address ‘10.10.90.12’
# commit
# save
# exit
$

まだ、VyOS 上で 3130/tcp をリッスンするプログラムが無いので、クライアントから HTTPS のサイトへはアクセスできません。次に、SO_ORIGINAL_DST オプションを使うように変更した socat をインストールします。

といっても、VyOS にはまだ gcc などの必要なツールが何もインストールされていないので、パッケージのインストールから始めます。VyOS では apt-get コマンドが使えますが、既定のリポジトリにはほとんど何も入っていないので、リポジトリのパスを追加するところからやります。どのリポジトリを使ってもいいのですが、他のマシンが Ubuntu なので Ubuntu のリポジトリを設定しました。

$ configure
# set system package repository trusty/main components main
# set system package repository trusty/main url
http://us.archive.ubuntu.com/ubuntu/
# set system package repository trusty/main distribution trusty
# set system package repository trusty/universe components universe
# set system package repository trusty/universe url
http://us.archive.ubuntu.com/ubuntu/
# set system package repository trusty/universe distribution trusty
# commit
# save
# exit
$

あとは Ubuntu と同じです。まずはリポジトリから最新情報を持ってきます。すると、2 つのパッケージで公開鍵がないというエラーが起きます。

vyos@vyos:~$ sudo apt-get update
Get:1
http://us.archive.ubuntu.com trusty Release.gpg [933 B]
Get:2
http://us.archive.ubuntu.com/ubuntu/ trusty/main Translation-en [943 kB]
Hit
http://packages.vyos.net helium Release.gpg
Ign
http://packages.vyos.net/vyos/ helium/main Translation-en
Hit
http://packages.vyos.net helium Release
Hit
http://packages.vyos.net helium/main amd64 Packages
Get:3
http://us.archive.ubuntu.com/ubuntu/ trusty/universe Translation-en [5063 kB]
Get:4
http://us.archive.ubuntu.com trusty Release [58.5 kB]
Ign
http://us.archive.ubuntu.com trusty Release
Get:5
http://us.archive.ubuntu.com trusty/main amd64 Packages [1743 kB]
Get:6
http://us.archive.ubuntu.com trusty/universe amd64 Packages [7589 kB]
Fetched 15.4 MB in 13s (1119 kB/s)
Reading package lists… Done
W: GPG error:
http://us.archive.ubuntu.com trusty Release: The following signatures couldn’t be verified because the public key is not available: NO_PUBKEY 40976EAF437D05B5 NO_PUBKEY 3B4FE6ACC0B21F32
vyos@vyos:~$

今回使うパッケージではないので無視してもいいですが、気持ち悪いので対応しておきます。

vyos@vyos:~$ sudo apt-key adv –keyserver keyserver.ubuntu.com –recv-keys 40976EAF437D05B5
Executing: gpg –ignore-time-conflict –no-options –no-default-keyring –secret-keyring /etc/apt/secring.gpg –trustdb-name /etc/apt/trustdb.gpg –keyring /etc/apt/trusted.gpg –primary-keyring /etc/apt/trusted.gpg –keyserver keyserver.ubuntu.com –recv-keys 40976EAF437D05B5
gpg: requesting key 437D05B5 from hkp server keyserver.ubuntu.com
gpg: key 437D05B5: public key "Ubuntu Archive Automatic Signing Key <ftpmaster@ubuntu.com>" imported
gpg: no ultimately trusted keys found
gpg: Total number processed: 1
gpg:               imported: 1
vyos@vyos:~$ sudo apt-key adv –keyserver keyserver.ubuntu.com –recv-keys 3B4FE6ACC0B21F32
Executing: gpg –ignore-time-conflict –no-options –no-default-keyring –secret-keyring /etc/apt/secring.gpg –trustdb-name /etc/apt/trustdb.gpg –keyring /etc/apt/trusted.gpg –primary-keyring /etc/apt/trusted.gpg –keyserver keyserver.ubuntu.com –recv-keys 3B4FE6ACC0B21F32
gpg: requesting key C0B21F32 from hkp server keyserver.ubuntu.com
gpg: key C0B21F32: public key "Ubuntu Archive Automatic Signing Key (2012) <ftpmaster@ubuntu.com>" imported
gpg: no ultimately trusted keys found
gpg: Total number processed: 1
gpg:               imported: 1  (RSA: 1)

もう一度 apt-get update します。今度はうまくいきました。

vyos@vyos:~$ sudo apt-get update
Get:1
http://us.archive.ubuntu.com trusty Release.gpg [933 B]
Hit
http://us.archive.ubuntu.com/ubuntu/ trusty/main Translation-en
Hit
http://us.archive.ubuntu.com/ubuntu/ trusty/universe Translation-en
Get:2
http://us.archive.ubuntu.com trusty Release [58.5 kB]
Get:3
http://us.archive.ubuntu.com trusty/main amd64 Packages [1743 kB]
Hit
http://packages.vyos.net helium Release.gpg
Ign
http://packages.vyos.net/vyos/ helium/main Translation-en
Hit
http://packages.vyos.net helium Release
Hit
http://packages.vyos.net helium/main amd64 Packages
Get:4
http://us.archive.ubuntu.com trusty/universe amd64 Packages [7589 kB]
Fetched 9333 kB in 6s (1476 kB/s)
Reading package lists… Done
vyos@vyos:~$

必要なパッケージをインストールします。

$ sudo apt-get install vim
$ sudo apt-get install build-essential libtool manpages-dev gdb git
$ sudo apt-get install autoconf yodl

VyOS に既定で入っている Vi は、Tiny VIM という必要最小限の機能しか持たないもので、例えば矢印キーが使えない、ソースコードの色分けをやってくれない、などいろいろ不便なので、Basic VIM を入れます。

socat を Git からクローンすると、configure ファイルが含まれていないので、autoconf で作る必要があります。yodl は、socat のビルドに必要でした。マニュアルの HTML 生成に必要なようです。

インストールが終わったら、あとはリポジトリをクローンしてビルドします。

$ git clone -b poodim-dev https://github.com/msmania/poodim.git poodim
$ cd poodim/
$ autoconf
$ CFLAGS="-g -O2" ./configure –prefix=/usr/local/socat/socat-poodim
$ make

わざわざインストールする必要もないので、そのまま実行します。-d を 3 つ付けると、Info レベルまでのログを出力します。

vyos@vyos:~/poodim$ ./socat -d -d -d TCP-LISTEN:3130,fork TCP:dummy.com:0
2015/01/16 05:45:08 socat[7670] I socat by Gerhard Rieger – see http://www.dest-unreach.org
2015/01/16 05:45:08 socat[7670] I setting option "fork" to 1
2015/01/16 05:45:08 socat[7670] I socket(2, 1, 6) -> 3
2015/01/16 05:45:08 socat[7670] I starting accept loop
2015/01/16 05:45:08 socat[7670] N listening on AF=2 0.0.0.0:3130

前回と違うのは、転送先のアドレスとポート番号に適当な値を指定しているところです。3130/tcp 宛てのパケットを受信すると、そのソケットに対して SO_ORIGINAL_DST オプションを使ってオリジナルの宛先情報を取得し、そこにパケットを転送します。これが今回の変更の目玉であり、これによって、任意のアドレスに対する 443/tcp の通信を仲介することができるようになりました。

例えばクライアントから google.com に繋ぐと、新たに追加した以下のログが出力され、ダミーの代わりにオリジナルのアドレスに転送されていることが分かります。

2015/01/16 05:50:24 socat[7691] N forwarding data to TCP:216.58.216.142:443 instead of TCP:dummy.com:0

これで、宛先アドレスの問題が解決し、完全な透過プロキシとしての動作が実現できました。

実はこの改変版 socat には、もう一点仕掛けがあります。2015/1/15 現在は、Google 検索のページには接続できますが、同じく HTTPS 通信を行う facebook.com や twitter.com には接続できないはずです。

かなり雑な実装ですが、TLSv1.x のプロトコルによる Client Hello を検出したら、パケットの内容を適当に改竄してサーバーが Accept しないように細工を行ないました。これによって何が起こるかというと、クライント側から見たときには TLSv1.x のコネクションは常に失敗するので唯一の選択肢である SSLv3.0 の Client Hello を送るしかありません。一方のサーバー側では、SSLv3.0 の Client Hello がいきなり送られてきたようにしか見えません。結果として、クライアント、サーバーがともに TLSv1.x をサポートしているにもかかわらず、MITM 攻撃によって強制的に SSLv3.0 を使わせる、という手法が成り立ちます。

昨年末に POODLE Attack という名前の脆弱性が世間を騒がせましたが、これは SSLv3.0 には根本的な脆弱性が存在し、もはや使用するのは危険になったことを意味します。さらに、クライアントとサーバーが TLS に対応しているだけでも不十分である、ということが今回の例から分かると思います。簡単に言えば、クライアントもサーバーも、SSLv3.0 を使えるべきではないのです。

現在最新の Chrome、IE、Firefox では、ブラウザーがこのフォールバックを検出した時点で、サイトへのアクセスはブロックされます。

Google、「Chrome 40」でSSL 3.0を完全無効化へ – ITmedia エンタープライズ
http://www.itmedia.co.jp/enterprise/articles/1411/04/news050.html

IE 11の保護モード、SSL 3.0へのフォールバックをデフォルトで無効に – ITmedia エンタープライズ
http://www.itmedia.co.jp/enterprise/articles/1412/11/news048.html

サーバー側では、2014 年 10 月半ばにリリースされた Openssl-1.0.1j において、フォールバック検出時に新たなエラー コードを返す実装が追加されています。Apache など、OpenSSL を SSL のモジュールとして使うサーバーは、この機能によって対応がなされると考えられます。この OpenSSL の防御機能は、パケットからアラート コードを見ると判断できます。実際に確かめていませんが、twitter.com と facebook.com は OpenSSL で防御されているはずです。

OpenSSL Security Advisory [15 Oct 2014]
https://www.openssl.org/news/secadv_20141015.txt

この次のステップとしては、当然 socat に POODLE を実装することなのですが、もし実装できたとしてもさすがに公開する勇気は・・。

広告

Setting up a transparent proxy over SSL #3 – squid again

前回は非透過プロキシを構築しました。クライアントから送られるパケットの宛先が Web サーバーではなくプロキシ サーバーになっていて、プロキシはペイロードの内容を「解釈して」 宛先を割り出すという動作がポイントでした。

3. squid による HTTP 透過プロキシ

透過プロキシ (ここでは intercepting proxy を意味します) の場合、クライアントにはプロキシが存在することを教えません。したがってクライアントは、ただ単に Web サーバーに向けて HTTP 要求を送信します。何もしなければ HTTP 要求は Client –> VyOS –> 物理ルーター、という経路で外に流れてしまい、Squid には流入しません。つまり、透過プロキシを動作させるためには、ルーティングを少し捻じ曲げる必要があります。

Capture

設定で参考にさせていただいたページがこちら。

vyattaを使って透過型プロキシ環境を構築する時の設定内容
http://www.virment.com/vyatta/vyatta-proxy/12/

squidを透過型プロキシとして使う時の設定
http://www.virment.com/vyatta/vyatta-proxy/15/

ネットワーク構成はほぼ同じ、実行するコマンドもかなり共通しています。VyOS 上で PBR (Policy Based Routing) の設定を行ない、特定の条件に合致するパケットは eth1 に流すのではなく Squid に流すように設定します。さらに、Squid に流入したパケットが透過プロキシ用の待機ポートである 3129/tcp (前回の記事で squid.conf に設定しています) に流れるように、Squid サーバー上で iptables コマンドを使ってポート転送の設定をします。

VyOS 側の設定コマンドは以下の通りです。

$ configure
# set protocols static table 100 route 0.0.0.0/0 next-hop 10.10.90.11
# set policy route PROXYROUTE rule 100 protocol tcp
# set policy route PROXYROUTE rule 100 destination address 0.0.0.0/0
# set policy route PROXYROUTE rule 100 destination port 80
# set policy route PROXYROUTE rule 100 source address 10.10.0.0/16
# set policy route PROXYROUTE rule 100 source mac-address !00:15:5d:01:02:12
# set policy route PROXYROUTE rule 100 set table 100
# set interfaces ethernet eth0 policy route PROXYROUTE
# commit
# save
# exit
$

ここで設定したルールは以下のようなものです。言い換えると、「VyOS に対して Squid サーバー以外の Internal ネットワーク内アドレスから HTTP パケット (80/tcp) が来たら Squid に流す」 という設定です。

  • table 100
    – 10.10.90.11 (= Squid サーバー) にルーティング
  • rule 100 (ポリシー名 PROXYROUTE)
    – TCP パケットである
    – 宛先 TCP ポートが 80 である
    – 送信元の IP アドレスが 10.10.0.0/16 ネットワークに所属している
    – 送信元の MAC アドレスが 00:15:5d:01:02:12 (=Squid サーバーのもの) ではない
    – 上記に該当するパケットはルーティングの table 100 を適用
  • Internal ネットワーク側の eth0 に対して PROXYROUTE を適用

これだけだと、Squid に来たパケットの宛先ポートはまだ 80/tcp になっているので、透過プロキシのターゲットである 3129/tcp に変更する必要があります。Squid サーバー上で以下のコマンドを実行します。

$ sudo iptables -t nat -A PREROUTING -i eth0 -p tcp –dport 80 \
> -j REDIRECT –to-port 3129

設定は以上です。Squid 側は、前回の状態から変更はありません。squid が起動していなければ起動しておいてください。また、クライアントからプロキシの設定を消すのを忘れないように。

image
透過プロキシなのでプロキシ設定は削除

クライアントでブラウザーを開いて HTTP のページにアクセスすると、プロキシの設定をしていないにも関わらず、Squid のアクセス ログにログが記録されます。これが透過プロキシの動作です。この状態で HTTPS にアクセスするとどうなるでしょうか。もちろんアクセスできます。なぜなら何も設定していないため、単純に VyOS を素通りしているだけだからです。

ここで Squid の話は終わってもよいのですが、実は設定時に少し違和感がありました。まあ私自身が iptables に明るくないからなんでしょうけど。ふと、「なぜポート転送したパケットを Squid の 3129/tcp が受信することができるのか」 という疑問が浮かびました。

クライアントから送られるパケットの旅を考えてみます。今回は非透過プロキシであり、クライアントから送信された時点でパケットは Web サーバーを宛先としています。VyOS で PBR が適用されて、Squid に送られます。このルーティングはどのように実現されているでしょうか。もし、Squid 上でポートが変更されたときに宛先の IP アドレスが Web サーバーのままだったら、そのアドレスはどう考えても Squid サーバーのアドレスである 10.10.90.11 とは似ても似つかないものであるわけで、ただ単に 3129/tcp ポートが一致したからといって Squid が受信できるのはおかしいだろう、という疑問です。

上記疑問を調べるため、Squid 上の iptables の設定を削除し、代わりに Squid が 80/tcp ポートで非透過プロキシを処理するように設定します。

iptables の削除は、-A を -D に置き換えて実行するだけです。-L オプションで、現在のルールの一覧を確認できます。

$ sudo iptables -t nat -D PREROUTING -i eth0 -p tcp –dport 80 \
> -j REDIRECT –to-port 3129

$ sudo iptables -t nat -L
Chain PREROUTING (policy ACCEPT)
target     prot opt source               destination

Chain INPUT (policy ACCEPT)
target     prot opt source               destination

Chain OUTPUT (policy ACCEPT)
target     prot opt source               destination

Chain POSTROUTING (policy ACCEPT)
target     prot opt source               destination

/usr/local/squid/current/etc/squid.conf を開き、ポートの部分を 3129 から 80 に変更します。

# Squid normally listens to port 3128
http_port 3128
http_port 80 transparent

squid を再起動します。以下のログから、今度は 80/tcp が透過モードでの待機ポートであることが分かります。

2015/01/08 17:46:10| Accepting HTTP Socket connections at local=[::]:3128 remote=[::] FD 9 flags=9
2015/01/08 17:46:10| Accepting NAT intercepted HTTP Socket connections at local=[::]:80 remote=[::] FD 10 flags=41

クライアントから HTTP のページにアクセスします。今度は "This page can’t be displayed" エラーで繋がらないはずです。これが意味することは、ポートが一致しているだけでは Squid はパケットを受信できない、ということです。また、VyOS の PBR は、宛先をアドレスを Squid の 10.10.90.11 には変更していないようです。

ということは、iptables によるポート転送は、ポートだけではなく宛先アドレスも自分のものに変更するのでしょうか。ちょっとこれは確かめようがありません。iptables の実装を見るのはさすがにしんどい。

とりあえず 3129/tcp ポートを使う設定に戻してから、今度は Client、VyOS、Squid、Web サーバーの 4 点でパケット キャプチャーを行ない、様子を確認することにします。Web サーバーは Azure 上に作った仮想マシン (http://ubuntu-web.cloudapp.net/) を使います。

Ubuntu Server 上でのキャプチャーには tcpdump コマンドが使えます。作成されたファイルは Network Monitor や Wireshark で開くことができます。

$ sudo tcpdump -nn -s0 -i eth0 -w squid.cap

ここで少し注意。VyOS には eth0 と eth1 の 2 枚の NIC があります。tcpdump コマンドで -i any オプションを使うと、全ての NIC 上でキャプチャーを開始することができます、がしかし、パケットのイーサネット フレームが正確に記録されません。

For example, the "any" device on Linux will have a link-layer header type of DLT_LINUX_SLL even if all devices on the system at the time the "any" device is opened have some other data link type, such as DLT_EN10MB for Ethernet.

— Manpage of PCAP http://www.tcpdump.org/pcap3_man.html

ここで出てくる DLT_LINUX_SLL とは・・・

DLT_LINUX_SLL – Linux "cooked" capture encapsulation

— pcap(3): Packet Capture library – Linux man page http://linux.die.net/man/3/pcap

実際に any でキャプチャーして Network Monitor で開いた様子を以下に示します。イーサネット フレーム部分が Linuxecookedmode という名前で表示され、MAC アドレスは送信元の情報だけが記録されています。イーサネット部分を見ない場合はこれでよいですが、今回はこれだと不便です。tcpdump では -i any は使わず、VyOS 上では eth0 と eth1 それぞれのために 2 つの tcpdump プロセスを起動することにします。

image

そんなこんなでキャプチャーを取り、HTTP GET メソッドの要求と応答に着目し、送信元及び送信先のアドレスについてまとめました。細かいので Excel のキャプチャー画像で。

image

 
ルーティングによって、MAC アドレスや IP アドレスが変更されている様子が確認できます。ポートについては、宛先は常に 80 を指していますが、クライアント側の動的ポートは 53281 と 33788 の 2 つが見えます。前者が Windows クライアント上のポートであり、後者は Squid サーバー上の動的ポートです。

一つ気が付くのは、透過プロキシ用のポートである 3129/tcp ポートが見えないことです。上記テーブルの 3 行目、VyOS から Squid に転送されたパケットの宛先 IP アドレスは依然として ubuntu-web.cloudapp.net を名前解決した結果である 104.42.10.172 を指していますが、ポートも 80 のままです。このことから、tcpdump コマンドは iptables の PREROUTING チェーンが処理される前にパケットを取得していると考えられます。ポート転送された後のパケット内容がどうなっているかを、tcpdump から見ることはできません。

Setting up a transparent proxy over SSL #1 – vyos

とあるテストのため、ルーターやらプロキシをごにょごにょ導入して環境を作りました。長くなりそうなので、先に作業内容を書いておきます (何せ書き溜めていないものでして・・)。参考 URL などは、都度掲載します。

  1. vyos によるソフトウェア ルーティング
  2. squid による HTTP/HTTPS 非透過プロキシ
  3. squid による HTTP 透過プロキシ
  4. socat による TCP リレー

1. vyos によるソフトウェア ルーティング

今の時代、多くの人は自宅にインターネット環境を持っていて、ルーターと呼ばれる小さな機械をプロバイダーからレンタルしていると思います。ルーターは、インターネット (英語だと、"the Internet" のように、固有名詞として先頭が大文字になり、the が付く) という地球規模の単一のネットワークと、自宅内の小規模なネットワーク (ルーターとパソコンが 1 台あれば、それはもうネットワークと呼べる) を繋ぐもので、ただ単にパケットを右から左へ受け流しているだけでなく、パケット内のアドレス情報を変更するというとても大事な仕事を行っています。最近のルーターは、IP マスカレードやファイアウォールなど、多くの機能が付随してきますが、ルーターの基本の機能は、宛先や差出人のアドレスをうまいこと調整することです。

ただし、自宅からプロバイダーを経由してインターネットにつなぐ場合、普通はユーザー アカウントの認証が必要です。認証は、PPPoE (Point to Point Protocol over Ethernet) というプロトコルで行われます。インターネットに繋ぐときのルーターは、IP レベルのルーティングを行なう単純なルーターに加え、PPPoE クライアントとしての機能を持つ、ブロードバンド ルーターという種別になります。

と、断言したものの、あまり自信がない。詳細は wiki を見てください。

    ルーター – Wikipedia
    http://ja.wikipedia.org/wiki/%E3%83%AB%E3%83%BC%E3%82%BF%E3%83%BC
    ほとんどの人は意識していないと思いますが、ルーターの機械の中では、メインの仕事、すなわちルーティングを行っているソフトウェアが動いています。wiki によると、Unix が使われているものが多いらしいです。つまり、ルーターは NIC (= Network Interface Controller, LAN アダプターとも呼ばれる) が 2 枚刺さったパソコンと言うこともでき、(NIC が 2 枚あれば) 普通のパソコンでも、ルーターの提供元が用意した Unix の代わりに Linux (Windows でも可能) を入れてルーターとして使うことができます。

今回は、ルーティング ソフトとして VyOS という Linux ディストリビューションを使うことにしました。以前は Vyatta というルーター OS があったらしいですが、買収されて有償化されてしまい、無償版が VyOS としてフォークされた、ということが wiki に書いてありました。

VyOS
http://vyos.net/wiki/Main_Page

本家のサイトから、ISO をダウンロードします。2015/1/7 現在の最新版は 1.1.1 のようです。amd64 用の ISO をダウンロードします。ダウンロード後は、本家の User Guide がとても親切なので、それにしたがって実行するだけです。

http://vyos.net/wiki/User_Guide

インストール先は、Hyper-V の仮想マシンにします。User Guide に、512MB RAM、2GB ストレージが推奨と書いてあるので、その通りに仮想マシンを作ります。NIC を 2 枚にするのを忘れないように。

image

ISO をセットして起動すると、ISO から vyos が起動してログインを求められるので、ユーザー名 vyos、パスワード vyos を入力します。ログイン後、シェルが起動するので、install image コマンドを実行します。ここまで User Guide 通りです。

image
ログインして install image まで実行したところ

ここからも User Guide 通りです。

このあといろいろと聞かれますが、User Guide 通りに進めます。単なるルーターですし。唯一、イメージ名だけデフォルトの 1.1.1 ではなく vyos-1.1.1 に変えました。

image
インストールが終わったところ

インストールが終わったら、User Guide では reboot になっていますが、再起動するとまたすぐに ISO から vyos が起動してしまうので、シャットダウンして ISO を抜いてから再起動することにします。シャットダウンは poweroff コマンドです。shutdown コマンドは存在しません。

起動してログインしたら、ネットワーク設定と SSH サーバーの設定を行います。

仮想マシンに刺してある 2 枚の NIC は、1 枚目が Internal、2 枚目が External の Hyper-V 仮想スイッチに繋がるようにしたので、VyOS から見ると eth0 が Internal、eth1 が External になります。Internal ネットワークは 10.10.0.0/16、External は 10.0.0.0/24 のアドレス空間が割り当てられています。VyOS の eth0 には 10.10.90.12 のアドレスを割り当て、External の eth1 には、自宅の物理ルーター (10.0.0.1) をサーバーとする DHCP を利用することにします。

設定と言っても、以下のコマンドを実行するだけです。Ubuntu よりも簡単ですね。

$ configure

# set interfaces ethernet eth0 address ‘10.10.90.12/16’
# set interfaces ethernet eth0 description ‘Internal’
# set interfaces ethernet eth1 address dhcp
# set interfaces ethernet eth1 description ‘External’

# set service ssh port ’22’
# set service ssh listen-address 10.10.90.12

# commit
# save
# exit

これで SSH が使えるようになったので、10.10.90.12 に対して Teratern で繋ぎにいきます。なお VyOS には ifconfig コマンドもありません。IP アドレスや MAC アドレスは show interfaces system コマンドで見ることができます。

vyos@vyos:~$ show interfaces system
eth0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc pfifo_fast state UP group default qlen 1000
    link/ether 00:15:5d:01:02:10 brd ff:ff:ff:ff:ff:ff
    inet 10.10.90.12/16 brd 10.10.255.255 scope global eth0
       valid_lft forever preferred_lft forever
    inet6 fe80::215:5dff:fe01:210/64 scope link
       valid_lft forever preferred_lft forever

    RX:  bytes    packets     errors    dropped    overrun      mcast
         73227        834          0          0          0          0
    TX:  bytes    packets     errors    dropped    carrier collisions
         79657        566          0          0          0          0

eth1: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc pfifo_fast state UP group default qlen 1000
    link/ether 00:15:5d:01:02:11 brd ff:ff:ff:ff:ff:ff
    inet 10.0.0.42/24 brd 10.0.0.255 scope global eth1
       valid_lft forever preferred_lft forever
    inet6 fe80::215:5dff:fe01:211/64 scope link
       valid_lft forever preferred_lft forever

    RX:  bytes    packets     errors    dropped    overrun      mcast
         59125        422          0          0          0          0
    TX:  bytes    packets     errors    dropped    carrier collisions
          5522         58          0          0          0          0

lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN group default
    link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
    inet 127.0.0.1/8 scope host lo
       valid_lft forever preferred_lft forever
    inet6 ::1/128 scope host
       valid_lft forever preferred_lft forever

    RX:  bytes    packets     errors    dropped    overrun      mcast
         78208       1274          0          0          0          0
    TX:  bytes    packets     errors    dropped    carrier collisions
         78208       1274          0          0          0          0

では、いよいよルーティングの設定に移ります。コマンドを実行する前に、ここで実現したいことを確認します。

今 VyOS をインストールした仮想マシンに加え、もう一台 Windows が入った仮想マシンがあり、こちらには Internal の NIC が一枚だけ刺さっているとします。

  • Hyper-V ゲスト #1  (今 VyOS をインストールした)
    – Network Adapter#1 (Internal)
    – Network Adapter#2 (External)
  • Hyper-V ゲスト #2 (単なる Windows マシン)
    – Network Adapter (Internal)

このままだと、この Windows マシンからはインターネットにアクセスできません。Internal は Hyper-V 仮想マシン同士、及び仮想マシンと Hyper-V ホストとを通信可能にする閉じたネットワークであるためです。(下記の "内部仮想ネットワーク" 参照)

仮想ネットワークを構成する
http://technet.microsoft.com/ja-jp/library/cc816585(v=WS.10).aspx

インターネットに接続しているのは自宅にある物理ルーターであり、その物理ルーターに接続している物理ネットワーク、すなわち Hyper-V の外部仮想ネットワークです。ここで、Internal とExternal との両方へのアクセスを持つ VyOS をルーターとして経由することで、Internal としか接点を持たない Windows もインターネットに接続させることができます。

実は、ルーティングの設定はとても簡単です。さすが特化型 OS です。configure モードでコマンドを 3 つ実行するだけです。

$ configure

# set nat source rule 100 outbound-interface ‘eth1’
# set nat source rule 100 source address ‘10.10.0.0/16’
# set nat source rule 100 translation address ‘masquerade’
# commit
# save
# exit
$

VyOS 側の設定はこれだけです。再起動も不要です。

ただし、これだけでは目的は達成できず、Windows 側にも設定が必要です。なぜなら、この時点で Windows はルーターの位置を知らないからです。ルーターの位置を教えるためには、NIC の設定にあるデフォルト ゲートウェイのところに VyOS の内部ネットワーク側の IP アドレスである 10.10.90.12 を指定します。

image

これで、Windows が 10.10.0.0/16 のネットワークの外にあるアドレスにアクセスしようとすると、パケットは VyOS に転送されて、うまいことルーティングしてくれるはずです。

が、ブラウザーからインターネット (ここでは bing.com) に接続しようとすると、"This page can’t be displayed" エラーで怒られます。これの理由は、インターネットに接続する以前に bing.com を名前解決できなかったためです。ルーティングの設定が正しくても、そもそもアクセスしようとする場所のアドレスが分からないので、ルーティングが起こる前にエラーになっています。

ということは、IP を直打ちすれば外部にアクセスできるはずです。例えば、Google が持つ DNS サーバーの IP アドレスが 8.8.8.8 であることは比較的有名です。そこで、nslookup を使って名前解決を試します。

C:\MSWORK> nslookup bing.com. 8.8.8.8
Server:  google-public-dns-a.google.com
Address:  8.8.8.8

Non-authoritative answer:
Name:    bing.com
Address:  204.79.197.200

無事動きました。ゲートウェイの設定とルーティングは問題ないようです。

そこで、解決策の一つとしては、ゲートウェイの設定に加えて DNS サーバー参照の設定に 8.8.8.8 などの外部 DNS サーバーを追加する方法が考えられます。これでブラウザーから bing.com にアクセスできるようになります。

image
とりあえず Google に頼っておく場合の設定

今回の環境は、クライアントが Active Directory ドメイン contoso.nttest.microsoft.com に所属しており、そのドメイン コントローラー (IP=10.10.10.10) に DNS サーバーの役割もインストールしてあります。クライアントがドメイン所属のコンピューターとして正しく動作するためには、内部の DNS サーバーにアクセスできなければいけません。そこで、ドメイン コントローラー兼 DNS サーバーのマシンにデフォルト ゲートウェイの設定を行ない、外部ホスト名の名前解決に関しては、内部 DNS サーバー経由で名前解決ができるようにします。

クライアント側の優先 DNS サーバーを 10.10.10.10 に設定します。

image
内部 DNS サーバー 10.10.10.10 を見に行くように設定

次に、DNS サーバーが外部にアクセスできるように、ドメイン コントローラー兼 DNS サーバーのマシン上でもデフォルト ゲートウェイの設定を追加します。OS は Windows Server 2012 です。

image

DNS サーバー サービス上の設定は不要です。内部 DNS サーバーから見て、もし自分の知らないレコードの解決を求められた場合、まずは DNS フォワーダーを見に行きます。フォワーダーが設定されていれば、クエリを丸投げします。フォワーダーが使えない場合、ルート ヒントが使われます。ルート ヒントには、予めインターネットのルート ネーム サーバーの IP アドレスが設定されています。その他、Windows Server の DNS には条件付きフォワーダーというのもあります。

フォワーダーとは
http://technet.microsoft.com/ja-jp/library/cc730756.aspx

フォワーダーを使用する
http://technet.microsoft.com/ja-jp/library/cc754931.aspx

ルートサーバ – Wikipedia
http://ja.wikipedia.org/wiki/%E3%83%AB%E3%83%BC%E3%83%88%E3%82%B5%E3%83%BC%E3%83%90

今回の環境において、内部 DNS サーバーにフォワーダーの設定はしていません。

image

しかるべき順序にしたがって、クライアントから来た DNS クエリはルートヒントに転送されることになります。この内部 DNS サーバーも Internal のネットワークにしか繋がっていないので、ルートヒントへの転送は VyOS を経由して送られます。

image
デフォルトのルート ヒント設定

試しに DNS 名前解決の様子をキャプチャーしてみました。以下の例では、ルート ヒントの一つである 192.203.230.10 (e.root-servers.net) が使われていることが確認できます。パケットの宛先 IP アドレスはゲートウェイの 10.10.90.12 ではなく、最終目的地であるルート サーバーになっているところがポイントでは。ではなぜこのパケットがゲートウェイに届くかというと、パケットの宛先 MAC アドレスが VyOS の eth0 である 00:15:5d:01:02:10 になっているためです。

image

ここでは試しませんが、ルート ヒントの代わりに、フォワーダーとして 8.8.8.8 を設定してもよいです。