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.sendmsgif (dport == 53) としておけば名前解決に関係する udp の送信だけトレースできるのだけど、たとえば dig で名前を引いたときにはこれだと引っかからなかったことで気付いた。

実際にトレースしたいプログラムで名前解決が実行される場合は普通に dport == 53 で引っかかるので特に問題はなさそうだが、どういうケースで 0 になってしまうのか確認した。

結論から言うと、(正確にはわかっていないが)

  • systemtap は socket 構造体の daddr, dportを見ているので、
  • データを送信(受信)している socket が -- connect していれば connect したアドレスが daddr, dport として見える -- connect していなければ daddr, dport は 0

ということのようだった。

確認した環境は

ーーー

systemtapudp.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) }

tcpのものを見ている。 tcp.stpを見てみると

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_daddrinet_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;

これは、 udpsend で宛先を指定しなかった場合に宛先として利用される箇所と一致している、はず…。下記は udp_sendmsg の該当箇所。 msgsend にわたす宛先などの情報で、それがない場合は else の方に入り、 inet->inet_daddrinet->inet_dport をローカル変数の daddrdport に代入している。

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;
    }

ということで、

  • systemtapudp.sendmsg の probe 内の daddr, dport は実際にパケットを送信した宛先が見えているのではない
  • 使える変数は probe にもともと定義されている
  • udp.sendmsg の場合、socket から宛先の情報を取得している。
  • udpの場合、socket を connect して send のときには宛先を指定しないこともできるし、 connect せず send 時に宛先を指定することもできる
  • connect した場合は socket に宛先情報が設定されているので systemtap の probe 内から参照できる。
  • connect していない場合は socket の宛先情報とは関係なく送信するので、 systemtap の probe 内からは宛先を正しく参照することができない。

ということのようだ。

ちなみに glibcgetaddrinfo では connect しているっぽくて、実際 systemtap でトレースしていても dport がちゃんと取れている。 dig ではどうかと思ったが dig はソースコード読むのが難しかったので諦めた