arp

arp

こちらPC(パトカー)3号。本部応答ねがいます。車両紹介願います。だ埼玉、ケの2323(ふさふさ)です。アグリ(ス)の、成金坊やが、17号でクラッシュです。

こういう事はいやだね。まだ居るんですかね? 車のナンバーって、陸運局で、車体番号ってか製造番号と紐づけられて、登録される。オイラーは、だ埼玉からこちらに引っ越してきた時 、真っ先に陸運局と言うDHCPサーバーに、変更依頼をしたよ。

ってなもんで、ネットワークの世界も、番号が2重構造になっている。NICの製造メーカーが付与するMACアドレス(イーサネットアドレスとも言う)。それじゃ余りにローカル過ぎて使い道が限定される。もう一つの番号は、権威ある所が発行する、世界的に通じる番号だ。IPアドレスってやつ。富豪はその番号を購入して使うけど、貧乏人はリースするのさ。

で、このMACアドレスとIPアドレスの対応が必要。まあ、車検証と思っていれば間違いない。

お前はこのIPアドレスを使えって指示されても、それをMACアドレスに変換する必要がある。なんたって、NICはIPアドレスなんて封筒の中にある番号と思って、直接はタッチしないから。

この対応を取る為のプロトコル(約束)が決まっている。

ARPパケットフォーマット

ARPプロトコル・パケットの構造

単純なやつだ。追い易い。って事で、手だししてみる。

実践 pkttools

前回知ったptktoolsを使ってARPのパケットを追ってみる。実験場はvboxに入れたOpenBSD(32Bit)。NATの環境であるから、無駄なパケットに邪魔される事の無い、静寂な世界である。

vbox# arp -a
Host                                 Ethernet Address    Netif Expire    Flags
10.0.2.2                             52:54:00:12:35:02     em0 14m41s
10.0.2.15                            08:00:27:20:e8:38     em0 permanent l

現状のテーブルを確認。10.0.2.2はGW。10.0.2.15の方は自ホストだ。

vbox$ ./pkt-recv -i em0 ETHERNET.TYPE==0x0806
LINKTYPE: 1 (Ethernet)
-- 1 --
TIME: 1624768195.956720 Sun Jun 27 13:29:55 2021
SIZE: 42/42
000000: FF FF FF FF  FF FF 08 00  27 20 E8 38  08 06 00 01 : ........ ' .8....
000010: 08 00 06 04  00 01 08 00  27 20 E8 38  0A 00 02 0F : ........ ' .8....
000020: 00 00 00 00  00 00 0A 00  02 02                    : ........ ..
==
-- 2 --
TIME: 1624768195.957108 Sun Jun 27 13:29:55 2021
SIZE: 60/60
000000: 08 00 27 20  E8 38 52 54  00 12 35 02  08 06 00 01 : ..' .8RT ..5.....
000010: 08 00 06 04  00 02 52 54  00 12 35 02  0A 00 02 02 : ......RT ..5.....
000020: 08 00 27 20  E8 38 0A 00  02 0F 00 00  00 00 00 00 : ..' .8.. ........
000030: 00 00 00 00  00 00 00 00  00 00 00 00              : ........ ....
==

arpのパケットだけを受信。ただ待っていてもパケットが流れてこない。

vbox# arp -d -a
10.0.2.2 (10.0.2.2) deleted
vbox# arp 10.0.2.2
Host                                 Ethernet Address    Netif Expire    Flags
10.0.2.2                             52:54:00:12:35:02     em0 19m48s

テーブルを削除してから、10.0.2.2の問い合わせをした。

vbox$ cat LOG | ./pkt-analyze
LINKTYPE: 1 (Ethernet)
-- 1 --
received: 42 bytes    1624767761.972572 Sun Jun 27 13:22:41 2021
Ethernet        08:00:27:20:e8:38 -> ff:ff:ff:ff:ff:ff  (type: 0x0806)
ARP     operation       : 1
        hrd/prt (size)  : 1 / 0x0800 (6 / 4)
        sender MAC/IP   : 08:00:27:20:e8:38     / 10.0.2.15
        target MAC/IP   : 00:00:00:00:00:00     / 10.0.2.2
==

受信した1番目のパケットをログに落として、それを解析。要領よく結果を表示してくれている。

vbox$ cat LOG | ./pkt-txt2txt -a
LINKTYPE: 1 (Ethernet)
-- 1 --
TIME: 1624767761.972572 Sun Jun 27 13:22:41 2021
SIZE: 42/42
000000: FF FF FF FF  FF FF 08 00  27 20 E8 38  08 06 00 01 : ........ ' .8....
000010: 08 00 06 04  00 01 08 00  27 20 E8 38  0A 00 02 0F : ........ ' .8....
000020: 00 00 00 00  00 00 0A 00  02 02                    : ........ ..
ETHERNET.DST_ADDR:      ff:ff:ff:ff:ff:ff
ETHERNET.SRC_ADDR:      08:00:27:20:e8:38
ETHERNET.TYPE:          0x806
ARP.HARDWARE:           0x1
ARP.PROTOCOL:           0x800
ARP.HARDWARE_SIZE:      0x6
ARP.PROTOCOL_SIZE:      0x4
ARP.OPERATION:          0x1
ARP.SENDER_MACADDR:     08:00:27:20:e8:38
ARP.SENDER_IPADDR:      10.0.2.15
ARP.TARGET_MACADDR:     00:00:00:00:00:00
ARP.TARGET_IPADDR:      10.0.2.2
==

こちらは、別の表現になっている。

vbox# cat LOG | ./pkt-send -i em0

そして、白眉は、テキストのログをパケットとして送出出来ちゃう事。CMにあったよね。子供達が、クレヨンで魚の絵を書くと、それが海で泳ぎ出すって言う夢みたいなのが(お台場の科学館でもやっていそう)。

この機能は、おじさんの夢でしたよ。パケットをtext-editorで編集。それをネットに放流出来るんですから。

promisc detect

この夢の機能を使って、promiscモードになってるホストを発見するってのをやってみる。以前見つけておいた方法の実践。

まずは、OpenBSD側(aa.bb.cc.128)から、Debian(aa.bb.cc.129)側がpromiscになってるか確認。

ob# tcpdump -t arp host aa.bb.cc.129
tcpdump: listening on em0, link-type EN10MB
arp who-has aa.bb.cc.129 tell aa.bb.cc.128 (00:0c:29:59:e8:ba)

返答パケットはtcpdumpでモニターする。promiscになっていない場合は、返事が無い。

ob# cat LOG129 | ./pkt-send -i em0

問い合わせはARP(ちょいと細工したパケット)パケットを送出する事で実現。

ob# tcpdump -t arp host aa.bb.cc.129
tcpdump: listening on em0, link-type EN10MB
arp who-has aa.bb.cc.129 tell aa.bb.cc.128 (00:0c:29:59:e8:ba)
arp reply aa.bb.cc.129 is-at 00:0c:29:4d:18:2d

promiscモードに設定していると、直ぐに返事が有った。当然、攻守を交代しても、きちんと機能する。

root@pen:/tmp/pkttools-1.16# cat LOG128 | ./pkt-analyze
LINKTYPE: 1 (Ethernet)
-- 1 --
received: 42 bytes    1624775815.883029 Sun Jun 27 15:36:55 2021
Ethernet        00:0c:29:4d:18:2d -> ff:ff:ff:ff:ff:fe  (type: 0x0806)
ARP     operation       : 1
        hrd/prt (size)  : 1 / 0x0800 (6 / 4)
        sender MAC/IP   : 00:0c:29:4d:18:2d     / aa.bb.cc.129
        target MAC/IP   : 00:00:00:00:00:00     / aa.bb.cc.128
==

これがOpenBSDがpromiscなってるか確認する為のプローブ・パケットだ。ブロードキャストするMACアドレスの最後が普通はFなんだけど、Eになってる。これだけの違いしか無い。このフレームは、規格外のものである。

promiscになってると、NICと言うハードなフィルターは開いた状態になってる(素通り出来ちゃう)。ソフト的なフィルターも、そこまで厳密に見ていないんで、そのままOSに実装されてるARPが反応しちゃうって訳。間隙をついている訳だな。

code of arp

arpは、やってる事が単純明快なので、ソースコードも、すっきり・さっぱりしてるに違いない。鑑賞してみるかな。で、その前に仕様書の確認だな。arp(4), arp(8), ndp(8) for IPv6

-W

     -W ether_addr [iface]
             Send the Wake on LAN frame from all interfaces on the local
             machine that are up, if iface has not been specified.  Otherwise
             the frame will be sent from iface.  ether_addr is the Ethernet
             address of the remote machine or a hostname entry in /etc/ethers.
             This option cannot be used in combination with any other option.

HISTORY
     The arp command appeared in 4.3BSD.  Wake on LAN functionality was added
     in OpenBSD 4.9.

割合新し目のコマンドなのね。そしてWoLはOpenBSDのサービス機能なの? なんて、あたかもWoLを知ってるそぶりだけど、実は知らないのさ。世界の百科事典を引いてみたよ。 Wake-on-LAN

魔法のパケットを投げると、遠隔地に置いてあるパソコンが復活するとな。ハリポタの呪文か。

send_frame(int bpf, const struct ether_addr *addr)
{
        struct {
                struct ether_header hdr;
                u_char sync[SYNC_LEN];
                u_char dest[ETHER_ADDR_LEN * DESTADDR_COUNT];
        } __packed pkt;
        u_char *p;
        int i;

        (void)memset(&pkt, 0, sizeof(pkt));
        (void)memset(&pkt.hdr.ether_dhost, 0xff, sizeof(pkt.hdr.ether_dhost));
        pkt.hdr.ether_type = htons(0);
        (void)memset(pkt.sync, 0xff, SYNC_LEN);
        for (p = pkt.dest, i = 0; i < DESTADDR_COUNT; p += ETHER_ADDR_LEN, i++)
                bcopy(addr->ether_addr_octet, p, ETHER_ADDR_LEN);
        if (write(bpf, &pkt, sizeof(pkt)) != sizeof(pkt))
                return (errno);
        return (0);
}

これが呪文発生器。 SYNC_LEN は6と定義されてる。0xFFを6回繰り返しバッファに書き込み。続いて指定された(起床させたいパソコンのMACアドレス)を、 DESTADDr_COUNT 回、バッファに追加(16回)。これでパケットが準備出来た。writeシステムコールを使って、BPFに書き込み(==パケット送出)。BPFってのは、ユーザーランド側からカーネルと通信しあうためのportだ。

この呪文は、盲腸みたいな物でOpenBSDにしか実装されていない。コードも、本来のarp.cに付け足しましたってのがよく分かるように、新しいクレジットで始まっていた。0xFFFFFFで始まる呪文なんで、arpに潜り込ませてしまえってのが、アリアリと見て取れる。

きっと、リモートに置いてあるサーバーを自宅からコントロールしたいって希望を出した人が、突っ込んでしまったものだろう。 本来このコードだけを取り出して、新しいコマンドを作るべきだろうけど(リナだと、絶対にそうするだろう。自慢気にね。)。そうしなかったのは、大人の対応ってもんです(多分、きっと)。

AF-ROUTE

他のコードもチラチラ見てる。

getsocket(void)
{
        socklen_t len = sizeof(rdomain);

        if (rtsock >= 0)
                return;
        rtsock = socket(AF_ROUTE, SOCK_RAW, 0);
        if (rtsock == -1)
                err(1, "routing socket");
        if (setsockopt(rtsock, AF_ROUTE, ROUTE_TABLEFILTER, &rdomain, len) == -1)
                err(1, "ROUTE_TABLEFILTER");
         :

こんなコードが出て来るんだけど、socketの第一引数は、初めてお目にかかるな。普通は AF_UNIX とか AF_INET ぐらいでしょう。manを見ても特に言及されていないし。ソース嫁、それもヘッダーファイルだぞ。

/*
 * Address families.
 */
#define AF_UNSPEC       0               /* unspecified */
#define AF_UNIX         1               /* local to host */
#define AF_INET         2               /* internetwork: UDP, TCP, etc. */
  :
#define AF_APPLETALK    16              /* Apple Talk */
#define AF_ROUTE        17              /* Internal Routing Protocol */
#define AF_LINK         18              /* Link layer interface */
   :

色々な用途に使えるよくばりな機構なのね。まるでUSBみたいなやっちゃな。USBはシリアルを扱うのが得意。ソケットも、そだねって言っておこう。まあ、 USBが誕生したのは「奥さんのプリンタをつなげる手間にキレたから」 USBの設計当時を振り返る だそうですから。不便が発明を生むんだな。

こんな事になってるんで、保持するデータも多岐にわたる。余り深入りしても徒労に終わりそうなんで、これぐらいにするかな。

想像するに、ISPのパケット交通整理のおじさんは、日夜おかまいなく、BGPとかで、特殊な事をやってるのでしょう。そういう人向けにコードを書きやすくする仕組みなんだろうね。

/*
 * Search the entire arp table, and do some action on matching entries.
 */
void
search(in_addr_t addr, void (*action)(struct sockaddr_dl *sdl,
    struct sockaddr_inarp *sin, struct rt_msghdr *rtm))
     :
               if (sysctl(mib, 7, buf, &needed, NULL, 0) == -1) {
                        if (errno == ENOMEM)
                                continue;
                        err(1, "actual retrieval of routing table");
                }

arp -a した時、このルーチンが呼び出される。sysctlってのは、カーネルが保持してるデータを呼び出す関数だ。こんな事をして、データをかき集めてきてる。オイラーは、単純に、hashテーブルぐらいに保持してるかと思ったんで、予想が大外れ。

まあ、頑張れば、シスコのルーター並みの事も出来るんで、大変な事になってるんだろうね。

arp reply

arpコマンドで、要求を出す方法は分かった。でも、返答パケットを処理するのはkernel様だ。一体どんな具合になってるのだろう? arpの返事ってのに注目すればいいんだな。/sys/netの下あたりに、arp.cなんてのが有るかと期待したけど、そんなの無かった。代わりに if_arp.h に、こんな定義がされていた。冒頭のWebリンクに有る図をtext表現しましたって構図(いや真実は逆だけど)だな。

struct  arphdr {
        u_int16_t ar_hrd;       /* format of hardware address */
#define ARPHRD_ETHER    1       /* ethernet hardware format */
#define ARPHRD_IEEE802  6       /* IEEE 802 hardware format */
#define ARPHRD_FRELAY   15      /* frame relay hardware format */
#define ARPHRD_IEEE1394 24      /* IEEE 1394 (FireWire) hardware format */
        u_int16_t ar_pro;       /* format of protocol address */
        u_int8_t  ar_hln;       /* length of hardware address */
        u_int8_t  ar_pln;       /* length of protocol address */
        u_int16_t ar_op;        /* one of: */
#define ARPOP_REQUEST   1       /* request to resolve address */
#define ARPOP_REPLY     2       /* response to previous request */
#define ARPOP_REVREQUEST 3      /* request protocol address given hardware */
#define ARPOP_REVREPLY  4       /* response giving protocol address */
#define ARPOP_INVREQUEST 8      /* request to identify peer */
#define ARPOP_INVREPLY  9       /* response identifying peer */
/*
 * The remaining fields are variable in size,
 * according to the sizes above.
 */
#ifdef COMMENT_ONLY
        u_int8_t  ar_sha[];     /* sender hardware address */
        u_int8_t  ar_spa[];     /* sender protocol address */
        u_int8_t  ar_tha[];     /* target hardware address */
        u_int8_t  ar_tpa[];     /* target protocol address */
#endif
};

次は、 ARPOP_REPLY のリンクを探せばいいんだな。 netinet/if_ehter.c

struct mbuf *arppullup(struct mbuf *m);
void arpinvalidate(struct rtentry *);
void arptfree(struct rtentry *);
void arptimer(void *);
struct rtentry *arplookup(struct in_addr *, int, int, unsigned int);
void in_arpinput(struct ifnet *, struct mbuf *);
void in_revarpinput(struct ifnet *, struct mbuf *);
int arpcache(struct ifnet *, struct ether_arp *, struct rtentry *);
void arpreply(struct ifnet *, struct mbuf *, struct in_addr *, uint8_t *,
    unsigned int);

いかにもってコードが有るんだけど、gdbでの網には引っかかってこないなあ。

arp -d -a して、キャッシュをクリアしてから、arp 10.0.2.2 してるんで、リクエストは出てるはずなんだけど。見てる所が違うのかな?

ちなみに、ping 10.0.2.2 とかすると、総元締めの net/if.c

Breakpoint 5, if_netisr (unused=0x0) at /usr/src/sys/net/if.c:979
979             int n, t = 0;
 :
994                     if (n & (1 << NETISR_ARP)) {
(gdb)
996                             arpintr();
(gdb) s

Breakpoint 4, arpintr () at /usr/src/sys/netinet/if_ether.c:476
476             niq_delist(&arpinq, &ml);

あっ、網にかかった。って事は、arpコマンドの処理時は経路が違うの? まあ、arpテーブルに登録が無いIPにping打つと、確実にarp(この場合は、相当の動作?)が実行されるからね。

(gdb) bt
#0  in_arpinput (ifp=0xd18ee030, m=0xd1910100) at /usr/src/sys/netinet/if_ether.c:590
#1  0xd0d48753 in arpintr () at /usr/src/sys/netinet/if_ether.c:482
#2  0xd03521f9 in if_netisr (unused=0x0) at /usr/src/sys/net/if.c:996
#3  0xd0af67b2 in taskq_thread (xtq=0xd18a0040) at /usr/src/sys/kern/kern_task.c:367
#4  0xd0e4baf9 in proc_trampoline ()

arpの受信の処理は in_arpinput で行われているな。

ob# arp 10.0.2.2
Host                                 Ethernet Address    Netif Expire    Flags
10.0.2.2                             (incomplete)          em0 5m21s
ob# ping 10.0.2.2
PING 10.0.2.2 (10.0.2.2): 56 data bytes
64 bytes from 10.0.2.2: icmp_seq=0 ttl=255 time=1568.846 ms
64 bytes from 10.0.2.2: icmp_seq=1 ttl=255 time=13.924 ms
    :
^C
--- 10.0.2.2 ping statistics ---
6 packets transmitted, 6 packets received, 0.0% packet loss
round-trip min/avg/max/std-dev = 1.734/265.076/1568.846/583.081 ms
ob# arp -a
Host                                 Ethernet Address    Netif Expire    Flags
10.0.2.2                             52:55:0a:00:02:02     em0 19m33s
10.0.2.3                             52:55:0a:00:02:03     em0 19m1s
10.0.2.15                            52:54:00:12:34:56     em0 permanent l

ping 10.0.2.2 するとetherAddressが決まるのは分かるんだけど、10.0.2.3 って何者? ご宣託をオラクル様に聴いてみたいものだ。 VirtualBox: デフォルト NIC が接続する NAT ネットワークの調整 ああ、今動いているのは、qemuだけど、まあ、同じようなものだろう。で、DNSですかい。ならば、ちょいと実験。

ob# arp -a
Host                                 Ethernet Address    Netif Expire    Flags
10.0.2.2                             (incomplete)          em0 4m2s
10.0.2.15                            52:54:00:12:34:56     em0 permanent l
ob# dig yahoo.co.jp

; <<>> DiG 9.4.2-P2 <<>> yahoo.co.jp
 :
ob# arp -a
Host                                 Ethernet Address    Netif Expire    Flags
10.0.2.2                             (incomplete)          em0 3m17s
10.0.2.3                             52:55:0a:00:02:03     em0 19m15s
10.0.2.15                            52:54:00:12:34:56     em0 permanent l

確かに、10.0.2.3が出てきたね。ダメだしで、ping -n 10.0.2.2 すれば、DNSが発動せず、10.0.2.3のarp解決(こういう呼び方でいいんだろうか)がされない事を確認したよ。

もっと端的に、/etc/resolv.confを見るだけでOk。

wakeup

上の方で取り上げた、マジックパケットの処理ってどうなってるのだろう? カーネルで処理してる? カーネルには、ヘッダーファイルは存在するけど、信号取り出し用のヘッダー・ピンが有るなんて、聞いた事ないぞ。普通に考えたら、電源が切れていればカーネルが動いている訳無いんだから、信号の出しようが無い。

すると、信号ピンが有るのは、NICなボードって事になる。emなデバイスに当たってみる。

ob$ grep wakeup if_em*
if_em_hw.c:int32_t              em_access_phy_wakeup_reg_bm(struct em_hw *, uint32_t,
if_em_hw.c:      * The 82578 Rx buffer will stall if wakeup is enabled in host and
if_em_hw.c:      * the ME.  Reading the BM_WUC register will clear the host wakeup bit.
if_em_hw.c:      * Reset the phy after disabling host wakeup to reset the Rx buffer.
if_em_hw.c: *  Read BM PHY wakeup register.  It works as such:
if_em_hw.c:em_access_phy_wakeup_reg_bm(struct em_hw *hw, uint32_t reg_addr,
if_em_hw.c:             ret_val = em_access_phy_wakeup_reg_bm(hw, reg_addr,
if_em_hw.h:/* Four wakeup IP addresses are supported */
if_em_hw.h:/* Definitions for power management and wakeup registers */
if_em_hw.h:#define E1000_WUFC_ALL_FILTERS 0x000F00FF /* Mask for all wakeup filters */

ハードな部分の担当なんですな。多分、信号ピンも立ってる事でしょう。そしてNICな石がマジックパケットを検出すると、電源ONの信号が出て、それがフォトカプラとかを経由して、電源回路の行き、電源ボタンをプチィと押したのと同じ事をする。

と言う事は、電源が切れていても、NICなボードには通電されてる必要が有るな。

マジックパケットとさも偉そうに名前を付けているけど、こんな事は携帯やスマホでは、当たり前の事だ。スマホがスタンバイ状態になってても、電話やLINEのメッセージが来れば、こちらの都合を無視して、電話ですよとか言ってくるものね。こんな悪い時代が始まったのは、ポケベル時代からだな。安眠妨害に断固抗議します。だったら、電源切っておけよ。

スマホとかだと、スタンバイになる時、sleep命令が実行されるんだろうね。wakeup割り込みを設定して、不要な回路への通電を停止。こうして、電池の消耗を抑える。

こうなっても、電波の受信回路だけは動いていて、自携帯の番号が呼び出されると、wakeup割り込み発生。これで、全体が通電状態になるとな。

こんなマジックパケットを投げてみた。

ob$ arp -W 00:0c:29:59:e8:ba

そして、pkt-recv での受信記録。

-- 42 --
TIME: 1624949738.044647 Wed Jun 30 15:55:38 2021
SIZE: 116/116
000000: FF FF FF FF  FF FF 00 0C  29 59 E8 BA  00 00 FF FF : ........ )Y......
000010: FF FF FF FF  00 0C 29 59  E8 BA 00 0C  29 59 E8 BA : ......)Y ....)Y..
000020: 00 0C 29 59  E8 BA 00 0C  29 59 E8 BA  00 0C 29 59 : ..)Y.... )Y....)Y
000030: E8 BA 00 0C  29 59 E8 BA  00 0C 29 59  E8 BA 00 0C : ....)Y.. ..)Y....
000040: 29 59 E8 BA  00 0C 29 59  E8 BA 00 0C  29 59 E8 BA : )Y....)Y ....)Y..
000050: 00 0C 29 59  E8 BA 00 0C  29 59 E8 BA  00 0C 29 59 : ..)Y.... )Y....)Y
000060: E8 BA 00 0C  29 59 E8 BA  00 0C 29 59  E8 BA 00 0C : ....)Y.. ..)Y....
000070: 29 59 E8 BA                                        : )Y..
==

このパケットをwiresharkでキャプチャしたらWoLって理解してくれるかと期待した。けど、ただのデータとしか表示されなかった。まあ、当然だわな。拡大解釈されたら恐いですよ。


This year's Index

Home