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 を実装することなのですが、もし実装できたとしてもさすがに公開する勇気は・・。

広告

Pitfalls when debugging Netfilter

Linux カーネル デバッグ実践の第一弾、ということで iptables、すなわち netfilter に手を出しています。思っていたほど魔境ではありませんでした。1 つ 1 つの関数は短いですし、言語が C++ じゃなく C ですし。仕事で読んでいるコードのほうが魔境であることが再確認できました。ただ、コードそのものよりも、vi や bash などのツールに慣れていないので時間がかかります。

デバッグを進めるうちに、Linux カーネル デバッグ特有の罠に嵌ったので、解決方法とともに紹介したいと思います。今後もこのシリーズが続きそうです。

開発環境 (debugger) とテスト環境 (debuggee) は前回の記事の環境をそのまま使います。VMware ESXi 上の Ubuntu Server 14.04.1 に Linux 3.18.2 をインストールした環境です。

1. iptables コマンドで nat テーブルが存在しないと怒られる

今回のデバッグで目指しているのは、iptables でポート フォワードしたときの、オリジナルの宛先 IP アドレスを求める方法を探ることです。背景などの詳細はこちらの記事をご参照下さい。

Setting up a transparent proxy over SSL #4 – netcat/socat | すなのかたまり
https://msmania.wordpress.com/2015/01/10/setting-up-a-transparent-proxy-over-ssl-4-netcatsocat/

何はともあれ、まずは新しい Linux カーネルで上記記事と同じ環境を作るので、iptables コマンドを実行してみました。

$ sudo iptables -t nat -A PREROUTING -i eth0 -p tcp –dport 443 -j REDIRECT –to-port 3130
iptables v1.4.21: can’t initialize iptables table `nat’: Table does not exist (do you need to insmod?)
Perhaps iptables or your kernel needs to be upgraded.

いきなり iptables コマンドがエラーになります。悪い冗談はやめて欲しいですね。

ここでけっこう詰まったのですが、結論から言うと、ビルドしたときの config パラメーターが不足していました。.config ファイルを直接開いて、それっぽい項目を探すと、CONFIG_IP_NF_NAT という設定がコメントになっています。

# CONFIG_IP_NF_NAT is not set

でも make oldconfig したのになぜ・・という話ですが、カーネルが 3.16 から 3.17 になったときに設定の名前が変わったという情報を見つけました。Ubuntu Server 14.04.1 のオリジナルは Linux 3.13.0 だったので、見事に引っかかりました。デフォルトが無効になったのが解せぬ。

> Between linux version 3.16 and 3.17 the option
> CONFIG_NF_NAT_IPV4 changed the name to
> CONFIG_IP_NF_NAT. This option is not enabled in linux-image-3.17-rc5-amd64

https://lists.debian.org/debian-kernel/2014/09/msg00298.html

make oldconfig のログを読み返すと、確かに新しい設定として検出されていました。ええ、Enter キーを長押しにしていたので気づきませんでしたとも。こんなの気付かないよなぁ・・。

*
* IP: Netfilter Configuration
*
IPv4 connection tracking support (required for NAT) (NF_CONNTRACK_IPV4) [M/n/?] m
ARP packet logging (NF_LOG_ARP) [N/m/y] (NEW)
IPv4 packet logging (NF_LOG_IPV4) [M/y] (NEW)
IPv4 nf_tables support (NF_TABLES_IPV4) [M/n/?] m
  IPv4 nf_tables route chain support (NFT_CHAIN_ROUTE_IPV4) [M/n/?] m
IPv4 packet rejection (NF_REJECT_IPV4) [M/y] (NEW)
ARP nf_tables support (NF_TABLES_ARP) [M/n/?] m
IPv4 NAT (NF_NAT_IPV4) [M/n/?] m
  IPv4 nf_tables nat chain support (NFT_CHAIN_NAT_IPV4) [M/n/?] m
  IPv4 masquerade support (NF_NAT_MASQUERADE_IPV4) [N/m/?] (NEW)
  Basic SNMP-ALG support (NF_NAT_SNMP_BASIC) [M/n/?] m
IP tables support (required for filtering/masq/NAT) (IP_NF_IPTABLES) [M/n/y/?] m
  "ah" match support (IP_NF_MATCH_AH) [M/n/?] m
  "ecn" match support (IP_NF_MATCH_ECN) [M/n/?] m
  "rpfilter" reverse path filter match support (IP_NF_MATCH_RPFILTER) [M/n/?] m
  "ttl" match support (IP_NF_MATCH_TTL) [M/n/?] m
  Packet filtering (IP_NF_FILTER) [M/n/?] m
    REJECT target support (IP_NF_TARGET_REJECT) [M/n/?] m
  SYNPROXY target support (IP_NF_TARGET_SYNPROXY) [M/n/?] m
  iptables NAT support (IP_NF_NAT) [N/m/?] (NEW)
  Packet mangling (IP_NF_MANGLE) [M/n/?] m
    CLUSTERIP target support (IP_NF_TARGET_CLUSTERIP) [M/n/?] m
    ECN target support (IP_NF_TARGET_ECN) [M/n/?] m
    "TTL" target support (IP_NF_TARGET_TTL) [M/n/?] m
  raw table support (required for NOTRACK/TRACE) (IP_NF_RAW) [M/n/?] m
  Security table (IP_NF_SECURITY) [M/n/?] m

make menuconfig の位置は ↓ です。

Prompt: iptables NAT support
Location:
  -> Networking support (NET [=y])
    -> Networking options
      -> Network packet filtering framework (Netfilter) (NETFILTER [=y])
        -> IP: Netfilter Configuration
          -> IP tables support (required for filtering/masq/NAT) (IP_NF_IPTABLES [=m])

二度とこのようなことがないように、よく分からない項目も含めて netfilter に関する項目を menuconfig から有効にしておきます。追加で有効にした項目はこちら。

> CONFIG_NETFILTER_DEBUG=y
> CONFIG_NF_TABLES_INET=m
> CONFIG_NFT_MASQ=m
> CONFIG_NFT_QUEUE=m
> CONFIG_NFT_REJECT=m
> CONFIG_NFT_REJECT_INET=m
> CONFIG_NETFILTER_XT_NAT=m
> CONFIG_NFT_REJECT_IPV4=m
> CONFIG_NF_NAT_MASQUERADE_IPV4=m
> CONFIG_NFT_MASQ_IPV4=m
> CONFIG_IP_NF_NAT=m
> CONFIG_IP_NF_TARGET_MASQUERADE=m
> CONFIG_IP_NF_TARGET_NETMAP=m
> CONFIG_IP_NF_TARGET_REDIRECT=m
> CONFIG_NFT_REJECT_IPV6=m
> CONFIG_NF_NAT_MASQUERADE_IPV6=m
> CONFIG_NFT_MASQ_IPV6=m
> CONFIG_IP6_NF_NAT=m
> CONFIG_IP6_NF_TARGET_MASQUERADE=m
> CONFIG_IP6_NF_TARGET_NPT=m

で、ビルドしたら無事に iptables コマンドはうまくいきました。検索すると、同じように困っている人が世界中にいるようでした。oldconfig で Enter キーを長押しした人たちでしょう。

2. Kernel Loadable Modules のシンボルが解決できない

iptables によるポート フォワードの設定はうまくいきました。次は、フォワードされたパケットを補足する簡単なソケット通信のプログラムを書き、ポート フォワードを簡単に引き起こせるようにしておきます。

netfilter のコードを見渡して、IPv4 を NAT するときに実行されそうな処理の一つとして、net/ipv4/netfilter/nf_nat_l3proto_ipv4.c にある nf_nat_ipv4_manip_pkt という関数に目をつけました。そこで、SysRq-g でブレークさせて関数を探すと・・・。ハイ、見つかりません。

(gdb) i functions nf_nat_ipv4_manip_pkt
All functions matching regular expression "nf_nat_ipv4_manip_pkt":

どうせインライン展開でもされているんでしょう、と思って呼び出し元を探そうとすると、関数アドレスがグローバル変数の nf_nat_l3proto_ipv4.manip_pkt に代入されているのを発見しました。というわけで、この関数がインライン展開されるとは思えません。おかしいですね。

nf_ で始まるシンボルを全部表示してみます。

(gdb) i functions ^nf_
All functions matching regular expression "^nf_":

File net/ipv4/netfilter.c:
__sum16 nf_ip_checksum(struct sk_buff *, unsigned int, unsigned int, u_int8_t);
static __sum16 nf_ip_checksum_partial(struct sk_buff *, unsigned int, unsigned int, unsigned int,

(snip)

net/ipv4/netfilter ディレクトリのファイルが全滅しています。ますますおかしい。そこで net/ipv4/netfilter/Makefile の内容を見てみると、以下の行を発見。むむっ、これはもしかして。

nf_nat_ipv4-y       := nf_nat_l3proto_ipv4.o nf_nat_proto_icmp.o
obj-$(CONFIG_NF_NAT_IPV4) += nf_nat_ipv4.o

ふと思いついて lsmod  コマンドを実行しました。どうやらモジュール ファイルが別になっているのが理由のようです。つまり、kdb セッションに繋ぐ前に、開発機にある vmlinux をターゲットとして gdb を起動しているので、vmlinux (すなわち /boot/vmlinuz-**** の中身) のシンボルしか解決できないのでしょう。

john@linux-test:~$ sudo lsmod | grep nf
[sudo] password for john:
nf_conntrack_ipv4      14806  1
nf_defrag_ipv4         12758  1 nf_conntrack_ipv4
nf_nat_ipv4            14115  1 iptable_nat
nf_nat                 22050  2 nf_nat_ipv4,xt_REDIRECT
nf_conntrack          100933  3 nf_nat,nf_nat_ipv4,nf_conntrack_ipv4

Kernel Loaded Module をデバッグするときはどうするのかを調べると、gdb 上で add-symbol-file コマンドを実行して、手動でシンボル ファイルを追加しないといけないらしいです。しかも、モジュール イメージのコード セクションがロードされているアドレスも指定しないといけないとか。何これ。超めんどくさいんですけど。Windows Debugger なら全部自動でやってくれるのに!

幸いなことに、カーネル モジュールのコード セクションは sysfs 仮想ファイル システム経由でで簡単に見つけられます。そこで、以下のスクリプトを書いて、全カーネル モジュールのコード セクションのアドレスを一気にダンプできるようにしました。こういうことはデバッガーのコマンド経由でできるといいのですが。

#!/bin/bash
for file in $(find /sys/module -name .text) ; do
    addr=`cat ${file}`
    echo $addr " : " ${file}
done

とにかく無事アドレスをゲット。

john@linux-test:~$ sudo ./lm.sh  | sort -k1
0xffffffffa0000000  :  /sys/module/floppy/sections/.text
0xffffffffa0012000  :  /sys/module/mac_hid/sections/.text
0xffffffffa0019000  :  /sys/module/mptbase/sections/.text
0xffffffffa0033000  :  /sys/module/mptspi/sections/.text
0xffffffffa003a000  :  /sys/module/mptscsih/sections/.text
0xffffffffa0049000  :  /sys/module/e1000/sections/.text
0xffffffffa006c000  :  /sys/module/parport/sections/.text
0xffffffffa0078000  :  /sys/module/lp/sections/.text
0xffffffffa007e000  :  /sys/module/parport_pc/sections/.text
0xffffffffa0087000  :  /sys/module/shpchp/sections/.text
0xffffffffa0092000  :  /sys/module/serio_raw/sections/.text
0xffffffffa0097000  :  /sys/module/coretemp/sections/.text
0xffffffffa009c000  :  /sys/module/crc_ccitt/sections/.text
0xffffffffa00a1000  :  /sys/module/psmouse/sections/.text
0xffffffffa00be000  :  /sys/module/i2c_piix4/sections/.text
0xffffffffa00c5000  :  /sys/module/vmw_vmci/sections/.text
0xffffffffa00db000  :  /sys/module/vmw_balloon/sections/.text
0xffffffffa00e0000  :  /sys/module/ppdev/sections/.text
0xffffffffa00e6000  :  /sys/module/nf_nat_ipv4/sections/.text
0xffffffffa00eb000  :  /sys/module/irda/sections/.text
0xffffffffa010c000  :  /sys/module/drm/sections/.text
0xffffffffa015b000  :  /sys/module/ttm/sections/.text
0xffffffffa0171000  :  /sys/module/nf_nat/sections/.text
0xffffffffa0178000  :  /sys/module/drm_kms_helper/sections/.text
0xffffffffa0191000  :  /sys/module/vmwgfx/sections/.text
0xffffffffa01bd000  :  /sys/module/x_tables/sections/.text
0xffffffffa01c7000  :  /sys/module/ip_tables/sections/.text
0xffffffffa01cf000  :  /sys/module/nf_conntrack/sections/.text
0xffffffffa01e9000  :  /sys/module/nf_defrag_ipv4/sections/.text
0xffffffffa01ee000  :  /sys/module/nf_conntrack_ipv4/sections/.text
0xffffffffa01f3000  :  /sys/module/iptable_nat/sections/.text
0xffffffffa01f8000  :  /sys/module/xt_tcpudp/sections/.text
0xffffffffa01fd000  :  /sys/module/xt_REDIRECT/sections/.text

ターゲットにする関数は nf_nat_ipv4_manip_pkt で、nf_nat_ipv4  に実装されているはずですが、念のため事前に objdump でオブジェクト ファイルのシンボル テーブルを確認しておきます。

john@linux-dev:/usr/src/linux-3.18.2$ find . -name nf_nat_ipv4.o
./net/ipv4/netfilter/nf_nat_ipv4.o
john@linux-dev:/usr/src/linux-3.18.2$ objdump –syms ./net/ipv4/netfilter/nf_nat_ipv4.o | grep nf_nat_ipv4_manip_pkt
0000000000000330 l     F .text  000000000000011b nf_nat_ipv4_manip_pkt

無事見つかりました。これで一安心です。では実際にデバッガーにシンボル追加して、ブレークポイントを設定します。

(gdb) add-symbol-file ./net/ipv4/netfilter/nf_nat_ipv4.o 0xffffffffa00e6000
add symbol table from file "./net/ipv4/netfilter/nf_nat_ipv4.o" at
        .text_addr = 0xffffffffa00e6000
(y or n) y
Reading symbols from ./net/ipv4/netfilter/nf_nat_ipv4.o…done.

(gdb) i functions nf_nat_ipv4_manip_pkt
All functions matching regular expression "nf_nat_ipv4_manip_pkt":

File net/ipv4/netfilter/nf_nat_l3proto_ipv4.c:
static bool nf_nat_ipv4_manip_pkt(struct sk_buff *, unsigned int, const struct nf_nat_l4proto *,
    const struct nf_conntrack_tuple *, enum nf_nat_manip_type);

(gdb) break nf_nat_ipv4_manip_pkt
Breakpoint 1 at 0xffffffffa00e6330: file net/ipv4/netfilter/nf_nat_l3proto_ipv4.c, line 83.

(gdb) i break
Num     Type           Disp Enb Address            What
1       breakpoint     keep y   0xffffffffa00e6330 in nf_nat_ipv4_manip_pkt
                                                   at net/ipv4/netfilter/nf_nat_l3proto_ipv4.c:83
(gdb) c
Continuing.

ようやくブレークポイントを設定できました。クライアントからパケットを送って、ポート フォワードを引き起こしてみます。

(gdb) c
Continuing.

Breakpoint 1, nf_nat_ipv4_manip_pkt (skb=0xffff88003d1d9600, iphdroff=2685874880,
    l4proto=0xffffffffa01732c0, target=0xffff88003fc03a20, maniptype=NF_NAT_MANIP_DST)
    at net/ipv4/netfilter/nf_nat_l3proto_ipv4.c:83
83      {
(gdb) bt
#0  nf_nat_ipv4_manip_pkt (skb=0xffff88003d1d9600, iphdroff=2685874880,
    l4proto=0xffffffffa01732c0, target=0xffff88003fc03a20, maniptype=NF_NAT_MANIP_DST)
    at net/ipv4/netfilter/nf_nat_l3proto_ipv4.c:83
#1  0xffffffffa0171126 in ?? ()
#2  0xffffffff81ce3ca8 in init_net ()
#3  0x000000003000000a in ?? ()
#4  0x0000000000000000 in ?? ()
(gdb) c
Continuing.

Breakpoint 1, nf_nat_ipv4_manip_pkt (skb=0xffff88003d1d9e00, iphdroff=2685874880,
    l4proto=0xffffffffa01732c0, target=0xffff88003fc03640, maniptype=NF_NAT_MANIP_SRC)
    at net/ipv4/netfilter/nf_nat_l3proto_ipv4.c:83
83      {
(gdb) bt
#0  nf_nat_ipv4_manip_pkt (skb=0xffff88003d1d9e00, iphdroff=2685874880,
    l4proto=0xffffffffa01732c0, target=0xffff88003fc03640, maniptype=NF_NAT_MANIP_SRC)
    at net/ipv4/netfilter/nf_nat_l3proto_ipv4.c:83
#1  0xffffffffa0171126 in ?? ()
#2  0xffffffff81ce3ca8 in init_net ()
#3  0x00000000ac0a2a68 in ?? ()
#4  0x0000000000000000 in ?? ()

読み通り、ポート フォワードのときに nf_nat_ipv4_manip_pkt は実行されました。これを取っ掛かりとしてコードを追いかけていけば何とかなるでしょう。途中のフレームにあるモジュールのシンボルが無いため、コール スタックはおかしなことになっているのが気になります。もう少しシンボルを追加することにします。

上述したコード セクションのアドレスをダンプするスクリプトの出力結果は、アドレス順にソートしてあります。アドレス一覧と nf_nat_ipv4_manip_pkt のリターン アドレス 0xffffffffa0171126  とを比べると、このアドレスは nf_nat のモジュール内であることが分かります。そこで今度は nf_nat のシンボルを追加して、もう一度コール スタックを確認します。

(gdb) add-symbol-file ./net/netfilter/nf_nat.o 0xffffffffa0171000
add symbol table from file "./net/netfilter/nf_nat.o" at
        .text_addr = 0xffffffffa0171000
(y or n) y
Reading symbols from ./net/netfilter/nf_nat.o…done.
(gdb) c
Continuing.

(gdb) bt
#0  nf_nat_ipv4_manip_pkt (skb=0xffff880036799700, iphdroff=2685874880,
    l4proto=0xffffffffa01732c0, target=0xffff88003fc03a20, maniptype=NF_NAT_MANIP_DST)
    at net/ipv4/netfilter/nf_nat_l3proto_ipv4.c:83
#1  0xffffffffa0171126 in nf_nat_packet (ct=<optimized out>, ctinfo=<optimized out>,
    hooknum=<optimized out>, skb=0xffff880036799700) at net/netfilter/nf_nat_core.c:501
#2  0xffffffffa00e6896 in nf_nat_ipv4_fn (ops=0xffffffffa01f5040, skb=0xffff880036799700,
    in=<optimized out>, out=0x0 <irq_stack_union>, do_chain=<optimized out>)
    at net/ipv4/netfilter/nf_nat_l3proto_ipv4.c:339
#3  0xffffffffa00e695e in nf_nat_ipv4_in (ops=<optimized out>, skb=0xffff880036799700,
    in=<optimized out>, out=<optimized out>, do_chain=<optimized out>)
    at net/ipv4/netfilter/nf_nat_l3proto_ipv4.c:359
#4  0xffffffffa01f30a5 in ?? ()
#5  0xffff88003fc03b58 in ?? ()
#6  0xffffffff81694e5e in nf_iterate (head=<optimized out>, skb=0xffff880036799700,
    hook=<optimized out>, indev=0xffff88003651c000, outdev=0x0 <irq_stack_union>,
    elemp=0xffff88003fc03ba0, okfn=0xffffffff8169b650 <ip_rcv_finish>, hook_thresh=-2116913248)
    at net/netfilter/core.c:142
Backtrace stopped: frame did not save the PC

(gdb) c
Continuing.

Breakpoint 1, nf_nat_ipv4_manip_pkt (skb=0xffff880036799800, iphdroff=2685874880,
    l4proto=0xffffffffa01732c0, target=0xffff88003fc03640, maniptype=NF_NAT_MANIP_SRC)
    at net/ipv4/netfilter/nf_nat_l3proto_ipv4.c:83
83      {
(gdb) bt
#0  nf_nat_ipv4_manip_pkt (skb=0xffff880036799800, iphdroff=2685874880,
    l4proto=0xffffffffa01732c0, target=0xffff88003fc03640, maniptype=NF_NAT_MANIP_SRC)
    at net/ipv4/netfilter/nf_nat_l3proto_ipv4.c:83
#1  0xffffffffa0171126 in nf_nat_packet (ct=<optimized out>, ctinfo=<optimized out>,
    hooknum=<optimized out>, skb=0xffff880036799800) at net/netfilter/nf_nat_core.c:501
#2  0xffffffffa00e6896 in nf_nat_ipv4_fn (ops=0xffffffffa01f5078, skb=0xffff880036799800,
    in=<optimized out>, out=0xffff88003651c000, do_chain=<optimized out>)
    at net/ipv4/netfilter/nf_nat_l3proto_ipv4.c:339
#3  0xffffffffa00e6a08 in nf_nat_ipv4_out (ops=<optimized out>, skb=0xffff880036799800,
    in=<optimized out>, out=<optimized out>, do_chain=<optimized out>)
    at net/ipv4/netfilter/nf_nat_l3proto_ipv4.c:389
#4  0xffffffffa01f3085 in ?? ()
#5  0xffff88003fc03768 in ?? ()
#6  0xffffffff81694e5e in nf_iterate (head=<optimized out>, skb=0xffff880036799800,
    hook=<optimized out>, indev=0x0 <irq_stack_union>, outdev=0xffff88003651c000,
    elemp=0xffff88003fc037b0, okfn=0xffffffff8169ff80 <ip_finish_output>, hook_thresh=-2116913184)
    at net/netfilter/core.c:142
Backtrace stopped: frame did not save the PC

まだ完全ではないようですが、先ほどよりは多くの情報が出てきました。

シンボルが解決できないときにコール スタックをうまく辿れなくなるということは、ビルドでは FPO (=Frame Pointer Omission) が有効になっているように見えます。それっぽい設定は以下の 3 箇所ありました。有効になっている SCHED_OMIT_FRAME_POINTER が他の設定を上書きしているのかもしれません。これ以上追いかけていないので、確証はありませんが。

Symbol: FRAME_POINTER [=y]
Type  : boolean
Prompt: Compile the kernel with frame pointers
  Location:
    -> Kernel hacking
(1)   -> Compile-time checks and compiler options
  Defined at lib/Kconfig.debug:296
  Depends on: DEBUG_KERNEL [=y] && (CRIS || M68K || FRV || UML || AVR32 || SUPERH || BLACKF
  Selected by: FAULT_INJECTION_STACKTRACE_FILTER [=n] && FAULT_INJECTION_DEBUG_FS [=n] && S

Symbol: ARCH_WANT_FRAME_POINTERS [=y]
Type  : boolean
  Defined at lib/Kconfig.debug:292
  Selected by: X86 [=y]

Symbol: SCHED_OMIT_FRAME_POINTER [=y]
Type  : boolean
Prompt: Single-depth WCHAN output
  Location:
(2) -> Processor type and features
  Defined at arch/x86/Kconfig:589
  Depends on: X86 [=y]

とりあえずはこの設定でデバッグを続けるか、netfilter をビルトインしてしまうかは迷うところです。

How to Live Debug a Linux Kernel in Ubuntu Server

gdb についても、何とか使えるレベルにはなってきたので、Linux カーネルのデバッグにも手を出してみることにしました。Windows であれば、普通にインストールして bcdedit でカーネル デバッグを有効にするだけでいいのに、Linux だと相当めんどくさいです。しかも人によって実行しているコマンドがばらばら・・これだから Linux は困る。いろいろと検索すると、Linux のカーネルをソースからビルドしてインストールしている人はたくさんいるようですが、カーネルをデバッグしている人となると、一気に数が少なくなる気がします。

最初は、Hyper-V の仮想マシン上にインストールした Ubuntu Server に対して、仮想のシリアル ポートを追加して、Hyper-V ホストの Windows から名前つきパイプ経由でデバッガーを直接繋ぐ環境を作りました。できるにはできたんですが、2009 年に kdb  と kgdb が統合された際、kdb 上で逆アセンブルするためのコマンド id が削除されていました。それだけでも論外なのですが、さらに kdb というデバッガーは gdb とは異なるコマンド セットを持っています。したがって現在の主流は、gdb を使ったリモート デバッグであり、ローカルの kdb に直接繋ぎに行くのは非常に特殊な場合に限られそうです。

KDB FAQ – KGDB Wiki
https://kgdb.wiki.kernel.org/index.php/KDB_FAQ

となると、Hyper-V で環境を作るのが一気にめんどくさくなります。gdb でリモート デバッグを行なうということは、debugger と debuggee の 2 台の Linux が必要になるということです。原理上当然ですし、Windows のカーネル デバッグも同じなので何も文句は無いのですが、問題は Hyper-V の仮想マシン同士をシリアル ポートで繋ぐ簡単な方法がないことです。Hyper-V では、仮想シリアル ポート デバイスを Hyper-V ホストの名前付きパイプにリダイレクトすることができます。しかし試してみたところ、2 つの仮想マシンで 1 つの名前付きパイプを共有して、仮想マシン同士をシリアル通信させることができませんでした。

以下の情報から、2 台の仮想マシンがそれぞれ別の名前付きパイプを使い、Hyper-V ホスト側でそれらの名前付きパイプをリレーするようなプログラムを動かすという方法がひょっとすると可能かもしれませんが試していません。

http://stackoverflow.com/questions/4973554/hyper-v-connecting-vms-through-named-pipe-loses-data

一方、VMware のハイパーバイザーでは、2 台の仮想マシンが 1 つの名前付きパイプを共有することが可能なので、大昔にインストールした VMware ESXi の環境を使うことにしました。いつも通り、OS は Ubuntu Server 14.04.1 を使います。

手順の流れは以下の通りです。

  1. ESXi 上に 2 台の仮想マシンを用意し、Ubuntu Server をインストール
  2. Debugger 上に Linux カーネルのソースをダウンロードし、ビルド
  3. 動作確認のため、ビルドしたカーネルを Debugger 上にインストールして再起動
  4. 必要なファイルを Debugger から Debuggee にコピー
  5. Debuggee 上でブートの設定を行ない、シリアル コンソールからカーネル デバッグを有効化
  6. Debugger からデバッグ開始

まずは ESXi の環境に仮想マシンを 2 台用意し、以下の設定で仮想シリアル ポート デバイスを追加します。

  • Debugger – デバッグする人
    Pipe Name: linux-debug
    Near End: Client
    Far End: A virtual machine
  • Debuggee – デバッグされる人
    Pipe Name: linux-debug
    Near End: Server
    Far End: A virtual machine

image
Debugger

image
Debuggee

Ubuntu Server のインストールでは特別な設定は不要です。OS が用意できたら、次に Linux カーネルをソースからビルドします。ソースは git リポジトリからクローンしてもいいのですが、かなり遅いらしいので、kernel.org からダウンロードします。2015/1/10 現在の最新は 3.18.2 でした。

The Linux Kernel Archives
https://www.kernel.org/

カーネルをビルドするコマンドは人によって千差万別で、どれを使うか迷いましたが、以下のコマンドで落ち着きました。

$ uname -a
Linux linux-dev 3.13.0-32-generic #57-Ubuntu SMP Tue Jul 15 03:51:08 UTC 2014 x86_64 x86_64 x86_64 GNU/Linux
$ sudo apt-get install build-essential libtool manpages-dev gdb
$ sudo apt-get install kernel-package libncurses5-dev
$ wget
https://www.kernel.org/pub/linux/kernel/v3.x/linux-3.18.2.tar.xz
$ tar -xvf linux-3.18.2.tar.xz
$ cd linux-3.18.2/
$ cp /boot/config-3.13.0-32-generic .config
$ make oldconfig
$ make menuconfig
$ sudo make-kpkg clean
$ sudo make-kpkg –initrd kernel_image kernel_headers \
> –append_to_version=-poodim -j2

少し補足です。

  • kernel-package
    make-kpkg コマンドのために必要です。このコマンドで、Debian パッケージを作ってインストールします。
  • libncurses5-dev
    make menuconfig コマンドのために必要です。
  • cp /boot/config-3.13.0-32-generic .config ; make oldconfig
    最初に、現在のカーネル設定を元に設定を作ります。一から設定してもいいのですが、互換性維持のためと、設定項目が膨大すぎるためです。というかみんなやってるし。
  • make menuconfig
    メインの設定はここでやります。好きなだけ弄っていってね!
  • –append_to_version=-poodim
    作るカーネルのリリース名のサフィックスを好きなように設定できます。例えば今回のコマンドだと、カーネルのリリース名が "3.18.2-poodim" になります。
  • -j2
    コンパイル時の並列実行数の設定です。ビルド環境の CPU コア数に合わせて設定してください。どれだけ効果があるのかは分かりません。私はこういう設定ではいつも コア数x2 にするようにしています。

menuconfig では、どこも設定を変更する必要はありませんでしたが、カーネル デバッグに必要な項目が有効化されていることを必ず確認しておきましょう。

  • Kernel hacking –> Magic SysRq key –> (0x1) Enable magic SysRq key functions by default
  • Kernel hacking –> Kernel debugging
  • Kernel hacking –> KGDB: kernel debugger –> KGDB: use kgdb over the serial console
  • Kernel hacking –> KGDB: kernel debugger
    –> KGDB_KDB: include kdb frontend for kgdb –> KGDB_KDB: keyboard as input device

image
menuconfig のトップ画面 – Kernel hacking を選んで Enter

image
Kernel hacking 画面 – SysRq と Kernel debugging が有効であることを確認

image
Kernel hacking 画面 – KGDB を選んで Enter

image
KGDB: kernel debugger 画面 – kgdb が有効であることを確認

Ubuntu のサイトにもカーネルのビルド方法の情報がありますが、kernel.org ではなく Ubuntu のリポジトリにあるソースを使うみたいなので、あまり参考になりません。一応リンクを掲載しておきます。

Kernel/BuildYourOwnKernel – Ubuntu Wiki
https://wiki.ubuntu.com/Kernel/BuildYourOwnKernel

以下のページには、メジャーなディストリビューションで使えるコマンドが紹介されています。分かりやすい。

How to compile and install Linux kernel in Ubuntu/Debian and RHEL/CentOS | Rui’s Blog
https://coelhorjc.wordpress.com/2014/12/05/howto-upgrade-to-latest-kernel-in-ubuntu/

ビルドはけっこう時間がかかりますので、のんびり待ちましょう。なお、いくつかのサイトではビルド時に sudo ではなく fakeroot を使うように書かれていましたが、fakeroot だと以下のパーミッション エラーが出たので sudo にしました。

$ fakeroot make-kpkg –initrd kernel_image kernel_headers –append_to_version=poodim -j4
                System.map "/usr/src/linux-3.18.2/debian/linux-image-3.18.2poodim//boot"
run-parts: executing /etc/kernel/postinst.d/apt-auto-removal 3.18.2poodim /usr/src/linux-3.18.2/debian/linux-image-3.18.2poodim//boot/vmlinuz-3.18.2poodim
/etc/kernel/postinst.d/apt-auto-removal: 84: /etc/kernel/postinst.d/apt-auto-removal: cannot create /etc/apt/apt.conf.d//01autoremove-kernels.dpkg-new: Permission denied
run-parts: /etc/kernel/postinst.d/apt-auto-removal exited with return code 2
make[3]: *** [install] Error 1
make[2]: *** [install] Error 2
make[2]: Leaving directory `/usr/src/linux-3.18.2′
make[1]: *** [debian/stamp/install/linux-image-3.18.2poodim] Error 2
make[1]: Leaving directory `/usr/src/linux-3.18.2′
make: *** [kernel_image] Error 2

ビルドが終わると、拡張子 deb の Debian パッケージ ファイルが /usr/src に作られます。

$ ls -l /usr/src
total 128108
drwxrwxr-x 25 john john     4096 Jan 10 19:56 linux-3.18.2
-rw-rw-r–  1 john john 80941176 Jan  8 10:34 linux-3.18.2.tar.xz
drwxr-xr-x 24 root root     4096 Jan 10 16:39 linux-headers-3.13.0-32
drwxr-xr-x  7 root root     4096 Jan 10 16:39 linux-headers-3.13.0-32-generic
drwxr-xr-x 23 root root     4096 Jan 10 21:42 linux-headers-3.18.2-poodim
-rw-r–r–  1 root root  7020266 Jan 10 19:59 linux-headers-3.18.2-poodim_3.18.2-poodim-10.00.Custom_amd64.deb
-rw-r–r–  1 root root 43196770 Jan 10 19:55 linux-image-3.18.2-poodim_3.18.2-poodim-10.00.Custom_amd64.deb

この後、必要なファイルを Debuggee にコピーしますが、その前に、作ったカーネルで OS が無事に動くかどうかを確かめるため、Debugger マシン上にカーネルをインストールして再起動します。ちょっと緊張します。

$ sudo dpkg -i /usr/src/linux-image-3.18.2-poodim_3.18.2-poodim-10.00.Custom_amd64.deb
$ sudo dpkg -i /usr/src/linux-headers-3.18.2-poodim_3.18.2-poodim-10.00.Custom_amd64.deb
$ sudo shutdown -r now

(再起動中)

$ uname -a
Linux linux-dev 3.18.2-poodim #1 SMP Sat Jan 10 17:25:35 PST 2015 x86_64 x86_64 x86_64 GNU/Linux

ここから先は、以下のサイトを参考にしました。

Kgdb – eLinux.org
http://elinux.org/Kgdb

kgdbを用いたカーネルデバッグ環境の構築 – big-eyed-hamsterの日記
http://d.hatena.ne.jp/big-eyed-hamster/20090331/1238470673

以下のファイルを debuggee 上にコピーします。

  • /boot/initrd.img-3.18.2-poodim – ブート時の初期 RAM ディスク
  • /boot/System.map-3.18.2-poodim – カーネルのシンボル テーブル
  • /boot/vmlinuz-3.18.2-poodim – カーネル本体

どうやってコピーしてもいいのですが、今回は Debugger となる linux-dev 上で以下の scp コマンドを実行しました。10.0.0.46 は Debuggee の IP アドレスです。とりあえずホーム ディレクトリにコピー。

$ scp /boot/initrd.img-3.18.2-poodim john@10.0.0.46:~
$ scp /boot/System.map-3.18.2-poodim john@10.0.0.46:~
$ scp /boot/vmlinuz-3.18.2-poodim john@10.0.0.46:~

次に debuggee となる linux-test にログインし、/boot ディレクトリへのコピーと、パーミッションの設定などを行います。

$ cd ~
$ sudo mv  *poodim /boot/
$ cd /boot
$ sudo chown root:root initrd.img-3.18.2-poodim System.map-3.18.2-poodim vmlinuz-3.18.2-poodim
$ sudo chmod 600 vmlinuz-3.18.2-poodim System.map-3.18.2-poodim
$ ls -la /boot
total 58051
drwxr-xr-x  4 root root     1024 Jan 10 22:00 .
drwxr-xr-x 22 root root     4096 Jan 10 16:32 ..
-rw-r–r–  1 root root  1162712 Jul 14 21:29 abi-3.13.0-32-generic
-rw-r–r–  1 root root   165611 Jul 14 21:29 config-3.13.0-32-generic
drwxr-xr-x  5 root root     1024 Jan 10 22:04 grub
-rw-r–r–  1 root root 20084102 Jan 10 16:36 initrd.img-3.13.0-32-generic
-rw-r–r–  1 root root 18521322 Jan 10 21:58 initrd.img-3.18.2-poodim
drwx——  2 root root    12288 Jan 10 16:31 lost+found
-rw-r–r–  1 root root   176500 Mar 12  2014 memtest86+.bin
-rw-r–r–  1 root root   178176 Mar 12  2014 memtest86+.elf
-rw-r–r–  1 root root   178680 Mar 12  2014 memtest86+_multiboot.bin
-rw——-  1 root root  3381262 Jul 14 21:29 System.map-3.13.0-32-generic
-rw——-  1 root root  3526496 Jan 10 21:59 System.map-3.18.2-poodim
-rw——-  1 root root  5798112 Jul 14 21:29 vmlinuz-3.13.0-32-generic
-rw——-  1 root root  6004160 Jan 10 21:59 vmlinuz-3.18.2-poodim

(少し修正)
上記の方法でも、OS の起動は問題ありませんでしたが、/lib/modules へのディレクトリ作成など、必要なセットアップ作業が行われません。したがって、ビルド後に作成された 2 つの Debian パッケージごとコピーして、Debuggee 上でも同様に sudo dpkg -i コマンドによるインストールを行ったほうが安全だと思います。

ブート時に、コピーしたカーネルが読み込まれるようにするため、ブートローダーである grub の設定をします。設定ファイルは /etc/default/grub です。

GRUB_DEFAULT=0
#GRUB_HIDDEN_TIMEOUT=0
GRUB_HIDDEN_TIMEOUT_QUIET=false
GRUB_TIMEOUT=30
GRUB_DISTRIBUTOR=`lsb_release -i -s 2> /dev/null || echo Debian`
GRUB_CMDLINE_LINUX_DEFAULT="kgdbwait kgdboc=ttyS0,115200"
GRUB_CMDLINE_LINUX=""

設定ファイルを変更したら、変更を /boot/grub/grub.cfg に反映させます。コピーしたカーネルが認識されたことが分かります。そのまま再起動します。

$ sudo update-grub
[sudo] password for john:
Generating grub configuration file …
Found linux image: /boot/vmlinuz-3.18.2-poodim
Found initrd image: /boot/initrd.img-3.18.2-poodim
Found linux image: /boot/vmlinuz-3.13.0-32-generic
Found initrd image: /boot/initrd.img-3.13.0-32-generic
Found memtest86+ image: /memtest86+.elf
Found memtest86+ image: /memtest86+.bin
done
john@linux-test:~$ sudo shutdown -r now

なお、上記の方法は現在のバージョンである GRUB 2 の方法です。昔の GRUB では、/boot/grub/menu.lst を編集してカーネルのブート パラメーターを変更していたようです。まだ多くの情報が menu.lst を変更する方法を紹介していますが、menu.lst は現在存在しないので注意してください。

Debuggee である linux-test 再起動すると、コンソール上にはログインのプロンプトではなく、[0]kdb> というプロンプトが表れ、ローカルの kdb セッションが入力待ちになります。Debuggee はこのまま放置しておきます。

image

次に Debugger となる linux-dev のセッションに戻ります。linux-dev の COM1 ポート (/dev/ttyS0) から gdb を使って linux-test に繋ぎに行くわけですが、今使っている作業用ユーザーの john には /dev/ttyS0 にアクセスする権限がありません。sudo で gdb を起動してもいいのですが、sudo しなくてもいいよう権限を与えることにします。まず現在の設定を確認します。

john@linux-dev:~$ ls -la /dev/ttyS0
crw-rw—- 1 root dialout 4, 64 Jan 11 01:02 /dev/ttyS0

john を dialout に参加させればいいようです。設定を反映させるため、ログアウトしてから再ログインします。

$ sudo adduser john dialout
Adding user `john’ to group `dialout’ …
Adding user john to group dialout
Done.
$ groups john
john : john adm dialout cdrom sudo dip plugdev lpadmin sambashare
$ logout

ようやく gdb を起動するところまできました。ここで重要なのは、ただ gdb を起動すればいいというわけではなく、linux-test 上でシンボルが正しく解決されるように、ビルドしたカーネル ファイルをターゲットにする必要があることです。今回の環境だと、/usr/src/linux-3.18.2/vmlinux がカーネルのバイナリです。

$ gdb /usr/src/linux-3.18.2/vmlinux
GNU gdb (Ubuntu 7.7.1-0ubuntu5~14.04.2) 7.7.1
Copyright (C) 2014 Free Software Foundation, Inc.
License GPLv3+: GNU GPL version 3 or later <
http://gnu.org/licenses/gpl.html>
This is free software: you are free to change and redistribute it.
There is NO WARRANTY, to the extent permitted by law.  Type "show copying"
and "show warranty" for details.
This GDB was configured as "x86_64-linux-gnu".
Type "show configuration" for configuration details.
For bug reporting instructions, please see:
<
http://www.gnu.org/software/gdb/bugs/>.
Find the GDB manual and other documentation resources online at:
<
http://www.gnu.org/software/gdb/documentation/>.
For help, type "help".
Type "apropos word" to search for commands related to "word"…
Reading symbols from /usr/src/linux-3.18.2/vmlinux…done.
(gdb) set serial baud 115200
(gdb) target remote /dev/ttyS0
Remote debugging using /dev/ttyS0
kgdb_breakpoint () at kernel/debug/debug_core.c:1051
1051            wmb(); /* Sync point after breakpoint */
(gdb) bt
#0  kgdb_breakpoint () at kernel/debug/debug_core.c:1051
#1  0xffffffff811151f0 in kgdb_initial_breakpoint () at kernel/debug/debug_core.c:949
#2  kgdb_register_io_module (new_dbg_io_ops=0xffffffff81cb6d80 <kgdboc_io_ops>)
    at kernel/debug/debug_core.c:991
#3  0xffffffff8149c9ef in configure_kgdboc () at drivers/tty/serial/kgdboc.c:200
#4  0xffffffff81d8ff5a in init_kgdboc () at drivers/tty/serial/kgdboc.c:229
#5  0xffffffff8100212c in do_one_initcall (fn=0xffffffff81d8ff46 <init_kgdboc>) at init/main.c:791
#6  0xffffffff81d43245 in do_initcall_level (level=<optimized out>) at init/main.c:856
#7  do_initcalls () at init/main.c:864
#8  do_basic_setup () at init/main.c:883
#9  kernel_init_freeable () at init/main.c:1004
#10 0xffffffff8175539e in kernel_init (unused=<optimized out>) at init/main.c:934
#11 <signal handler called>
#12 0x0000000000000000 in irq_stack_union ()
#13 0x0000000000000000 in ?? ()
(gdb)

(ボーレートを指定する set serial baud 115200 は省略しても問題ないみたいです。)

コールスタックから、kgdb_initial_breakpoint() というそれっぽい関数で止まっているのが分かります。あとは、ユーザーモードの gdb と同じように操作できます。とりあえず c で実行を再開して、linux-test にログインします。

あとは普通にデバッグすればいいのですが、この後必要となる操作は、カーネル デバッガーにブレークインする方法です。Windows ならカーネル デバッガー上で Ctrl-C を押せば止まりますが、gdb で Ctrl-C を押しても止まってくれません。

方法は幾つかあり、以下のページに載っています。

Chapter 5. Using kgdb / gdb
https://www.kernel.org/doc/htmldocs/kgdb/EnableKGDB.html

使用頻度が高そうな 2 通りの方法について紹介します。

まずはキーボードを使う場合。goo ランキングで、「実はイマイチ使いどころがわからないWindowsのキー」 の第一位に輝いた SysRq キーを使います。もし SysRq キーが無ければ、PrintScreen ボタンを使います。

実はイマイチ使いどころがわからないWindowsのキー – gooランキング
http://ranking.goo.ne.jp/ranking/category/092/fR4cJvWG2ytj/

Windows では確か全く使われないはずですが、Linux では、マジック SysRq キーと呼ばれ、カーネルに特殊なコマンドを発行するために使います。デバッガーにブレークインするには、SysRq-g を使います。

Magic SysRq key – Wikipedia, the free encyclopedia
http://en.wikipedia.org/wiki/Magic_SysRq_key

ただし、Ubuntu Server のデフォルトでは、誤操作を防ぐためキーボードからの SysRq が無効にされており、まずはそれを有効にします。設定は簡単で、/etc/sysctl.conf ファイルに以下の一行を追加するだけです。

kernel.sysrq = 1

設定を有効にするには、sysctl -p コマンドを実行します。再起動は不要です。

john@linux-test:~$ sudo sysctl -p
kernel.sysrq = 1

キーの押し方は以下のような感じ。Alt+SysRq+G の同時押しでも動きます。

  1. Alt キーを押したままにする
  2. SysRq キーを押して放す (Alt キーは押したまま)
  3. g キーを押して放す(Alt キーは押したまま)
  4. Alt キーを放す

これは、シェルが起動していなくても有効です。例えば、ログインのプロンプトでブレークインさせるとこんな感じ。

image

ログイン画面で Alt+SysRq+G

参考までに、このときのコールスタックはこんな感じ。sysrq_handle_dbg() という関数が呼ばれるみたいです。

(gdb) bt
#0  kgdb_breakpoint () at kernel/debug/debug_core.c:1051
#1  0xffffffff8111424c in sysrq_handle_dbg (key=<optimized out>) at kernel/debug/debug_core.c:810
#2  0xffffffff8147a867 in __handle_sysrq (key=103, check_mask=true) at drivers/tty/sysrq.c:536
#3  0xffffffff8147ac96 in sysrq_handle_keypress (value=<optimized out>, code=<optimized out>,
    sysrq=<optimized out>) at drivers/tty/sysrq.c:797
#4  sysrq_filter (handle=<optimized out>, type=<optimized out>, code=<optimized out>,
    value=<optimized out>) at drivers/tty/sysrq.c:859
#5  0xffffffff815bd579 in input_to_handler (handle=<optimized out>, vals=0xffff88003696b848,
    count=<optimized out>) at drivers/input/input.c:105
#6  0xffffffff815be8a9 in input_pass_values (dev=0xffff880036976800, vals=0xffff88003696b840,
    count=3) at drivers/input/input.c:147
#7  0xffffffff815c0b79 in input_pass_values (count=<optimized out>, vals=<optimized out>,
    dev=<optimized out>) at drivers/input/input.c:395
#8  input_handle_event (dev=0xffff880036976800, type=0, code=0, value=0)
    at drivers/input/input.c:396
#9  0xffffffff815c0ff9 in input_event (dev=0x67 <irq_stack_union+103>, type=<optimized out>,
    code=<optimized out>, value=<optimized out>) at drivers/input/input.c:431
#10 0xffffffff815c71cc in input_sync (dev=<optimized out>) at include/linux/input.h:414
#11 atkbd_interrupt (serio=0xffff880036975000, data=<optimized out>, flags=<optimized out>)
    at drivers/input/keyboard/atkbd.c:511
#12 0xffffffff815ba23a in serio_interrupt (serio=0xffff880036975000, data=34 ‘"’, dfl=0)
    at drivers/input/serio/serio.c:1008
#13 0xffffffff815bb98a in i8042_interrupt (irq=<optimized out>, dev_id=<optimized out>)
    at drivers/input/serio/i8042.c:537
#14 0xffffffff810c4e1e in handle_irq_event_percpu (desc=<optimized out>,
    action=0x1 <irq_stack_union+1>) at kernel/irq/handle.c:143
#15 0xffffffff810c4fc1 in handle_irq_event (desc=0xffff88003f805600) at kernel/irq/handle.c:192
#16 0xffffffff810c7f0f in handle_edge_irq (irq=<optimized out>, desc=0xffff88003f805600)
    at kernel/irq/chip.c:605
#17 0xffffffff810155f2 in generic_handle_irq_desc (desc=<optimized out>, irq=<optimized out>)
    at include/linux/irqdesc.h:128
#18 handle_irq (irq=1, regs=<optimized out>) at arch/x86/kernel/irq_64.c:87
#19 0xffffffff8176eeb3 in do_IRQ (regs=0xffffffff81c03dc8 <init_thread_union+15816>)
    at arch/x86/kernel/irq.c:200
#20 <signal handler called>
#21 0xffffffffffffffce in ?? ()
Cannot access memory at address 0x246

残念ながらマジック SysRq キーを Teraterm 上で使うことはできません。リモート セッションからブレークさせる場合は、/proc/sysrq-trigger に対して g をエコーで送って SysRq-g を実現できます。しかし sysrq-trigger への書き込みは、sudo を使っても Permission denied で怒られてしまいます。

john@linux-test:~$ sudo echo g > /proc/sysrq-trigger
-bash: /proc/sysrq-trigger: Permission denied
john@linux-test:~$ ls -la /proc/sysrq-trigger
–w——- 1 root root 0 Jan 11 01:38 /proc/sysrq-trigger

sysrq-trigger を使うには、sudo ではなく本物の root ユーザーが必要です。Ubuntu Server の初期状態では、root ユーザーにパスワードが割り当てられていないので、パスワードを割り当ててスイッチ ユーザーしてからもう一度実行します。

john@linux-test:~$ sudo passwd root
[sudo] password for john:
Enter new UNIX password:
Retype new UNIX password:
passwd: password updated successfully
john@linux-test:~$ su –
Password:
root@linux-test:~# echo g > /proc/sysrq-trigger

今度はブレークしました。コールスタックの上のほうは、キーボードからブレークさせたときと同じになっています。もたもたしているとリモートセッションがタイムアウトで切れてしまうので、早めに c で実行を再開しておきましょう。

(gdb) bt
#0  kgdb_breakpoint () at kernel/debug/debug_core.c:1051
#1  0xffffffff8111424c in sysrq_handle_dbg (key=<optimized out>) at kernel/debug/debug_core.c:810
#2  0xffffffff8147a867 in __handle_sysrq (key=103, check_mask=false) at drivers/tty/sysrq.c:536
#3  0xffffffff8147ad13 in write_sysrq_trigger (file=<optimized out>, buf=<optimized out>, count=1,
    ppos=<optimized out>) at drivers/tty/sysrq.c:1092
#4  0xffffffff81241e8d in proc_reg_write (file=<optimized out>, buf=<optimized out>,
    count=<optimized out>, ppos=<optimized out>) at fs/proc/inode.c:224
#5  0xffffffff811dd067 in vfs_write (file=0x67 <irq_stack_union+103>,
    buf=0x1bd2408 "g\nload is /sbin/reload\nt\ne-kpkg\nub-script-check\nbkdf2\n", ‘\337’ <repeats 145 times>, <incomplete sequence \337>…, count=<optimized out>, pos=0xffff88003cf13f50)
    at fs/read_write.c:534
#6  0xffffffff811ddae6 in SYSC_write (count=<optimized out>, buf=<optimized out>,
    fd=<optimized out>) at fs/read_write.c:585
#7  SyS_write (fd=<optimized out>, buf=29172744, count=2) at fs/read_write.c:577
#8  <signal handler called>
#9  0x00007f5362cc52f0 in ?? ()
c#10 0xffff88003cf14240 in ?? ()
#11 0x0000000000000000 in ?? ()

これで全ての準備が整ったので、あとは思う存分 Linux カーネルの海に漕ぎ出すだけです。最後に、何か適当なところにブレークポイントを貼って試してみることにしました。

Windows でいう CreateProcess 的な関数はどれかな、と探すと、arch/x86/kernel/process_64.c に start_thread() とかいう CreateThread っぽい関数を見つけたので、これで試すことにしました。ブレークポイントを設定してから、ifconfig などの適当なプロセスを実行します。

(gdb) i functions start_thread
All functions matching regular expression "start_thread":

File arch/x86/kernel/process_64.c:
void start_thread(struct pt_regs *, unsigned long, unsigned long);
void start_thread_ia32(struct pt_regs *, u32, u32);
static void start_thread_common(struct pt_regs *, unsigned long, unsigned long, unsigned int,
    unsigned int, unsigned int);

File drivers/usb/early/ehci-dbgp.c:
static int kgdbdbgp_start_thread(void);
(gdb) break start_thread
Breakpoint 1 at 0xffffffff81011460: file arch/x86/kernel/process_64.c, line 249.
(gdb) c
Continuing.
[New Thread 1445]
[Switching to Thread 1445]

Breakpoint 1, start_thread (regs=0xffff8800369cbf58, new_ip=140737391120576,
    new_sp=140737391120576) at arch/x86/kernel/process_64.c:249
249     {
(gdb) bt
#0  start_thread (regs=0xffff8800369cbf58, new_ip=140737391120576, new_sp=140737391120576)
    at arch/x86/kernel/process_64.c:249
#1  0xffffffff812321c5 in load_elf_binary (bprm=0xffff88003cd50400) at fs/binfmt_elf.c:975
#2  0xffffffff811e34b7 in search_binary_handler (bprm=0xffff8800369cbf58) at fs/exec.c:1374
#3  0xffffffff811e4901 in exec_binprm (bprm=<optimized out>) at fs/exec.c:1416
#4  do_execve_common (filename=0xffff88003c272000, argv=…, envp=…) at fs/exec.c:1513
#5  0xffffffff811e4cd9 in do_execve (__envp=<optimized out>, __argv=<optimized out>,
    filename=<optimized out>) at fs/exec.c:1555
#6  SYSC_execve (envp=<optimized out>, argv=<optimized out>, filename=<optimized out>)
    at fs/exec.c:1609
#7  SyS_execve (filename=<optimized out>, argv=32422696, envp=32196616) at fs/exec.c:1604
#8  0xffffffff8176c6c9 in stub_execve () at arch/x86/kernel/entry_64.S:648
#9  0x00007f196561cdb7 in ?? ()
Backtrace stopped: previous frame inner to this frame (corrupt stack?)

(gdb) p regs
$1 = (struct pt_regs *) 0xffff8800369cbf58
(gdb) p *regs
$2 = {r15 = 0, r14 = 0, r13 = 0, r12 = 0, bp = 0, bx = 0, r11 = 0, r10 = 0, r9 = 0, r8 = 0,
  ax = 0, cx = 0, dx = 0, si = 0, di = 0, orig_ax = 59, ip = 139747051818423, cs = 51,
  flags = 518, sp = 140733820489592, ss = 43}

普通に使えそう。というわけで、Linux カーネルとの距離を少し縮められた気がします。

デバッグ テクニックに関しては、Ubuntu のカーネル チームのサイトが充実していて、今後お世話になりそう。

Kernel/Debugging – Ubuntu Wiki
https://wiki.ubuntu.com/Kernel/Debugging

Setting up a transparent proxy over SSL #4 – netcat/socat

ここまで Squid で透過/非透過プロキシを設定しました。原理的に SSL だとプロキシできないことも分かりました。で、お前は一連の作業で一体何をしたいんだ、ということを白状すると、MITM (= Man-in-the-middle attack; 中間者攻撃) の環境を 1 から用意しようとしていました。クライアントとサーバーの間のネットワークに侵入して、パケットを盗んだり改竄したりする攻撃手法です。MITM を作るためのツールやプラットフォームは多々あるのでしょうが、それらを使わずに実現したいのです。

調べていくと、Linux カーネルのネットワーク実装がとても柔軟で、iptables コマンドを使えば NAT や IP マスカレードが自由に行えることが分かりました。この時点で、NIC を 2 枚持つ Linux マシンを立ててカーネルを弄れば、お好みの MITM 環境が完成♪ ということになります。しかし、私自身がまだ Linux カーネルとはよい関係を築けていないので、できればユーザー モードでやりたい、という考えがありました。しかも iptales の実装は魔境だという情報もありましたし。そこで、適当なオープン ソースのプロキシ サーバー (今回は Squid) を使えば、ソケットのやり取りとかの面倒な部分は省いて、純粋にデータだけを弄れるんじゃないかという発想になりました。しかしこれまで説明したように、SSL の場合はプロキシではなくトンネリングになります。でも今回の目的であればトンネリングだとしても、SSL パケットがユーザー モードの Squid を通過するので全く問題がありません。むしろ好都合なぐらいです。

Squid のコードを見ると、トンネリングは ClientHttpRequest::processRequest の中で HTTP CONNECT メソッドを検出したときに tunnelStart() を呼ぶところから始まるようです。しかし CONNECT は非透過プロキシでは使われません。MITM をするのにプロキシの設定が必要、なんてのはさすがにかっこ悪いものです。

透過プロキシのポートに SSL のパケットを流入させると、Squid はパケットの内容が読めないのでエラーを出力します。このエラーは、parseHttpRequest() から HttpParserParseReqLine() を呼んで、戻り値が -1 になることで発生しています。HTTP メッセージのパースは、HttpParser::parseRequestFirstLine() で行われていますが、この関数は HTTP ペイロードをテキストだと想定して "HTTP/" といった文字列を検索しています。実際にデバッガーで確認すると、SSL パケットが来たときには HttpParser::buf の内容が "16 03 00 00 35 01 00 00 …" のように SSL Client Hello の内容になっています。したがって、HTTP のパースの時に Client Hello を見つけたらトンネリングを開始するように実装を変えれば、Squid は透過プロキシとして、SSL ではトンネリングを行なうようにできます。

しかしここまで考えると、トンネリングなんてデータを受け流すだけの簡単なお仕事であり、わざわざ Squid のような大きなプログラムを変更するのは労力の無駄に思えます。2 つソケットを用意して何も考えずに双方向にデータを転送すればいいのだから、むしろ 1 からプログラムを書いた方が簡単、というか絶対誰かそういうのを作っているはず、ということで見つけました。netcat 及び socat というプログラムです。

4.socat による TCP リレー

netcat には流派があり、使えるオプションがそれぞれ異なります。少なくとも以下の 3 種類見つけました。

  • GNU Netcat (TCP/IP swiss army knife)
    netcat-traditional とも呼ばれる。VyOS には /bin/nc.traditional というファイル名でインストールされている。
  • OpenBSD Netcat
    Ubuntu Server に /bin/nc.openbsd というファイル名でインストールされている。nc というエイリアスで実行すると大体これ。
  • NMap Netcat
    NMap という素敵なツールと一緒に開発されている。ncat というエイリアスで実行すると大体これ。

netcat の便利なところは、ストリーム ソケットからのデータを標準出力や標準入力などのストリームに転送でき、スクリプトやプログラムに渡せるところです。今回実現させたいのは、ルーティングによってプロキシ サーバーに来た 443/tcp ポート宛てのパケットを、本来の Web サーバーに渡すことです。ただし、前回の記事の後半で試しましたが、VyOS からやってきた 80/tcp ポート宛てのパケットは、そのままだと Squid が受信することはできず、iptables によるポート転送が必要でした。今回も同様と考えられるので、プロキシ サーバー側で 443/tcp ポート宛てのパケットは 3130/tcp に転送することにします。これでプロキシ サーバーが待機するポートは 3 つになります。

  • 3128/tcp — Squid 非透過プロキシ用ポート (HTTP だとプロキシ、HTTP CONNECT が来るとトンネリングを開始)
  • 3129/tcp — Squid HTTP 透過 プロキシ用ポート
  • 3130/tcp — 443/tcp パケットのトンネリング用

この後行ないたいのは、3130/tcp ポートに来たパケットを Web サーバーにリレーすることです。これは以下のコマンドで実現できます。3130/tcp ポートで待機しておいて、パケットが来たら新たに ncat プロセスを起動して、Web サーバーの 443/tcp ポートに送信するというコマンドです。ここで使っている -e オプションは NMap Netcat 特有のものです。

$ ncat -e "ncat www.somewhere.com 443" -l 3130 -k

しかしここで非透過プロキシのときと同じ、「宛先の Web サーバーはどこにすればいいのか」 という問題が出てきます。これについては後で書くことにして、ここでは標的を決め打ちして、とりあえず全部 https://ubuntu-web.cloudapp.net/ に転送するようにしておきます。

Ubuntu Server に NMap Netcat は入っていないので、インストールから行ないます。後でコードを変更する可能性があるので、ソースからビルドします。本家サイトは以下。2015/1/9 現在の最新版は 6.47 です。

Download the Free Nmap Security Scanner for Linux/MAC/UNIX or Windows
http://nmap.org/download.html

実行したコマンドは以下の通り。ncat だけ使えればいいので、外せるものは外しました。OpenSSL はビルドしたものをリンクさせます。

$ wget http://nmap.org/dist/nmap-6.47.tar.bz2
$ tar -jxvf nmap-6.47.tar.bz2
$ cd nmap-6.47/
$ ./configure –prefix=/usr/local/nmap/nmap-6.47 \
> –with-openssl=/usr/local/openssl/current \
> –without-ndiff \
> –without-zenmap \
> –without-nping \
> –without-liblua \
> –without-nmap-update
$ make
$ sudo make install
$ sudo ln -s /usr/local/nmap/nmap-6.47 /usr/local/nmap/current
$ sudo ln -s /usr/local/nmap/current/bin/ncat /bin/ncat

make install を行なうと、strip コマンドが実行され、シンボル情報が消されてからインストールが行われます。インストールした後のバイナリでデバッグを行う場合には、Makefile を変更する必要があります。

install: $(TARGET)
        @echo Installing Ncat;
        $(SHTOOL) mkdir -f -p -m 755 $(DESTDIR)$(bindir) $(DESTDIR)$(mandir)/man1
        $(INSTALL) -c -m 755 ncat $(DESTDIR)$(bindir)/ncat
        # $(STRIP) -x $(DESTDIR)$(bindir)/ncat
        if [ -n "$(DATAFILES)" ]; then \
                $(SHTOOL) mkdir -f -p -m 755 $(DESTDIR)$(pkgdatadir); \
                $(INSTALL) -c -m 644 $(DATAFILES) $(DESTDIR)$(pkgdatadir)/; \
        fi
        $(INSTALL) -c -m 644 docs/$(TARGET).1 $(DESTDIR)$(mandir)/man1/$(TARGET).1

インストールはこれで終わりです。プロキシ サーバー上でポート転送の設定を行います。

$ sudo iptables -t nat -A PREROUTING -i eth0 -p tcp –dport 443 \
> -j REDIRECT –to-port 3130
john@ubuntu-proxy:~$ sudo iptables -t nat -L
Chain PREROUTING (policy ACCEPT)
target     prot opt source               destination
REDIRECT   tcp  –  anywhere             anywhere             tcp dpt:http redir ports 3129
REDIRECT   tcp  –  anywhere             anywhere             tcp dpt:https redir ports 3130

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
john@ubuntu-proxy:~$

次に、VyOS 上で、80/tcp に加えて 443/tcp もプロキシ サーバーにルーティングされるようにルールを追加します。

$ configure
# set policy route PROXYROUTE rule 110 protocol tcp
# set policy route PROXYROUTE rule 110 destination address 0.0.0.0/0
# set policy route PROXYROUTE rule 110 destination port 443
# set policy route PROXYROUTE rule 110 source address 10.10.0.0/16
# set policy route PROXYROUTE rule 110 source mac-address !00:15:5d:01:02:12
# set policy route PROXYROUTE rule 110 set table 100
# commit
# save
# exit
$

あとは ncat を起動するだけです。とても高機能ではありますが、言ってしまえば Netcat は単なるソケット通信プログラムなので、root 権限は必要ありません。

$ ncat -version
Ncat: Version 6.47 (
http://nmap.org/ncat )
Ncat: You must specify a host to connect to. QUITTING.

$ ncat –listen 3130 –keep-open \
> –exec "/usr/local/nmap/current/bin/ncat ubuntu-web.cloudapp.net 443"

外部プログラムを起動するオプションには、–exec  (-e) と –sh-exec (-c) の 2 種類があります。前者は実行可能ファイルを直接実行するオプションで、後者はシェル経由でプロセスを実行します。今回はスクリプトを使う予定はないので、シェルを介在することのオーバーヘッドを嫌って –exec を使いました。そのため、相対パスではなく絶対パスが必要です。

クライアント側でプロキシの設定がされていないことを確認し、https://ubuntu-web.cloudapp.net/ にアクセスできることを確認します。また、ncat を終了するとサイトにアクセスできなくなります。今回はオリジナルの宛先に関わらず、全部のパケットを ubuntu-web に転送するので、それ以外の HTTPS サイトを見ることはできません。本来は VyOS の PBR、もしくはプロキシ サーバーの iptables で、宛先の IP アドレスによるフィルタリングが必要です。

これで、MITM の環境はほぼ整いました。ですが、リレーするのにわざわざ新しいプロセスを起動するのがスマートではありません。もっと単純に、2 つのソケットを扱えるプログラムはないのかと探したところ、見つけました。それが socat です。なんと、はてな検索のサイトで見つけました。

Linux で、TCP 接続に (というか IP パケットに) 何も手を加えず… – 人力検索はてな
http://q.hatena.ne.jp/1262698535

socat の本家サイトはこちらです。nmap や socat に関しては、ペネトレーション テストという名のものとで行われるハッキング ツールとも言えそうです。サイトも玄人志向な感じです。

socat
http://www.dest-unreach.org/socat/

ncat が手を一本しか持っていないのに対し、socat は二本持っています。まさに探していたものです。さっそくビルドします。2015/1/9 現在の最新バージョンは 1.7.2.4 です。デフォルトでコンパイル オプションに -g が付かないので、configure のときに明示的につけておきます。

$ wget http://www.dest-unreach.org/socat/download/socat-1.7.2.4.tar.bz2
$ tar -jxvf socat-1.7.2.4.tar.bz2
$ cd socat-1.7.2.4/
$ CFLAGS="-g -O2" ./configure –prefix=/usr/local/socat/socat-1.7.2.4
$ make
$ sudo make install
$ sudo ln -s /usr/local/socat/socat-1.7.2.4 /usr/local/socat/current
$ sudo ln -s /usr/local/socat/current/bin/socat /bin/socat

TCP リレーを行うためのコマンドは以下の通りです。これはかなり直観的です。素晴らしい。

$ socat TCP-LISTEN:3130,fork TCP:ubuntu-web.cloudapp.net:443

socat は、ソケットが閉じられたときにプロセスが終了するように作られています。一回の起動で複数のセッションを処理するためには、fork キーワードが必要です。こうすることで、セッション開始時にプロセスをフォークしてくれるので、次のセッションにも対応できるようになります。この仕様は不便なようにも感じますが、プロセスが処理しているセッションが必ず一つに限られるので、デバッグは楽になりそうです。

socat の中で実際にデータを扱っている関数は xiotransfer() です。ここで buff を弄れば、好きなようにパケットを改竄できます。シンプルでいい感じ。

これで環境はできました。残る課題は、転送先のアドレスを知る方法です。まだ実装していませんが、実現できそうなアイディアはあります。前回の記事の最後でパケットを採取しました。このとき、PBR によって VyOS から Squid サーバーに送られてきたパケットの先頭の宛先 IP アドレスは、tcpdump がデータを採取した段階ではまだ最終目的地の Web サーバーになっていました。これが iptables のルールを受けて変更され、Squid が受け取った時には宛先アドレスは分からなくなってしまうのです。つまり、プロキシ サーバーの OS 視点から見ればオリジナルのアドレスは分かっているのです。あくまでも、Squid などのユーザー モードから見えない、というだけの話です。

ソケット プログラミングでサーバーを書くときの基本の流れは、socket() –> bind() –> listen() –> accept() –> recv() となります。accept() を呼ぶと待ち状態に入って、データを受信すると制御が返ります。bind() のところで、待機する IP アドレスやポートを指定できますが、IP アドレスには INADDR_ANY を指定すると、すべてのインタフェースからのデータを受信できるようになります。マシンが複数の IP アドレスを持っている場合、データを受信した後に、どの IP アドレスでデータを受信したのかを知りたいことがあります。そんなときは、accept() の戻り値に対して getsockname() 関数を呼び出すことができます。

getsockname(2): socket name – Linux man page
http://linux.die.net/man/2/getsockname

試しに、PBR されてきたパケットに対して getsockname() を呼び出してみましたが、返ってきたのはオリジナルの宛先アドレスではなく、やはり自分のアドレスに変更されていました。まだ試していませんが、以下のページに説明がある IP_ORIGDSTADDR メッセージを取得すれば、オリジナルのアドレスを取得できそうなのですが、今のところ上手く動いてくれません。もし駄目だった場合は、Linux カーネルを弄るしかないようです。

ip(7): IPv4 protocol implementation – Linux man page
http://linux.die.net/man/7/ip

(2015/1/19 追記)
以下の記事に書きましたが、オリジナルのアドレスは getsockopt に対して SO_ORIGINAL_DST オプションを渡すことで簡単に取得できました。IP_RECVORIGDSTADDR/IP_ORIGDSTADDR に関しては、カーネルのコードを見ると ip_cmsg_recv_dstaddr というそれっぽい関数があるのですが、ip_recv_error 経由でしか呼び出されません。ユーザー モード側からフラグとして MSG_ERRQUEUE を渡して recvmsg を呼ぶと、ip_recv_error にたどり着くところまでは確認しました。あまり深く追いかけていませんが TCP のストリーム ソケットに対して IP_ORIGDSTADDR を使うことは想定されていないようです。

How to get the original address after NAT | すなのかたまり
https://msmania.wordpress.com/2015/01/15/how-to-get-the-original-address-after-nat/

最後に補足情報を。SSL の透過プロキシについて調べているときに、以下の OKWave の質問スレッドを見つけました。この中のアイディアが面白かったので紹介します。

透過型プロキシのHTTPS通信 【OKWave】
http://okwave.jp/qa/q6553861.html?by=datetime&order=ASC#answer

クライアントからCONNECTリクエストを受信したのち,本来のCONNECT先のホスト名のサーバとSSLハンドシェイクを行って,サーバ証明書を取得し,それと同じCNを持つ証明書をつくり,プロキシが署名します.で,それを使ってクライアントとsslハンドシェイクします.クライアントには,プロキシの署名が信頼できるものだとするために,ルート証明書をインストールしておきます.つまり,プロキシはCONNECTが来るまで,どのサーバに接続するかしらないということです.

同じ CN を持つ証明書を動的に作るというアイディアは、ルート証明書の課題があるとは言え、問題なく実装できそうです。自分の銀行のオンライン バンクのサイトが、どの認証局によって署名されているかなんて誰も知らないでしょうし。もし上記のような MITM 攻撃を受けると、クライアントからは、本来の認証局とは違う認証局の署名を持つ証明書を受け取ることになります。もちろん、OS にもともと入っているような真っ当な認証局であれば、証明書署名要求にある CN 名のドメインのオーナーの身分調査などをしっかり行うはずなので、既に存在する CN 名を持つ証明書には署名を行わないはずです。

ただし、多くのユーザーは SSL の仕組みや、サイトにアクセスしたときに出てくる証明書に関する警告メッセージの詳細なんて知りません。オンライン バンクにアクセスしたときに、信頼されていないルート証明書による署名が検出された、という警告が出たとして、構わず OK をクリックするユーザーは一定数いるはずです。万が一、有名な認証局の秘密鍵が盗まれたとすると、この攻撃手法が完全に有効になり、クライアント側からは MITM の有無を区別することは不可能になります。商用 CA やインターネットの世界でなくても、例えば企業の IT 部門に悪意あるものが侵入するということがあり得ます。企業が Active Directory を導入していれば、グループ ポリシーを少し弄って、自分の作ったルート証明書を配布してしまうことは可能です。

このように MITM は現在でもとても有効な攻撃手法です。例えば自分がカフェのオーナーだったとして、店舗に WiFi アクセスポイントを設置して、ルーターにちょこちょこっと細工をすれば簡単に MITM は実装できてしまいます。自分がオーナーじゃなくても、例えば企業内のどこかに置かれているルーターのところに行って、LAN ケーブルをささっと繋ぎ直して、おれおれルーターを設置する、ということも可能かもしれません。

セキュリティや IT の世界でも、情報の非対称性の存在が顕著だと言えそうです。(結論それか

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 #2 – squid

前回の記事で、VyOS をインストールした Hyper-V 仮想マシンをルーターとして使えるようになりました。次に HTTP プロキシを導入します。

2. squid による HTTP/HTTPS 非透過プロキシ

最終的に作るのは透過プロキシですが、先に非透過プロキシを作ります。ここで透過プロキシという単語の定義は曖昧なので注意が必要です。よく使われる意味としては 2 つあります。

  1. クライアント側での設定変更を必要としないように構成されたプロキシ
    プログラムの種類ではなく、ネットワーク構成で区別されるプロキシの種類です。いろいろと検索した結果、この意味で使われることが多いです。英語でも transparent proxy と言えばこちらの意味が多い気がします。その他に intercepting proxy とも言います。
  2. 要求や応答を一切変更しないプロキシ
    HTTP/1.1 についての RFC 2616 (http://tools.ietf.org/html/rfc2616) には、A "non-transparent proxy" is a proxy that modifies the request or response という部分があります。1. の意味とは違い、プログラムの種類につけられた名前です。どちらかというと 1. の意味で使われることの方が多い気がします。intercepting proxy と言ったときには 2. の意味は持ちません。

日本語の wiki には 1. の意味しか載っていませんが、英語の方には両方載っています。

Proxy server – Wikipedia, the free encyclopedia
http://en.wikipedia.org/wiki/Proxy_server

プロキシの意味についても確認しておくと、サーバーとクライアントの間に入って通信を中継するプログラムのことです。RFC 2616 には、サーバーとしてもクライアントとしても動作する、と書かれています。通信を中継するためには、まずサーバーとして要求を受け取って、何か処理して、クライアントとして要求を送信することが必要だからです。プロキシとは英語で代理人を意味する単語ですが、これは、受け取った要求をそのまま送るのではなく、内容を変更するかどうかに関わらず、プロキシが自分を差出人として要求を再送信することからつけられた名前と考えられます。この定義にしたがうと、ルーティングのように、アドレス部分だけ書き換えて、ペイロードが同じパケットを受け流すようなプログラムはプロキシとは呼ばないことになります。

この記事、及びタイトルにおける transparent proxy は、1. の意味です。つまりクライアント側で設定を変更せずに、パケットを中継させるような環境を作ります。

今回は、Squid というオープン ソースのプログラムを選びました。検索して有名そうだったから、という理由だけです。イカのロゴかわいいし。

squid : Optimising Web Delivery
http://www.squid-cache.org/

インストールは、squid3 というパッケージを apt-get するだけでいいのですが、後々プロキシの動作を書き換えたい意図があったので、ソースからビルドすることにしました。2015/1/8 現在の最新バージョンである 3.4.10 をダウンロードします。

予め開発ツールである gcc や gdb をインストールしておきましょう。前に書いた記事が参考になると思います。

Apache Live-Debugging on Linux | すなのかたまり
https://msmania.wordpress.com/2014/12/23/apache-live-debugging-on-linux/

今回実行したコマンドはこのような感じです。ビルドされた OpenSSL が /usr/local/openssl/current にインストールされているという前提です。

$ sudo apt-get install build-essential libtool manpages-dev gdb
$ wget
http://www.squid-cache.org/Versions/v3/3.4/squid-3.4.10.tar.bz2
$ tar jxvf squid-3.4.10.tar.bz2
$ cd squid-3.4.10/
$ ./configure –prefix=/usr/local/squid/squid-3.4.10 \
> –with-openssl=/usr/local/openssl/current \
> –enable-ssl
$ make
$ sudo make install
$ sudo ln -s /usr/local/squid/squid-3.4.10 /usr/local/squid/current

インストール後、Squid サーバーの実行可能ファイルは bin ではなく sbin の方の /usr/local/squid/current/sbin/squid になります。

john@ubuntu-proxy:~$ /usr/local/squid/current/sbin/squid -v
Squid Cache: Version 3.4.10
configure options:  ‘–prefix=/usr/local/squid/squid-3.4.10’ ‘–with-openssl=/usr/local/openssl/current’ ‘–enable-ssl’

john@ubuntu-proxy:~$ /usr/local/squid/current/sbin/squid -h
Usage: squid [-cdhvzCFNRVYX] [-s | -l facility] [-f config-file] [-[au] port] [-k signal]
       -a port   Specify HTTP port number (default: 3128).
       -d level  Write debugging to stderr also.
       -f file   Use given config-file instead of
                 /usr/local/squid/squid-3.4.10/etc/squid.conf
       -h        Print help message.
       -k reconfigure|rotate|shutdown|restart|interrupt|kill|debug|check|parse
                 Parse configuration file, then send signal to
                 running copy (except -k parse) and exit.
       -s | -l facility
                 Enable logging to syslog.
       -u port   Specify ICP port number (default: 3130), disable with 0.
       -v        Print version.
       -z        Create missing swap directories and then exit.
       -C        Do not catch fatal signals.
       -D        OBSOLETE. Scheduled for removal.
       -F        Don’t serve any requests until store is rebuilt.
       -N        No daemon mode.
       -R        Do not set REUSEADDR on port.
       -S        Double-check swap during rebuild.
       -X        Force full debugging.
       -Y        Only return UDP_HIT or UDP_MISS_NOFETCH during fast reload.

上記 -h の出力にあるように、何も指定しなければ etc/squid.conf が設定ファイルとして勝手に読み込まれます。

設定ファイルを変更する前に、ここで実現したいことを確認しておきます。前回構築した VyOS も含め、下図のようなネットワークができました。

Capture

Client からインターネットに接続するには、Hyper-V 仮想ネットワークの Internal から External に出るための VyOS によるルーターと、家の LAN からインターネットに出るための物理ルーターの 2 つのルーターを通る必要があります。ただし、今回の記事において Hyper-V の External から先はほとんど意識する必要がありません。

Squid を入れたプロキシ サーバーは、Client と同じように Hyper-V 仮想マシンとして作成し、Internal のネットワークだけに接続しています。(正確には、apt-get をするためにインターネット接続が必要なので、External にも繋げていますが、普段は無効化しています。)

この状態で Squid をプロキシとして介在させるには、Client –> Internal network –> VyOS –> External network となっている経路のどこかで Squid サーバーを見に行くように設定しないといけません。

透過ではないプロキシであれば、ブラウザーの設定でプロキシのアドレスとして 10.10.90.11 を設定すればいいことになります。この場合の経路は、Client –> Squid –> VyOS –> External network になります。クライアントから Internal network にある Squid に直接接続するところがポイントです。

冒頭の定義で確認したように、今回はクライアントの設定を変えずにプロキシを通すことが目標です。この場合、Client –> Squid というホットラインが使えないので、何らかの設定を VyOS 側で行わないといけないのが分かると思います。すなわち経路としては、Client –> VyOS –> Squid –> VyOS –> External network としたいのです。Squid も External へのアクセスを持たないので、VyOS を 2 度通らないといけません。

いずれの場合も、Squid が VyOS をゲートウェイとして使う必要があるので、忘れずにゲートウェイの設定をしておきます。Squid サーバー上で /etc/network/interfaces を以下のように設定しました。

auto lo
iface lo inet loopback

auto eth0
iface eth0 inet static
address 10.10.90.11
netmask 255.255.0.0
gateway 10.10.90.12
dns-nameservers 10.10.10.10

iface eth1 inet dhcp

DNS サーバー参照の設定が必要な理由は後述します。DNS の設定を /etc/resolv.conf に反映させるため、eth0 を再起動します。

$ sudo ifdown eth0
$ sudo ifup eth0

OS 側の設定は終了です。次に設定ファイル squid.conf をこのように変更しました。一部の抜粋で、青字が変更部分です。

# Example rule allowing access from your local networks.
# Adapt to list your (internal) IP networks from where browsing
# should be allowed
# acl localnet src 10.0.0.0/8   # RFC1918 possible internal network
acl localnet src 10.10.0.0/16   # Hyper-V Internal
acl localnet src 172.16.0.0/12  # RFC1918 possible internal network
acl localnet src 192.168.0.0/16 # RFC1918 possible internal network
acl localnet src fc00::/7       # RFC 4193 local private network range
acl localnet src fe80::/10      # RFC 4291 link-local (directly plugged) machines

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

(..snip..)

# Impersonation for logging to a file
cache_effective_user john

定義で確認した通り、プロキシは、サーバーとクライアント両方の役割を持っています。非透過プロキシとして 3128/tcp、透過プロキシとして 3129/tcp のポートを使ってサーバーとして動作するようにしました。3128/tcp は Squid のデフォルト ポートです。設定や動作の違いを見るため、非透過プロキシと透過プロキシの両方を設定してみます。

cache_effective_user は、Squid の動作仕様による設定です。Squid を root 権限で起動させると、その後の動作は root ではなく cache_effective_user で設定されたユーザーの UIDを使って動作します。何も設定しないと nobody というユーザーの UID が使われ、ログファイルなどを書き込もうとするときに、書き込み権限がないためのエラーが出ます。Squid 用の専用アカウントを作るのが標準的だとは思いますが、ここでは作業用ユーザーの john を設定しておきます。

ログが書き込めるように、var/logs ディレクトリの所有者を john にしておきます。

john@ubuntu-proxy:~$ ls -la /usr/local/squid/current/var/logs
total 8
drwxr-xr-x 2 root root 4096 Jan  8 11:43 .
drwxr-xr-x 5 root root 4096 Jan  8 11:28 ..
john@ubuntu-proxy:~$ sudo chown john:john  /usr/local/squid/current/var/logs
john@ubuntu-proxy:~$ ls -la /usr/local/squid/current/var/logs
total 8
drwxr-xr-x 2 john john 4096 Jan  8 11:43 .
drwxr-xr-x 5 root root 4096 Jan  8 11:28 ..
john@ubuntu-proxy:~$

これで準備が整いました。今回はテストなので、デーモンとしてバックグラウンドで動かすのではなく、-N をつけて非デーモン モードで実行します。ログがファイルの var/logs/cache.log だけでなく標準出力にも出るので分かりやすいです。

john@ubuntu-proxy:~$ sudo /usr/local/squid/current/sbin/squid -Nd1
2015/01/08 13:04:49| Set Current Directory to /usr/local/squid/squid-3.4.10/var/cache/squid
2015/01/08 13:04:49| Starting Squid Cache version 3.4.10 for x86_64-unknown-linux-gnu…
2015/01/08 13:04:49| Process ID 55453
2015/01/08 13:04:49| Process Roles: master worker
2015/01/08 13:04:49| With 1024 file descriptors available
2015/01/08 13:04:49| Initializing IP Cache…
2015/01/08 13:04:49| DNS Socket created at [::], FD 5
2015/01/08 13:04:49| DNS Socket created at 0.0.0.0, FD 6
2015/01/08 13:04:49| Adding nameserver 10.10.10.10 from squid.conf
2015/01/08 13:04:49| Logfile: opening log daemon:/usr/local/squid/squid-3.4.10/var/logs/access.log
2015/01/08 13:04:49| Logfile Daemon: opening log /usr/local/squid/squid-3.4.10/var/logs/access.log
2015/01/08 13:04:49| Store logging disabled
2015/01/08 13:04:49| Swap maxSize 0 + 262144 KB, estimated 20164 objects
2015/01/08 13:04:49| Target number of buckets: 1008
2015/01/08 13:04:49| Using 8192 Store buckets
2015/01/08 13:04:49| Max Mem  size: 262144 KB
2015/01/08 13:04:49| Max Swap size: 0 KB
2015/01/08 13:04:49| Using Least Load store dir selection
2015/01/08 13:04:49| Set Current Directory to /usr/local/squid/squid-3.4.10/var/cache/squid
2015/01/08 13:04:49| Finished loading MIME types and icons.
2015/01/08 13:04:49| HTCP Disabled.
2015/01/08 13:04:49| Squid plugin modules loaded: 0
2015/01/08 13:04:49| Accepting HTTP Socket connections at local=[::]:3128 remote=[::] FD 9 flags=9
2015/01/08 13:04:49| Accepting NAT intercepted HTTP Socket connections at local=[::]:3129 remote=[::] FD 10 flags=41
2015/01/08 13:04:50| storeLateRelease: released 0 objects

起動したようです。プロセスと TCP ポートを見てみると、確かに squid は john として動いています。

john@ubuntu-proxy:~$ ps -ef | grep squid
root     55452  1115  0 13:04 pts/1    00:00:00 sudo /usr/local/squid/current/sbin/squid -Nd1
john     55453 55452  0 13:04 pts/1    00:00:00 /usr/local/squid/current/sbin/squid -Nd1
john     55454 55453  0 13:04 ?        00:00:00 (logfile-daemon) /usr/local/squid/squid-3.4.10/var/logs/access.log
john     55465 55016  0 13:07 pts/2    00:00:00 grep –color=auto squid
john@ubuntu-proxy:~$ netstat -aon | grep 31
tcp6       0      0 :::3128                 :::*                    LISTEN      off (0.00/0/0)
tcp6       0      0 :::3129                 :::*                    LISTEN      off (0.00/0/0)

実はこれで非透過プロキシとしての設定は終了です。クライアント側の設定を変更して動作を確認します。

inetcpl.cpl –> Connections タブ –> LAN settings でプロキシの設定ができます。全てのプロトコルで 10.10.90.11:3128 を使うように設定しました。

image
Windows のプロキシ設定

これでブラウザーは通常通り動作するはずです。bing.com にアクセスした際の Squid のアクセスログを見てみると、何やらいろいろとログが書き込まれており、プロキシとしてまともに動いている、はずです。Squid の目玉機能であるキャッシュの機能は見ないので、ブラウザーからページが開ければ OK なのです。

john@ubuntu-proxy:~$ tail /usr/local/squid/current/var/logs/access.log
1420753075.003    463 10.10.20.71 TCP_MISS/200 73782 GET
http://www.bing.com/ – HIER_DIRECT/204.79.197.200 text/html
1420753075.205    157 10.10.20.71 TCP_MISS/200 710 POST
http://www.bing.com/rewardsapp/reportActivity – HIER_DIRECT/204.79.197.200 application/x-javascript
1420753075.206    106 10.10.20.71 TCP_MISS/200 503 GET
http://www.bing.com/fd/ls/l? – HIER_DIRECT/204.79.197.200 image/gif
1420753075.251    112 10.10.20.71 TCP_MISS/200 2255 GET
http://www.bing.com/HPImageArchive.aspx? – HIER_DIRECT/204.79.197.200 application/json
1420753075.367    171 10.10.20.71 TCP_CLIENT_REFRESH_MISS/200 493 HEAD
http://www.bing.com/rms/Framework/jc/9aaeda73/23d0202d.js? – HIER_DIRECT/204.79.197.200 application/x-javascript
1420753075.391    256 10.10.20.71 TCP_MISS/200 20970 GET
http://www.bing.com/hpm? – HIER_DIRECT/204.79.197.200 text/html
1420753075.397    202 10.10.20.71 TCP_MISS/200 519 GET
http://www.bing.com/notifications/render? – HIER_DIRECT/204.79.197.200 text/html
1420753075.708    100 10.10.20.71 TCP_MISS/200 503 GET
http://www.bing.com/fd/ls/l? – HIER_DIRECT/204.79.197.200 image/gif
1420753075.957    328 10.10.20.71 TCP_MISS/200 7528 CONNECT login.live.com:443 – HIER_DIRECT/131.253.61.66 –
1420753076.656    110 10.10.20.71 TCP_MISS/200 982 GET
http://www.bing.com/Passport.aspx? – HIER_DIRECT/204.79.197.200 t

では次に、twitter.com にアクセスしてみます。URL を見ると https:// で始まっているように、ツイッターとの通信は SSL で暗号化されています。クライアントの設定で、全プロトコルがプロキシを使うようにしているので、SSL でも Squid が介在するはずであり、結果は問題なくページを開くことができます。アクセス ログを見てみます。ログに出てくる CONNECT というのは HTTP の CONNECT メソッドのことです。

john@ubuntu-proxy:~$ tail /usr/local/squid/current/var/logs/access.log
1420753532.051 110352 10.10.20.71 TCP_MISS/200 51273 CONNECT twitter.com:443 – HIER_DIRECT/199.59.150.39 –
1420753532.051  88048 10.10.20.71 TCP_MISS/200 956439 CONNECT abs.twimg.com:443 – HIER_DIRECT/93.184.216.146 –
1420753532.052  70182 10.10.20.71 TCP_MISS/200 5350 CONNECT abs.twimg.com:443 – HIER_DIRECT/93.184.216.146 –
1420753532.052  70181 10.10.20.71 TCP_MISS/200 6320 CONNECT abs.twimg.com:443 – HIER_DIRECT/93.184.216.146 –
1420753532.052  70182 10.10.20.71 TCP_MISS/200 5318 CONNECT abs.twimg.com:443 – HIER_DIRECT/93.184.216.146 –
1420753532.052  70182 10.10.20.71 TCP_MISS/200 6320 CONNECT abs.twimg.com:443 – HIER_DIRECT/93.184.216.146 –
1420753532.113  85991 10.10.20.71 TCP_MISS/200 8153 CONNECT iecvlist.microsoft.com:443 – HIER_DIRECT/93.184.215.200 –
1420753532.113 109447 10.10.20.71 TCP_MISS/200 4111 CONNECT twitter.com:443 – HIER_DIRECT/199.59.150.39 –

実は、Squid が HTTPS を HTTP と同じようにプロキシできる、というのはおかしいのです。というか、実際には HTTPS についてはプロキシはしていないので、HTTP と同じようにページのキャッシュなどは行われません。理由は簡単で、Squid は暗号化された通信を解読できないためです。

冒頭に書いたように、プロキシはルーティングと異なり、サーバーとしてデータを受け取ってから、新しいリクエストを自分の力で代理人として送信しています。この過程で、パケットの内容を 「解釈」 する必要があるのです。ここで疑問として、「いやいやプロキシがデータを解釈しなくても、HTTPS だろうがなんだろうが、受け取った TCP パケットをまるっとコピーして、新しいパケットとして送信すればいいじゃないか」 と考えるかもしれません。しかしここでのポイントは、プロキシに入ってくるパケットにおいて、宛先 IP アドレス、宛先 TCP ポートは、最終目的地の Web サーバーのものではなく、プロキシのアドレス、ポート番号になっていることです。Windows のプロキシ設定を行なうと、クライアントから要求が送信される時点で、既に宛先はプロキシに設定されているのです。

これは同時に、なぜ Squid サーバー側で DNS サーバー参照設定を行わなければならないか、という理由に繋がります。SSL ではなく HTTP をプロキシする場合、Squid は、IP レベル (OSI 参照モデルのネットワーク層) の宛先 IP アドレスではなく、HTTP レベル (OSI 参照モデルのアプリケーション層) の HTTP 要求に必ず含まれている (たぶん HTTP ヘッダーの Host) 宛先情報を読み取って、データを送信しています。多くの場合、HTTP 要求ヘッダーに含まれる宛先情報は IP アドレスではなくホスト名であるため、Squid が名前解決をしないといけません。これは逆に考えると、クライアント側では名前解決が必要ないということになります。実は、クライアント側で DNS サーバー参照の設定を行わなくても、非透過プロキシを経由した通信は問題なく可能です。

そこで問題となるのが HTTPS です。Squid は暗号化されたペイロードを読めず、自分に送られてきたパケットをどこに送信すればいいか分かりません。では、上の例だと、なぜクライアントはツイッターにアクセスすることができたのでしょうか。理由は、Squid は 「ズル」 をさせてもらっているからです。どういうズルかというと、Squid は、これから始まる  TCP セッションの本当の宛先を、ブラウザーから前もって平文で教えてもらっているのです。これがログに出てくる HTTP CONNECT メソッドです。

クライアント側でキャプチャーを取って、HTTP CONNECT の内容を見てみました。どちらが使われているか分かりませんが、URI と Host という二つのフィールドに、本当の宛先である twitter.com:443 という値が設定されています。また、パケットの宛先が全て 10.10.90.11:3128 になっていることも確認できます。

image

HTTP CONNECT で宛先を教えてもらった後は、Squid は何も頭を使わず、送られてくる暗号化データを受け流す簡単なお仕事をするだけです。トンネリングとも呼びます。ブラウザー側はプロキシの設定に基づいて CONNECT メソッドを送り、プロキシから OK が返ってきたら、ClientHello から始まる鍵交換や、その後の暗号化通信を何事もなかったかのように始めます。CONNECT メソッドは Web サーバーには送信されません。

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 を設定してもよいです。