pingの向う側
里帰り
長らく友人宅に身を寄せていた本が、里帰りしてきた。 4.4BSDの設計と実装
家にはUNIXカーネルの設計と言う本が有るんだけど、対象がSYSTEM Vって事で、ネットワークの事が、なおざりに解説されてた。やはり本場の本と言う事で、急遽呼び戻した次第。
20世紀の最後に原著は出ているんだけど、日本語版が出たのは2003年。原著の冒頭にBSD本は、これで最後だろう。米国でもリナ本が優勢になってるなんて書かれていた。
あ、思い出した、BSD MAGAZINEがASCIIから出てたな。楽しく読んだ記憶がある。外国でのBSDカンファレンスが有る時、このマガジンを持って行くと、大喜びされたとか。オイラーも再読してみたい。
なんか、最近は電子出版が盛んだけど、これって未来永劫、読めるのかね? 数十年したら、出版元が倒産して、電子本にロックがかかってしまいましたとか、リーダー(キンドルみたいなやつね)が壊れてしまったので読めませんとかならないかね。
今回この厚い本を膝に置き、重い思いをして、ふとそんな事を考えてしまったぞ。
qemu
前回からpingを調べてる。今度はpingの向う側と称して、カーネル側を見ておきたい。 それには、カーネルをgdb出来る環境が必要。今まで何度も登場してるけど、qemu上でゲストOSとしてOpenBSD(6.6)を動かしている。で、qemuを動かしている、いわゆるホストOSは、debianだ。
やりたい事は、pingによって発行されるシステムコールを追ってみたい事。そして2番目の観光目的は、pingが発するICMPなパケットをカーネルがどうさばいているか観察する事。
この2番目の目標は、OpenBSDがICMPのパケットの受信側になる必要が有るって事だ。そんなの簡単だろう。ping 自host でいいんでないかい。実際にそれをやってパケットモニタすると、パケットが出ていない。まあ、意味無いから当然か。
だったらホストOS側からping打てばいいんでないかい。残念、それは出来ない相談。だってゲストOSはNATの中に隠れていて、鉄壁な守りを固めているから歯が立たないのよ。
そこでだ、NATを止めて、ゲストOSをホストOSと同一ネットに置いてしまおうと言う案が出てくる。ググると、混沌としたリナの世界が見えてくる。
ブリッジデバイスを作る。これがHUBになる。そこにホストOS側のeth0を刺す。tapデバイスも作って、やはりHUBに刺す。このtapはゲストOS側とツーツーになってる(?)。 ブリッジデバイスは、eth0って物理デバイスなんでインターネットに接続されてる。どうやら、こういう仕組みみたい。
色々やってみたけど、希望は達成出来ず。みんなリナばかりの試行なんで、ゲストOSがOpenBSDでも動くのか、甚だ疑問。ここで足踏みしててもしょうが無いので、別の手をかんがえる。
パケット詐欺
qemuの中だけで完結する方法を考える。
ping 10.0.2.2 ってやれば、自ホストである 10.0.2.15 から、問い合わせパケットが飛び、それがオウム返しに帰ってくる。この帰ってきたパケットを、問い合わせパケットに捏造しちゃったらどうよ。
10.0.2.2 -> 10.0.2.15 って事で、自ホストがパケットを処理するのではなかろうか。 それには、一度パケットをキャプチャし、返信パケットだけを抜き出し、編集(捏造)、それを送り出す。 キャプチャや送り出しは、今まで何度もお世話になってるpkttoolsを使う事にする。 幸いNATでも、外からファイルを取り込む事はできるから、コンパイルに多少時間がかかっても、pkttools群は使えるよ。
-- 1 -- received: 98 bytes 1625635176.702412 Wed Jul 7 14:19:36 2021 Ethernet 52:54:00:12:34:56 -> 52:55:0a:00:02:02 (type: 0x0800) IP head/total size : 20 / 84 bytes ID/fragment : 0xd038 / 0x0000 TTL/protocol : 255 / 1 checksum : 0xd35f (Valid) src/dst addr : 10.0.2.15 / 10.0.2.2 ICMP total size : 64 bytes type/code : 8 / 0 checksum : 0x3667 (Valid) == -- 2 -- received: 98 bytes 1625635176.706679 Wed Jul 7 14:19:36 2021 Ethernet 52:55:0a:00:02:02 -> 52:54:00:12:34:56 (type: 0x0800) IP head/total size : 20 / 84 bytes ID/fragment : 0x0076 / 0x0000 TTL/protocol : 255 / 1 checksum : 0xa322 (Valid) src/dst addr : 10.0.2.2 / 10.0.2.15 ICMP total size : 64 bytes type/code : 0 / 0 checksum : 0x3e67 (Valid) ==
2番目のパケットのtypeを0から8に変更すれば(それに伴いchecksumも)、送り出しパケットとして、利用できる。
それで、カーネル側で網を張るとしたら、 net/if_switch.c/switch_flow_classifier_icmpv4
のあたりかなあ。ソースを頭から眺めて行って、それらしいのが見つかったって言う、根拠の無い想像だけどね。
で、さっぱり網にかからない。何でかな? ええい、tcpdumpで、パケット観測しちゃえ。
vm# tcpdump -t tcpdump: listening on em0, link-type EN10MB 10.0.2.2 > 10.0.2.15: icmp: echo request 10.0.2.15.36781 > 10.0.2.3.domain: 8158+ PTR? 2.2.0.10.in-addr.arpa.(39) 10.0.2.3.domain > 10.0.2.15.36781: 8158 NXDomain 0/0/0(39) 10.0.2.15.31488 > 10.0.2.3.domain: 36760+ PTR? 15.2.0.10.in-addr.arpa.(40) 10.0.2.3.domain > 10.0.2.15.31488: 36760 NXDomain 0/0/0(40) 10.0.2.15.28602 > 10.0.2.3.domain: 37260+ PTR? 3.2.0.10.in-addr.arpa.(39) 10.0.2.3.domain > 10.0.2.15.28602: 37260 NXDomain 0/0/0(39) 10.0.2.2 > 10.0.2.15: icmp: echo request
確かにパケットは投げられていて、それにOSは反応して名前解決をしてる。けど、返事が無い。何か考え違いをしているのだろうか? それともOSが、悪巧みを見破って、返事を控えたのかな。
なんたって、オレオレ詐欺の一番の予防は、電話に出ない事です。仮に電話に出ても、冒頭で、この電話内容は、録音されてますって案内が出るような装置を、年寄世帯に配っているらしい。 警官が持って行っても、怪しんで取り付けを拒否される事が多いので、民生委員とペアで活動して、成果を上げているそうですよ。
catch icmp in kernel
詐欺パケットは諦めて、素直にデフォルトゲートウェイ目掛けてpingしてみる。網を張る所は少し変わって、netinetの中。なんたって、icmpはIP族ですから、ここに有るのさ。
(gdb) bt #0 icmp_input_if (ifp=0xd184b030, mp=0xf1cc6cbc, offp=0xf1cc6ca4, proto=1, af=2) at /usr/src/sys/netinet/ip_icmp.c:323 #1 0xd030535e in icmp_input (mp=0xf1cc6cbc, offp=0xf1cc6ca4, proto=1, af=2) at /usr/src/sys/netinet/ip_icmp.c:315 #2 0xd043ca5c in ip_deliver (mp=0xf1cc6cbc, offp=0xf1cc6ca4, nxt=1, af=2) at /usr/src/sys/netinet/ip_input.c:665 #3 0xd043a7b9 in ip_ours (mp=0xf1cc6cbc, offp=0xf1cc6ca4, nxt=1, af=0) at /usr/src/sys/netinet/ip_input.c:560 #4 0xd04395a3 in ip_input_if (mp=0xf1cc6cbc, offp=0xf1cc6ca4, nxt=4, af=0, ifp=0xd184b030) at /usr/src/sys/netinet/ip_input.c:345 #5 0xd0438f79 in ipv4_input (ifp=0xd184b030, m=0xd186d300) at /usr/src/sys/netinet/ip_input.c:215 #6 0xd05cfb2a in ether_input (ifp=0xd184b030, m=0xd186d300, cookie=0x0) at /usr/src/sys/net/if_ethersubr.c:458 #7 0xd0398db4 in if_ih_input (ifp=0xd184b030, m=0xd186d300) at /usr/src/sys/net/if.c:912 #8 0xd0398d36 in if_input_process (ifp=0xd184b030, ml=0xf1cc6d88) at /usr/src/sys/net/if.c:946 #9 0xd0a54e13 in ifiq_process (arg=0xd184b2a0) at /usr/src/sys/net/ifq.c:607 #10 0xd06be752 in taskq_thread (xtq=0xd17fd040) at /usr/src/sys/kern/kern_task.c:367 #11 0xd09af481 in proc_trampoline ()
最初のフレームを追って行ったら、やっぱり返事のパケット処理だったよ。
cksum
ちょっと骨休みする。余り飛ばし過ぎると疲れてしまいますからね。パケットを偽造する悪戯をした。その時にチェックサムを更新するって書いた。pkttoolsには、その為のアプリが用意されてる。pkt-correctだ。どんな具合になってるか確認。
/* This is compatible with FreeBSD */ icmphdr->icmp_cksum = 0; icmphdr->icmp_cksum = ~ip_checksum(icmphdr, total_size); /* Unneed htons() */
こんなコードがcorrect.cに置いてある。筆者様は、FreeBSD loveな方。で、定義は lib.cだ。
int ip_checksum(void *buffer, int size) { union { char c[2]; unsigned short s; } w; char *p; int sum = 0; for (p = buffer; size > 0; p += 2) { w.c[0] = p[0]; w.c[1] = (size > 1) ? p[1] : 0; sum += w.s; /* Unneed ntohs() */ size -= 2; } sum = (sum & 0xffff) + (sum >> 16); sum = (sum & 0xffff) + (sum >> 16); return sum; }
union構造体を使って、キャラクターの並びを数値に一発変換。ビッグエンディアンを仮定してるんで、インテルフォーマット(リトルエンディアン)を考えなくてもいいよとわざわざ注記してある。
ネットワーク上を流れるパケットは、最上位桁が先に来る(ネットワークバイトオーダー)。これは、最上位桁から見て行く事が、データの種別判定がやりやすい為である。
一方、リトルエンディアンは、多倍長演算時、最下位から演算するのに都合がよい。インテルさんは電卓の石で発展してきたんで、リトルエンディアンを採用しましたって事だ。 オイラーの頭は、電卓の石じゃ無いと、強く主張したいぞ >インテル。
UTFの文書も、問題になる。そこで文書の頭に判別用のBOMコードを入れる場合が有る。これがなければ、人に優しい、ビッグエンディアンだ。
at driver
ふと、NICの所でモニターしてみたくなった。専門用語で言うと、ボトム・ハーフとかだな。普通はもっとカーネルの上位の部分(トップ・ハーフ)を観測するんだろうけど、今回は特別だ。 NICの石の制御機構。ソースをざっと見して、知ってる名前の所にBPを置いた。
pingでも偽装パケットの送付でも、下記がヒットした。
(gdb) bt #0 em_transmit_checksum_setup (sc=0xd18ee000, mp=0xd1cb7e00, head=84, txd_upper=0xf1ca6e1c, txd_lower=0xf1ca6e18) at /usr/src/sys/dev/pci/if_em.c:2332 #1 0xd0460bf5 in em_encap (sc=0xd18ee000, m=0xd1cb7e00) at /usr/src/sys/dev/pci/if_em.c:1179 #2 0xd0460752 in em_start (ifq=0xd18ee1d8) at /usr/src/sys/dev/pci/if_em.c:645 #3 0xd0dc1489 in ifq_start_task (p=0xd18ee1d8) at /usr/src/sys/net/ifq.c:142 #4 0xd0dc127f in ifq_serialize (ifq=0xd18ee1d8, t=0xd18ee260) at /usr/src/sys/net/ifq.c:106 #5 0xd0dc1418 in ifq_run_start (ifq=0xd18ee1d8) at /usr/src/sys/net/ifq.c:79 #6 0xd0dc152e in ifq_bundle_task (p=0xd18ee1d8) at /usr/src/sys/net/ifq.c:160 #7 0xd0af67b2 in taskq_thread (xtq=0xd18a0040) at /usr/src/sys/kern/kern_task.c:367 #8 0xd0e4baf9 in proc_trampoline ()
トランポリンが起動ポイントになっているけど、これはきっとトップ・ハーフで送信準備が整って送信ってやった時に、起動してくるんだろうね。
下記は、同じ要領で受信側の処理。pingでは反応するけど、偽装のパケット(10.0.2.2 -> 10.0.2.15)では、反応が無かった。何故? ひょっとして送信即受信ってのが出来ないのかな?(なにせシュミレータですから、完全に同時は実現出来ない)
(gdb) bt #0 em_receive_checksum (sc=0xd18ee000, rx_desc=0xf1aa8250, mp=0xd3870800) at /usr/src/sys/dev/pci/if_em.c:2942 #1 0xd0464102 in em_rxeof (sc=0xd18ee000) at /usr/src/sys/dev/pci/if_em.c:2889 #2 0xd0463909 in em_intr (arg=0xd18ee000) at /usr/src/sys/dev/pci/if_em.c:964 #3 0xd0ce3f4b in intr_handler (frame=0xf1ca6e30, ih=0xd18be740) at /usr/src/sys/arch/i386/i386/machdep.c:4026 #4 0xd0e4eea4 in Xintr_ioapic0_untramp () #5 0xf1ca6e30 in ?? () #6 0xd046097c in em_start (ifq=0xd18ee1d8) at /usr/src/sys/dev/pci/if_em.c:689 #7 0xd0dc1489 in ifq_start_task (p=0xd18ee1d8) at /usr/src/sys/net/ifq.c:142 #8 0xd0dc127f in ifq_serialize (ifq=0xd18ee1d8, t=0xd18ee260) at /usr/src/sys/net/ifq.c:106 #9 0xd0dc1418 in ifq_run_start (ifq=0xd18ee1d8) at /usr/src/sys/net/ifq.c:79 #10 0xd0dc152e in ifq_bundle_task (p=0xd18ee1d8) at /usr/src/sys/net/ifq.c:160 #11 0xd0af67b2 in taskq_thread (xtq=0xd18a0040) at /usr/src/sys/kern/kern_task.c:367 #12 0xd0e4baf9 in proc_trampoline ()
again
冒頭のqemuの失敗、再度落ち着いてググる。そしたら、 Setting up Qemu with a tap interface こいうのが見つかった。これなら、意味が分かるよ。分解して図解してくれてるからね。
qemuを立ち上げる前に、ホストOS側で、下記を実行(勿論rootでね)。 br0って言うHUBを用意。そこに元々動いているNIC(ens33)を差し込む。tap0も作って差し込む。そして、IPアドレスを付与。
brctl addbr br0 # new ip addr flush dev ens33 # clear IP brctl addif br0 ens33 # insert ens33 tunctl -t tap0 -u `whoami` # new tap brctl addif br0 tap0 # insert tap ifconfig ens33 up # up ifconfig tap0 up ifconfig br0 up dhclient -v br0 # assign IP brctl show
次はqemuの起動パラメータ。勿論rootで実行。 rootだと、cu -l /dev/pts/2 みたいのが動かないので、それ関連は削除してる。それからGUIもroot権限になっちゃうんだけど、Xを飛ばそうとすると拒否される。しょうがないので、lxdeなDeskTopを立ち上げてから起動。こういう苦労はGUI嫌いゆえ発生しますね。 後は /etc/qemu-ifup とかを使わない設定になってる。
qemu-system-i386 -m 128 -s \ -netdev tap,id=mynet0,ifname=tap0,script=no,downscript=no \ -device e1000,netdev=mynet0 disk
ゲストOSのNIC(em0)に、ホストOS側と同じサブネットのIPが付与される。これがトンネル効果ってやつだ。勿論Host側からpingも普通に出来る。ゲストOS側でsshdを動かしておけば、ホストOS側からssh接続も自在だ。
使い終わったら、現状復帰するための処理をしとく。これもroot権限が必要だ。
brctl delif br0 tap0 # remove tunctl -d tap0 # delete brctl delif br0 ens33 # remove ifconfig br0 down # down brctl delbr br0 # delete ifconfig ens33 up dhclient -v ens33
brctlとtunctlって、コマンドの整合性が無いね。みんながバラバラに開発してるから、こういう事になる。頭を掻きむしりたくなるOSですよ。
それから大事な注意点が2つある。Host側のNICに無線LANのデバイスを指定出来ないって事だ。エラーになる。このせいで、パソコン丸ごとdebian(32Bit)にしてる奴は利用出来なかった。 ノートパソコンにリナを入れてる人は注意だな(諦めて、有線接続を優先しましょう)。
2番目は、sshでHostOSに接続してる場合だ。IP接続が一瞬切れる(場合によっては別IPが振られる)。ネットワーク環境をセットアップする時は、コンソールから行う、あるいはserial接続の端末を使うって事だ。
勿論、ホストOS起動時に、設定を済ませてしまえば(/etc/network/interfacesを編集)そんな苦労は不要だ。けど、qemuなんて常時使うわけでもないから、思案のしどころだ。いえね、余計な環境を背負い込むのが、オイラーは嫌いなんです。だから、GUIが嫌いって事も多分に有るな。あいつは余計なプロセスがごまんと動いていて、マニュアルもきちんと整備されてませんから。
返事は素早く
あちこち回り道をしちゃったけど、やっと本来の試験が出来る環境が整った。下記は、ホストOS側から、pingを打って確認。
ip_icmp.c/icmp_input_if
case ICMP_ECHO: if (!icmpbmcastecho && (m->m_flags & (M_MCAST | M_BCAST)) != 0) { icmpstat_inc(icps_bmcastecho); break; } => icp->icmp_type = ICMP_ECHOREPLY; goto reflect;
リクエストを受け取ったら、即返信に切り替える。後はIPアドレスの入れ替え(src,destIP)とかチェックサムの計算等をこの後行う。
(gdb) bt 4 #0 ip_send (m=0xd1996100) at /usr/src/sys/netinet/ip_input.c:1772 #1 0xd03052b6 in icmp_send (m=0xd1996100, opts=0x0) at /usr/src/sys/netinet/ip\ _icmp.c:845 #2 0xd0305f1a in icmp_input_if (ifp=0xd184b030, mp=0xf1cc648c, offp=0xf1cc6474\ , proto=1, af=2) at /usr/src/sys/netinet/ip_icmp.c:577 #3 0xd030535e in icmp_input (mp=0xf1cc648c, offp=0xf1cc6474, proto=1, af=2) at\ /usr/src/sys/netinet/ip_icmp.c:315 (More stack frames follow...)
そして、 ip_send
ていう、まんまな名前の関数に到達。送り出しパケットを、配送用の行列に登録。そして、配送依頼。
ip_send(struct mbuf *m) { mq_enqueue(&ipsend_mq, m); => task_add(net_tq(0), &ipsend_task); }
その後、 if_put()
が呼ばれて、配送ルーチンが起動する。
動作環境
まずはゲストOSであるOpenBSD側。qemu上で稼働。
vm$ ifconfig em0 em0: flags=808843<UP,BROADCAST,RUNNING,SIMPLEX,MULTICAST,AUTOCONF4> mtu 1500 lladdr 52:54:00:12:34:56 index 1 priority 0 llprio 3 groups: egress media: Ethernet autoselect (1000baseT full-duplex) status: active inet aa.bb.cc.131 netmask 0xffffff00 broadcast aa.bb.cc.255 vm$ arp -a Host Ethernet Address Netif Expire Flags aa.bb.cc.2 00:50:56:f3:2a:e1 em0 15m9s aa.bb.cc.129 00:0c:29:4d:18:2d em0 15m9s aa.bb.cc.131 52:54:00:12:34:56 em0 permanent l
そして、こちらはホストOSであるDebian(64Bit)機。VMWARE Player上で稼働
sakae@pen:~$ ip a : 2: ens33: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc pfifo_fast master br0 state UP group default qlen 1000 link/ether 00:0c:29:4d:18:2d brd ff:ff:ff:ff:ff:ff inet aa.bb.cc.129/24 brd aa.bb.cc.255 scope global dynamic ens33 valid_lft 1752sec preferred_lft 1752sec inet aa.bb.cc.130/24 brd aa.bb.cc.255 scope global secondary dynamic ens33 valid_lft 1584sec preferred_lft 1584sec 3: br0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue state UP group default qlen 1000 link/ether 00:0c:29:4d:18:2d brd ff:ff:ff:ff:ff:ff inet aa.bb.cc.129/24 brd aa.bb.cc.255 scope global dynamic br0 valid_lft 144sec preferred_lft 144sec 4: tap0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc pfifo_fast master br0 state UP group default qlen 1000 link/ether 92:a1:1d:d1:a1:bf brd ff:ff:ff:ff:ff:ff
DebianはブリッジデバイスをNICとみなしているんだな
sakae@pen:~$ arp -a ? (aa.bb.cc.131) at 52:54:00:12:34:56 [ether] on br0 ? (aa.bb.cc.1) at 00:50:56:c0:00:08 [ether] on br0 ? (aa.bb.cc.2) at 00:50:56:f3:2a:e1 [ether] on br0 ? (aa.bb.cc.254) at 00:50:56:fb:2d:fe [ether] on br0