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 はソースコード読むのが難しかったので諦めた