memo.log

技術情報の雑なメモ

setsocopt探訪のメモ(5)

前回までは、 setsocopt 関数はOSのシステムコールを呼んでいるらしいというところまで確認した。 今回は、カーネルシステムコールの実装を覗いてみたいと思います。(確認した時のカーネルのバージョンは linux-4.19.172 kernel/git/stable/linux.git - Linux kernel stable tree

setsocopt で検索すると、以下のファイルにそれっぽい定義がありました。 linux-4.19.172/net/packet/af_packet.c

すると以下の箇所に今回焦点を当てているオプションの PACKET_ADD_MEMBERSHIP に関する処理の定義があります。

以下の copy_from_user はメモリ空間からカーネル空間に変数のメモリをコピーする関数です。 ここで (copy_from_user(&mreq, optval, len)) では文字列のポインタ optval を packet_mreq_max 構造体にコピーしています。 つまり、ここでデータを指定の構造体に入れ直していると考えて良さそうです。 したがって、 copy_from_user において文字列データとしてユーザー空間からデータを取得するため、ユーザー空間のプログラムでは構造体でも文字列データでも同じように処理が出来たと考えられます。 これで一旦最初の疑問は解消できたように思います!

static int
packet_setsockopt(struct socket *sock, int level, int optname, char __user *optval, unsigned int optlen)
{
    struct sock *sk = sock->sk;
    struct packet_sock *po = pkt_sk(sk);
    int ret;

    if (level != SOL_PACKET)
        return -ENOPROTOOPT;

    switch (optname) {
    case PACKET_ADD_MEMBERSHIP:
    case PACKET_DROP_MEMBERSHIP:
    {
        struct packet_mreq_max mreq;
        int len = optlen;
        memset(&mreq, 0, sizeof(mreq));
        if (len < sizeof(struct packet_mreq))
            return -EINVAL;
        if (len > sizeof(mreq))
            len = sizeof(mreq);
        if (copy_from_user(&mreq, optval, len))
            return -EFAULT;

amzn.to

setsocopt探訪のメモ(4)

さて、前回までで、文字列データでオプションを指定しても、mreq 構造体で指定してもシステムコールとしては同じものが実行されるところまでを確認した。 今回は、Cのコードからシステムコールが実行されるまでの流れを追ってみようと思う。

setsockopt は定義としては glibc にあるようなので、glibcの動きをgdb で追ってみる。

glibcをgdbで追う. 不具合調査等で、Linuxのシステムのライブラリをgdbで追いたいことがある。例… | by Kazuhiro Masuda | Medium

の記事を参考に、まずは、GDBの準備とsetsockopt を用いるソースコードコンパイルを行っておく。

gdbを実行し、 setsockoptブレークポイントを設定し、プログラムを実行する。

% sudo gdb ./soc_test 
(gdb) b setsockopt
(gdb) r
(gdb) n
39        setsockopt(sock, 263, 1, mreq2, sizeof(mreq2));
(gdb) n
Breakpoint 2, setsockopt () at ../sysdeps/unix/syscall-template.S:84
84      T_PSEUDO (SYSCALL_SYMBOL, SYSCALL_NAME, SYSCALL_NARGS)
(gdb) n
85              ret

すると、上記のように syscall-template.S ファイルが実行されていることが分かった。 このファイルはなんだろうと調べてみると、

SyscallWrappers - glibc wiki

によると、システムコールをラップするときに用いられるファイルらしい。

ラッパーを使用するシステムコールのリストは、syscalls.listファイルに保持されます。(日本語訳)

とのことで、手元のファイルを確認してみると、以下にそれっぽいファイルがあり、 setsocopt 関数が見える。

sysdeps/unix/sysv/linux/x86_64/syscalls.list

# File name  Caller  Syscall name    # args  Strong name Weak names

arch_prctl  EXTRA   arch_prctl  i:ii    __arch_prctl    arch_prctl
modify_ldt  EXTRA   modify_ldt  i:ipi   __modify_ldt    modify_ldt
syscall_clock_gettime   EXTRA   clock_gettime   Ei:ip       __syscall_clock_gettime


# proper socket implementations:
bind        -   bind        i:ipi   __bind      bind
getpeername -   getpeername i:ipp   __getpeername   getpeername
getsockname -   getsockname i:ipp   __getsockname   getsockname
getsockopt  -   getsockopt  i:iiiBN __getsockopt    getsockopt
listen      -   listen      i:ii    __listen    listen
setsockopt  -   setsockopt  i:iiibn __setsockopt    setsockopt
shutdown    -   shutdown    i:ii    __shutdown  shutdown
socket      -   socket      i:iii   __socket    socket
socketpair  -   socketpair  i:iiif  __socketpair    socketpair

つまり、glibc的には setsocoptシステムコールをラップしていて、右から左に流しているように見える。 ということで、次はカーネルの中に入って setsocopt を覗いてみることで、 optval 引数をどのように扱っているかを確認していく。

疑問メモ

sysdeps/unix/sysv/linux/setsockopt.c ここにそれっぽいのがあるけど、この辺のコードは経由しないのか?

カーネルをコンパイルして、1行だけデバッグログを追加する最低限の手順

前提

  • Amazon Linux2 ( amzn2-ami-hvm-2.0.20210126.0-x86_64-gp2 )
  • アップデート前のカーネルバージョン
  • 4.14.214
  • アップデート後のカーネルバージョン
    • 4.19.172

手順

必要なパッケージのインストール

# yum groupinstall "Development Tools"
# yum install ncurses ncurses-devel
# yum -y install openssl-devel

資材の準備

# wget https://cdn.kernel.org/pub/linux/kernel/v4.x/linux-4.19.172.tar.xz
# xz -dv linux-4.19.172.tar.xz
# tar xvf linux-4.19.172.tar
# cd linux-4.19.172/

1行だけデバッグログを追加してみる

# vim net/packet/af_packet.c # 
static int 
packet_setsockopt(struct socket *sock, int level, int optname, char __user *optval, unsigned int optlen)
{
+       pr_warn("my setsocopt\n");
        struct sock *sk = sock->sk;
        struct packet_sock *po = pkt_sk(sk);

ビルド

# cp /boot/config-4.14.214-160.339.amzn2.x86_64 .config
# make syncconfig
# make
# make modules_install
# make install
# reboot

使用するカーネルバージョンの指定

# awk -F\' '$1=="menuentry " {print i++ " : " $2}' /etc/grub2.cfg # リストの中から使用するバージョンのカーネルの番号を確認する
# grub2-set-default 0 # 0 の場合
# grub2-editenv list
# grub2-mkconfig -o /boot/grub2/grub.cfg
# reboot

動作確認

カーネルバージョンの確認

# uname -r # 4.19 が表示されればOK

デバッグログの確認

以下のファイルを準備

soc_test.c

#include <stdio.h>
#include <sys/socket.h> // PACKET_ADD_MEMBERSHIP
#include <net/ethernet.h>
#include <netinet/in.h> // htons
#include <linux/if_packet.h>

int main(void)
{
  int sock;
  sock = socket(AF_PACKET, SOCK_RAW, htons(ETH_P_ALL));

  struct packet_mreq mreq;
  mreq.mr_ifindex = 2; // eth0
  mreq.mr_type = PACKET_MR_PROMISC; // 1
  mreq.mr_alen = 0;
  mreq.mr_address[0] ='\0';

  setsockopt(sock, 263, 1, (void *)&mreq, sizeof(mreq));

  return 0;
}

実行・確認

# ./soc_test
# dmesg # 追加したデバッグログ (my setsocopt)が表示されればOK 

参考

qiita.com

setsocopt探訪のメモ(3)

さて、前回記事では、Cでの setsocopt 関数の optval にはRubyの同メソッドからオプションのデータを渡した際には文字列データを渡していることまでを確認した。では、Cの仕様としては指定の構造体を渡すところで、なぜ文字列でも問題ないのか、本当に問題ないのか、といったあたりを探っていこうと思う。

まずは、文字列データを渡しても本当に良いのかということを確認するため、Cで構造体を渡すときと、文字列データを渡すときとで動作の違いを確認しようと思う。setsocopt はシステムコールなので、Cの関数で実行されたら最終的にはシステムコールとして実行されるはずだ。

そこで、両者を実行し、 strace コマンドで実行されるシステムコールの内容を比べてみることとした。

まずは普通に packet_mreq 構造体で実行するパターンで用意し、 strace sudo ./soc のように strace 付きでプログラムを実行する。

  sock = socket(AF_PACKET, SOCK_RAW, htons(ETH_P_ALL));
  struct packet_mreq mreq;
  mreq.mr_ifindex = 2; // eth0
  mreq.mr_type = PACKET_MR_PROMISC; // 1
  mreq.mr_alen = 0;
  mreq.mr_address[0] ='\0';
  setsockopt(sock, SOL_PACKET, PACKET_ADD_MEMBERSHIP, mreq, sizeof(mreq));

すると、以下のシステムコールが確認できた。

setsockopt(3, SOL_PACKET, PACKET_ADD_MEMBERSHIP, {mr_ifindex=if_nametoindex("eth0"), mr_type=PACKET_MR_PROMISC, mr_alen=0, mr_address=}, 16) = 0

次に代わりに以下のような文字列データを用意する。内容、サイズとも packet_mreq 構造体で指定したものの、上の要素から順番に並べたものだ。前回記事でRubyのソース中で printf で確認した内容と同じものと言っても良い。

char mreq2[16] = {3, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0};
setsockopt(sock, SOL_PACKET, PACKET_ADD_MEMBERSHIP, mreq2, sizeof(mreq2));

strace 付きで実行すると、以下の内容が確認できた。 packet_mreq 構造体で指定した内容と全く同じであった!

setsockopt(3, SOL_PACKET, PACKET_ADD_MEMBERSHIP, {mr_ifindex=if_nametoindex("eth0"), mr_type=PACKET_MR_PROMISC, mr_alen=0, mr_address=}, 16) = 0

これにより、 setsockopt では optval に構造体で指定したときでも、文字列データで指定したときでも同じ結果が得られると確認できた。 しかし、それは任意のオプションのときにも言えることなのだろうか?( PACKET_ADD_MEMBERSHIP の時だけでは無いだろうか?) なぜ、文字列データでも問題ないのだろうか?

長くなってきたので、続きは次回に。

setsocopt探訪のメモ(2)

さて、前回ではRubyBasicSocket#setsocopt メソッドの optval になぜ文字列を渡せばいいのか、という疑問が生じるまでの背景を記した。今回は、その疑問の深堀りをさらに進めていく。

Rubyは実装はCで書かれているため、 BasicSocket#setsocopt メソッドも最終的にはCのsetsockopt 関数が呼ばれるはずだ(実際には、これはCの関数というより、システムコールの1つのようでCはそのラップをしているだけのようだ)。

まず、疑問があるのはCレベルでのsetsocopt の optval に渡される値が途中でRubyの文字列から途中で変換されて、 packet_mreq 構造体が渡されるのか、文字列が渡されるのか、ということだ。ドキュメント的には文字列と記載されているのだが、Cの方のドキュメントには構造体を渡すものとしか記載されておらず、本当に文字列が渡されるのか疑問だったので、ここをコードレベルで確認することにした。

Ruby の setsockopt メソッドは class BasicSocket - Documentation for Ruby 3.0.0click to toggle source をみてソースを確認すると、 ext/socket/basicsocket.cbsock_setsockopt 関数が実体のようだ(Ruby 3.0.0 のソースで確認)。GitHubでは https://github.com/ruby/ruby/blob/v3_0_0/ext/socket/basicsocket.c#L198 あたりに該当する。

ここのソースを見ると、241行目に setsockopt(fptr->fd, level, option, v, vlen) があり、ここで setsockopt が実行され、この時の v が optval のデータに該当するようだ。さてこの v は何かというと、少し上の方に以下のような記述があり、 RSTRING_PTR マクロは何かというと、文字列の先頭ポインタを返すマクロのようだ。

 StringValue(val);
    v = RSTRING_PTR(val);
    vlen = RSTRING_SOCKLEN(val);

ということは、 setsockopt の optval には文字列を直接渡しているということだろうか…? 実際に v の中身を展開して確認してみることとした。 泥臭いが、RubyのCコードを編集し、v を printf で出力するように setsocopt 関数を改変して、Rubyから setsocopt メソッドを実行してみる(いまいちgdbを使う方法が分からなかった)。

すると、以下の通り、 v.mr_type で参照しようとしても、コンパイルエラーとなり、中身は文字列で間違いないようだ。 内容としても、Rubyから指定した内容と合致する。

    rb_io_check_closed(fptr);
    // printf("mreq.mr_type: %d\n", v.mr_type); コンパイルエラーになる
    printf("%d\n", v[1]); // 3 or 4 
    printf("%d\n", v[1]); // 0
    printf("%d\n", v[2]); // 0
    printf("%d\n", v[3]); // 0
    printf("%d\n", v[4]); // 1
    printf("%d\n", v[5]); // 0
    printf("%d\n", v[6]); // 0
    printf("%d\n", v[7]); // 0
    printf("%d\n", v[8]); // 0
    printf("%d\n", v[9]); // 0
    printf("%d\n", v[10]); // 0
    printf("%d\n", v[11]); // 0
    printf("%d\n", v[12]); // 0
    printf("%d\n", v[13]); // 0
    printf("%d\n", v[14]); // 0
    printf("%d\n", v[15]); // 0
    printf("%d\n", vlen); // 16

    if (setsockopt(fptr->fd, level, option, v, vlen) < 0)

つまり、 setsockopt 関数のoptval には文字列を渡しているということになる。 なぜ指定の構造体では無いのに、問題なく動作しているのだろうか?

長くなってきたので、続きは次回に。

setsocopt探訪のメモ(1)

ソケットをプロミスキャスモードにするためにはPACKET_ADD_MEMBERSHIPオプションをsetsockoptで設定する必要がある。

よくあるサンプルコードだが、Cなら以下のような感じだ。

  sock = socket(AF_PACKET, SOCK_RAW, htons(ETH_P_ALL));
  struct packet_mreq mreq;
  mreq.mr_ifindex = 2; // eth0
  mreq.mr_type = PACKET_MR_PROMISC; // 1
  mreq.mr_alen = 0;
  mreq.mr_address[0] ='\0';
  setsockopt(sock, SOL_PACKET, PACKET_ADD_MEMBERSHIP, mreq2, sizeof(mreq2));

packet_mreq というのは、 PACKET_ADD_MEMBERSHIPオプションのページに説明があり、このオプションで使用する構造体だ。構造は以下の通り。この動作( mr_type )に PACKET_MR_PROMISC を設定することでプロミスキャスモードにできる。

struct packet_mreq {
    int            mr_ifindex;    /* インターフェース番号 */
    unsigned short mr_type;       /* 動作 */
    unsigned short mr_alen;       /* アドレスの長さ */
    unsigned char  mr_address[8]; /* 物理層のアドレス */
};

さて、これはRubyでも同様で、BasicSocke#setsockoptメソッドがあり、 https://github.com/kuredev/simple_bridge でもそのように実装した(ネット上の情報を参考に構成)。

simple_bridge/bridge.rb at a92bfd9f5bb1919fec3af350f310215a6b06d402 · kuredev/simple_bridge · GitHub

  PACKET_ADD_MEMBERSHIP = 0x0001 # netpacket/packet.h
....
  def promiscuous(socket, interface)
    ifreq = [interface].pack('a' + IFREQ_SIZE.to_s)
    socket.ioctl(SIOCGIFINDEX, ifreq)
    if_num = ifreq[Socket::IFNAMSIZ, IFINDEX_SIZE]
    mreq = if_num.dup
    mreq << [PACKET_MR_PROMISC].pack('s')
    mreq << ("\x00" * (PACKET_MREQ_SIZE - mreq.length))
    socket.setsockopt(SOL_PACKET, PACKET_ADD_MEMBERSHIP, mreq)
  end

さて、この setsocopt であるが、設定値が整数値や真偽値の場合は単純で良いのだが、上記のようにプロミスキャスモードを設定するとなると、低レイヤのレベルでは packet_mreq 構造体を渡すのが作法というか仕様となっている。 ところが、上記Rubyコードでは文字列を指定している。これが packet_mreq にどう対応しているのか、分からなかった。というか、なぜ、文字列で指定すると動作するのかよく分からなかった。 雰囲気的には構造体の上の要素から順番に文字列にしていけばよいのだろうが、なぜこれで動作するのか…。そもそもそれで良いのか。

たしかにRubyのドキュメントを読んでも、setsocopt の optval の値は以下のように説明されている。

# より複雑な場合
optval = IPAddr.new("224.0.0.251").hton +
         IPAddr.new(Socket::INADDR_ANY, Socket::AF_INET).hton
sock.setsockopt(Socket::IPPROTO_IP, Socket::IP_ADD_MEMBERSHIP, optval)

IP_ADD_MEMBERSHIP というオプションはこちらのページの解説等を読むと、どうやらマルチキャストに対応するためのオプションのようで本来必要な設定値は以下の構造体のようだ。

struct ip_mreq {
    struct in_addr imr_multiaddr;   /* multicast group to join */
    struct in_addr imr_interface;   /* interface to join on */
}

in_addrsockaddr_in構造体 によると、

  struct in_addr {
      u_int32_t s_addr;
   };

であり、 u_int32_tuint32_t ‐ 通信用語の基礎知識 によると、 32ビット長の無符号整数である。

IPAddr.new("224.0.0.251").hton というのはデータとしては "\xE0\x00\x00\xFB" となる。中身的には8桁の16進数となっており、16進数1文字 = 4bit なので、 8文字×4bit で32bitで長さ的にも整合する。

とはいえ、 より複雑な場合 と言われても、C言語の構造体と Ruby のIPAddr.new.htonが整合するなんてピンと来ないし、他のオプションでは他の構造体の可能性もあるので、原則的な仕様が知りたいところだ。 というか、なぜ、Rubyから文字列を渡してCの構造体として解釈される(?)のかが不明だ。 上記Rubyドキュメントでは 文字列の場合には setsockopt(2) にはその文字列と長さが渡されます。 とあり、どうやらCには文字列として渡されているようだ。

長くなってきたので、続きは次回に。

【CircleCI】1つずつ試しながら config.yml を作成していく(job, workflow, executors)

.circleci/config.yml を編集しながら試していく。

これじゃ駄目

さすがにversionだけだと駄目。 これだとジョブかワークフローを作れということでエラー。

version: 2.1

以下のエラー

# Error calling workflow: 'workflow'
# Cannot find a definition for job named build

最小構成

ジョブを1つ作る。ジョブ名は build とする必要がある。

CircleCI を設定する - CircleCI https://circleci.com/docs/ja/2.0/configuration-reference/

 Workflows を 使わない 場合は、jobs マップ内に build という名前のジョブを用意します。build ジョブは GitHub など VCS によるプッシュをトリガーとして実行する際のデフォルトのエントリーポイントとなります。あるいは、CircleCI API を利用して別のジョブを実行することも可能です。

  • 実行環境として、docker / machine / macos / windowsのどれかを選ぶ。今回は docker を選択する。
  • docker の場合は image が必須
    • steps は必須
    • その下は色々(今回は run )
  • 1つのジョブに実行環境は1つ(Executorにその縛りがある)
version: 2.1
jobs:
  build:
    docker:
      - image: circleci/ruby:2.7.2 
    steps: #
      - run: echo "hello world"

ワークフローを使う(ジョブを複数)

ワークフローを構成して、それぞれで実行環境を切り替えてみる。

version: 2.1
workflows:
  version: 2
  build:
    jobs:
      - job1
      - job2
jobs:
  job1:
    docker:
      - image: circleci/ruby:2.6.5
    steps:
      - run: echo "2.6.5"

  job2:
    docker:
      - image: circleci/ruby:2.7.2
    steps:
      - run: echo "2.7.2"

Executor

Executorを使って実行環境を切り替えてみる。

version: 2.1
executors:
  my-ruby1:
   docker:
    - image: circleci/ruby:2.6.5
  my-ruby2:
   docker:
    - image: circleci/ruby:2.7.2
workflows:
  version: 2
  build:
    jobs:
      - job1
      - job2
jobs:
  job1:
    executor: my-ruby1
    steps:
      - run: ruby -v

  job2:
    executor: my-ruby2
    steps:
      - run: ruby -v

参考

CircleCI を設定する - CircleCI