[Linux] setsockopt (SO_BINDTODEVICE) の Kernel Codeをちょっと読んでみた話

ソケットを特定のNetwork Interfaceに紐づける socket option SO_BINDTODEVICE の動きについて調べる機会がありました. それについて書きます.

注) 2022.5.2 追記、修正あり.

setsockopt (SO_BINDTODEVICE)

Linux man ページによると,

SO_BINDTODEVICEこのソケットを、引数で渡したインターフェース名で指定される (“eth0” のような) 特定のデバイスにバインドする。 名前が空文字列だったり、オプションの長さ (optlen) が 0 の場合には、 ソケットのバインドが削除される。 渡すオプションは、インターフェース名が 入ったヌル文字で終端された可変長の文字列である。 文字列の最大のサイズは IFNAMSIX である。
ソケットがインターフェースにバインドされると、 その特定のインターフェースから受信されたパケットだけを処理する。 このオプションはいくつかのソケットタイプ、 特に AF_INET に対してのみ動作する点に注意すること。 パケットソケットではサポートされていない (通常の bind(2) を使うこと)。

SO_BINDTODEVICEオプションの存在は以前から知っていましたが、具体的な動作が良くわかっていなかったので調べることにしました.

設定例

SO_BINDTODEVICE オプション設定のコード例です.

#include <stdio.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <string.h>

#define DEVICE "eth0"

int main()
{
  int sock, st;
  
  sock = socket (AF_INET, SOCK_DGRAM, 0);
  if (sock < 0)
    {
      perror ("socket");
      return -1;
    }
  
  st = setsockopt (sock, SOL_SOCKET, SO_BINDTODEVICE, DEVICE, strlen (DEVICE));
  if (st < 0)
    {
      perror ("setsockopt (SO_BINDTODEVICE)");
      return -1;
    }
  return 0;
}

setsockopt() の第4引数にはbindしたいNetwork Interface名、第5引数はそのInterface名の文字列長を渡します.

setsockopt (SO_BINDTODEVICE) は Kernel 内で何が行われる?

setsockopt (SO_BINDTODEVICE)が実行されたときに Linux Kernel 内ではどのような処理がされるのか? Kernel Code を読んでみました.

参照した kernel は Version 5.12-rc5.

setsockopt() を実行すると net/core/sock.c の中の sock_setsockopt() に飛ぶようです.

/*
 *	This is meant for all protocols to use and covers goings on
 *	at the socket level. Everything here is generic.
 */

int sock_setsockopt(struct socket *sock, int level, int optname,
		    sockptr_t optval, unsigned int optlen)
{
	struct sock_txtime sk_txtime;
	struct sock *sk = sock->sk;
	int val;
	int valbool;
	struct linger ling;
	int ret = 0;

	/*
	 *	Options without arguments
	 */

	if (optname == SO_BINDTODEVICE)
		return sock_setbindtodevice(sk, optval, optlen);

setsockopt() に渡された option が SO_BINDTODEVICE の場合、sock_setbindtodevice() に飛びます.

static int sock_setbindtodevice(struct sock *sk, sockptr_t optval, int optlen)
{
	int ret = -ENOPROTOOPT;
#ifdef CONFIG_NETDEVICES
	struct net *net = sock_net(sk);
	char devname[IFNAMSIZ];
	int index;

	ret = -EINVAL;
	if (optlen < 0)
		goto out;

	/* Bind this socket to a particular device like "eth0",
	 * as specified in the passed interface name. If the
	 * name is "" or the option length is zero the socket
	 * is not bound.
	 */
	if (optlen > IFNAMSIZ - 1)
		optlen = IFNAMSIZ - 1;
	memset(devname, 0, sizeof(devname));

	ret = -EFAULT;
	if (copy_from_sockptr(devname, optval, optlen))
		goto out;

	index = 0;
	if (devname[0] != '\0') {
		struct net_device *dev;

		rcu_read_lock();
		dev = dev_get_by_name_rcu(net, devname);
		if (dev)
			index = dev->ifindex;
		rcu_read_unlock();
		ret = -ENODEV;
		if (!dev)
			goto out;
	}
	return sock_bindtoindex(sk, index, true);
out:
#endif
	return ret;
}

ここでは指定したデバイス名(Network Interface名)の長さをチェックし, 問題なければ dev_get_by_name_rcu() にてデバイス名からインタフェースのindex番号に変換します. (31行目)

struct net *net を渡していることから, network namespace 毎に閉じた世界でのデバイス名からindex番号に変換されていることに注意が必要です.

正常にindex番号に変換出来たら, その index 番号とsocketを紐づける sock_bindtoindex()を呼び出します.

static int sock_bindtoindex_locked(struct sock *sk, int ifindex)
{
	int ret = -ENOPROTOOPT;
#ifdef CONFIG_NETDEVICES
	struct net *net = sock_net(sk);

	/* Sorry... */
	ret = -EPERM;
	if (sk->sk_bound_dev_if && !ns_capable(net->user_ns, CAP_NET_RAW))
		goto out;

	ret = -EINVAL;
	if (ifindex < 0)
		goto out;

	sk->sk_bound_dev_if = ifindex;
	if (sk->sk_prot->rehash)
		sk->sk_prot->rehash(sk);
	sk_dst_reset(sk);

	ret = 0;

out:
#endif

	return ret;
}

まず、この SO_BINDTODEVICE の設定を行う際, 実行する人の Capability をチェックされます. (9行目)

ここでは CAP_NET_RAW capabilityが設定されている必要があり, それを設定しないで先のサンプルプログラムを実行すると, 以下のようなエラーとなります.

setsockopt (SO_BINDTODEVICE): Operation not permitted

capability のチェックにパスすれば, ソケット構造体 struct sock の sk_bound_dev_if に指定された inndex値が格納される、という流れとなります.

sk_bound_dev_if のはたらき

正直なところそれ以降の sk_bound_dev_if の動作についてまだ追い切れてないのですが,

  • setsockopt (SO_BINDTODEVICE) を設定しなければ, sk_bound_dev_if の値は 0 (初期値)
  • sk_bound_dev_if の値が 0 ならば routing table 等の働きで最終的な出力先、すなわち Network Interface が適切に決められる.
  • sk_bound_dev_if の値が 0 以外であれば、その値が尊重され routing table は参照されず出力先の Network Interface は指定されたモノが選ばれる.

というのがコード読みと動きから推測できました.

おや、ここまで読むと, 複数 Network Interface があって、それぞれネットワークに接続されているホストでは setsockopt (SO_BINDTODEVICE) してしまえば Routing Tableの設定などいらないのでは? と思ってしまう方、いるかもしれません.

私もその一人でした.

Routing Table の設定は不要なのか?

前述したコードの socket を使って Network Interface eth0 を Default Route への経路に設定してインターネット上のホスト(例として 8.8.8.8)と通信することは可能でした.

ところが, Default Route の設定を解除した際, インターネット上の所望のホスト 8.8.8.8 にパケットが届きません.

その理由は以下の通り.

  • eth0 を Default Route への経路を eth0 に bind したソケットを使って外部ホストに通信した場合, MAC層の宛先は Default Route に設定され、Router は IP 層の宛先アドレス 8.8.8.8 を見て所望の外部ホストにパケットが届く.
  • Default Route の設定を解除、あるいは Default Router への経路を別の Interface に設定した場合, eth0 に bind したソケットは Routing Table を参照しないで eth0 から出力するため, Router の存在を知らない.
  • eth0 に bind したソケットは, eth0 に紐づいたRouting Table を参照する.
  • そのため, eth0 に紐づいた Default Route が見つからない場合, 8.8.8.8 にRouter を介さないで直接送信しようとする. eth0 からは 8.8.8.8 の ARP query を送出.
  • 8.8.8.8 の ARP 解決がするはずがなく, eth0 からは IP パケットは出て行かない.

ということで setsockopt (SO_BINDTODEVICE) は特定の目的, たとえば DHCP server/client など Routing によってパケット送受信の Interface が変わってしまうとマズイ場合の特殊用途に使われるんだな、と一旦は理解した次第です.

ただ、複数のインタフェースが存在するシステムで, それぞのインタフェースに紐づいた default route をメトリック値をおのおの違う値で設定した場合はこの限りではなく、所望のインタフェースから外部ネットワークに通信することが可能です.

…というかまだまだ Kernel code の世界は奥深い…

では.

“[Linux] setsockopt (SO_BINDTODEVICE) の Kernel Codeをちょっと読んでみた話” への1件の返信

コメントは受け付けていません。