systemtap で udp の daddr, dport が 0.0.0.0 や 0 に見えることがあるのはなんでなのか
(udp だけど)tcpdump すれば当然宛先の ip address や port が見えるが、systemtap で daddr, dport を printf してみると daddr が 0.0.0.0, dport が 0 に見えることがある。
名前解決のトレースをしていて、通常名前解決をしているときは宛先ポートが 53 になので、 probe udp.sendmsg
で if (dport == 53)
としておけば名前解決に関係する udp の送信だけトレースできるのだけど、たとえば dig で名前を引いたときにはこれだと引っかからなかったことで気付いた。
実際にトレースしたいプログラムで名前解決が実行される場合は普通に dport == 53
で引っかかるので特に問題はなさそうだが、どういうケースで 0 になってしまうのか確認した。
結論から言うと、(正確にはわかっていないが)
- systemtap は socket 構造体の daddr, dportを見ているので、
- データを送信(受信)している socket が
--
connect
していればconnect
したアドレスが daddr, dport として見える --connect
していなければ daddr, dport は 0
ということのようだった。
確認した環境は
ーーー
systemtap の udp.sendmsg の probe の daddr, dport の定義を見てみる。
systemtap自体の中身を追うのはかなりしんどいはずだが、probeについて調べるなら簡単だった。tapset 以下に stp ファイルがあり、そこに定義されている。tapset/linux/udp.stpを見てみると、udp.sendmsg
はこのように定義されている。
sourceware.org Git - systemtap.git/blob - tapset/linux/udp.stp
39 probe udp.sendmsg = kernel.function("udp_sendmsg") { 40 name = "udp.sendmsg" 41 sock = $sk 42 size = $len 43 %( systemtap_v >= "2.3" %? 44 family = __ip_sock_family($sk) 45 saddr = format_ipaddr(__ip_sock_saddr($sk), __ip_sock_family($sk)) 46 daddr = format_ipaddr(__ip_sock_daddr($sk), __ip_sock_family($sk)) 47 sport = __udp_sock_sport($sk) 48 dport = __udp_sock_dport($sk) 49 %) 50 }
確認したい dport
は __udp_sock_dport($sk)
の部分。これはもっと上に定義されていて、
sourceware.org Git - systemtap.git/blob - tapset/linux/udp.stp
15 @__private30 function __udp_sock_sport (sock) { return __tcp_sock_sport (sock) } 16 @__private30 function __udp_sock_dport (sock) { return __tcp_sock_dport (sock) }
sourceware.org Git - systemtap.git/blob - tapset/linux/tcp.stp
82 /* return the TCP destination port for a given sock */ 83 function __tcp_sock_dport:long (sock:long) 84 { 85 port = @choose_defined(@inet_sock_cast(sock)->sk->__sk_common->skc_dport, # kernel >= 3.8 86 @choose_defined(@inet_sock_cast(sock)->inet_dport, # kernel >= 2.6.33 87 @choose_defined(@inet_sock_cast(sock)->dport, # kernel >= 2.6.11 88 @inet_sock_cast(sock)->inet->dport))) 89 return ntohs(port) 90 }
@hogehogeはよくわからないが、要するに skc_dport
を見てるんだなとわかる。ような気がする。
これは大体 inet_dport
と同じようなものだろう。
udp の場合、 connect
で呼ばれるのは おそらく ip4_datagram_connect
のはずで、
https://elixir.bootlin.com/linux/v4.0/source/net/ipv4/datagram.c#L23
その中でソケットに inet_daddr
や inet_dport
を設定している。
https://elixir.bootlin.com/linux/v4.0/source/net/ipv4/datagram.c#L76
int ip4_datagram_connect(struct sock *sk, struct sockaddr *uaddr, int addr_len) { struct inet_sock *inet = inet_sk(sk); struct sockaddr_in *usin = (struct sockaddr_in *) uaddr; ... inet->inet_daddr = fl4->daddr; inet->inet_dport = usin->sin_port; sk->sk_state = TCP_ESTABLISHED;
これは、 udp の send
で宛先を指定しなかった場合に宛先として利用される箇所と一致している、はず…。下記は udp_sendmsg
の該当箇所。 msg
が send
にわたす宛先などの情報で、それがない場合は else
の方に入り、 inet->inet_daddr
や inet->inet_dport
をローカル変数の daddr
や dport
に代入している。
https://elixir.bootlin.com/linux/v4.0/source/net/ipv4/udp.c#L932
int udp_sendmsg(struct kiocb *iocb, struct sock *sk, struct msghdr *msg, size_t len) { struct inet_sock *inet = inet_sk(sk); ... ... /* * Get and verify the address. */ if (msg->msg_name) { DECLARE_SOCKADDR(struct sockaddr_in *, usin, msg->msg_name); if (msg->msg_namelen < sizeof(*usin)) return -EINVAL; if (usin->sin_family != AF_INET) { if (usin->sin_family != AF_UNSPEC) return -EAFNOSUPPORT; } daddr = usin->sin_addr.s_addr; dport = usin->sin_port; if (dport == 0) return -EINVAL; } else { if (sk->sk_state != TCP_ESTABLISHED) return -EDESTADDRREQ; daddr = inet->inet_daddr; dport = inet->inet_dport; /* Open fast path for connected socket. Route will not be used, if at least one option is set. */ connected = 1; }
ということで、
- systemtap で udp.sendmsg の probe 内の daddr, dport は実際にパケットを送信した宛先が見えているのではない
- 使える変数は probe にもともと定義されている
- udp.sendmsg の場合、socket から宛先の情報を取得している。
- udpの場合、socket を
connect
してsend
のときには宛先を指定しないこともできるし、connect
せずsend
時に宛先を指定することもできる connect
した場合は socket に宛先情報が設定されているので systemtap の probe 内から参照できる。connect
していない場合は socket の宛先情報とは関係なく送信するので、 systemtap の probe 内からは宛先を正しく参照することができない。
ということのようだ。
ちなみに glibc の getaddrinfo
では connect
しているっぽくて、実際 systemtap でトレースしていても dport がちゃんと取れている。
dig ではどうかと思ったが dig はソースコード読むのが難しかったので諦めた