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と同一ネットに置いてしまおうと言う案が出てくる。ググると、混沌としたリナの世界が見えてくる。

QEMUのネットワーク設定

TAPを使ったネットワークの利用方法

ブリッジデバイスを作る。これが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が嫌いって事も多分に有るな。あいつは余計なプロセスがごまんと動いていて、マニュアルもきちんと整備されてませんから。

ハイパーバイザの作り方~ちゃんと理解する仮想化技術~ 第19回 bhyveにおける仮想NICの実装

Linux bridge、Tapインタフェースとは

返事は素早く

あちこち回り道をしちゃったけど、やっと本来の試験が出来る環境が整った。下記は、ホスト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

eBPF