2011/07/27(Wed)IPv6におけるインタフェース指定パケット送信

2011/07/27 20:41 Software::Linux
C言語でソケットプログラミングをしていて、IPv6でインタフェース指定のパケットを出したい事があったので、色々調べてみましたとさ。
RFCにもちらっと説明があるけど、あまり詳細が分からなかったのでメモ。

はじめに

1つのサーバ上で複数のネットワークとreachableなマルチホーム環境、またはリンクローカルな通信などの用途で、インタフェースを指定したパケットの送信をやりたいということがある。
IPv6は、一つのインタフェースに複数のIPv6アドレスを付与できるため、単純に出力先インタフェースの指定*1だけでなく、送信元IPアドレスも指定する必要がある。

ここでは、RFC3542 Advanced Sockets Application Program Interface (API) for IPv6で定義されているオプションを使用する。
ソケットオプションで直接setsockopt()を用いて設定する方法と、インタフェース情報を補助的データとして添えてsendmsg()で送信する方法がある。

*1 : 実際にはインタフェースインデックスを使用する

IPv6拡張ヘッダへのアクセス

RFC3542の4. Access to IPv6 and Extension Headersに説明されているが、IPv6ソケットでは、今回使用するインタフェース情報の指定以外にも、以下のような情報をカーネルに指示することが出来る。
1. The send/receive interface and source/destination address,
2. The hop limit,
3. Next hop address,
4. The traffic class,
5. Routing header,
6. Hop-by-Hop options header, and
7. Destination options header.
そして、それらの情報の渡し方は以下の2つの方法がある。
Two different mechanisms exist for sending this optional information:

1. Using setsockopt to specify the option content for a socket.
These are known "sticky" options since they affect all transmitted
packets on the socket until either a new setsockopt is done or the
options are overridden using ancillary data.

2. Using ancillary data to specify the option content for a single
datagram. This only applies to datagram and raw sockets; not to
TCP sockets.
それぞれ日本語では、付随(sticky)オプション、補助的(ancillary)データと呼べばいいかな。

IPV6_PKTINFO

そして、今回インタフェース情報の指定のために使うIPV6_PKTINFOというオプションデータが、付随(sticky)オプションや補助的(ancillary)データとして指定されていた時の評価順序が6.7. Summary of Outgoing Interface Selectionに示されている。
For a given outgoing packet on a given socket, the outgoing interface
is determined in the following order:

1. if an interface is specified in an IPV6_PKTINFO ancillary data
item, the interface is used.

2. otherwise, if an interface is specified in an IPV6_PKTINFO sticky
option, the interface is used.

3. otherwise, if the destination address is a multicast address and
the IPV6_MULTICAST_IF socket option is specified for the socket,
the interface is used.

4. otherwise, if an IPV6_NEXTHOP ancillary data item is specified,
the interface to the next hop is used.

5. otherwise, if an IPV6_NEXTHOP sticky option is specified, the
interface to the next hop is used.

6. otherwise, the outgoing interface should be determined in an
implementation dependent manner.
ちょっと説明を加えると、
  1. IPV6_PKTINFO補助的(ancillary)データ
    CMSG_DATA(cmsg)にin6_pktinfo構造体を指定。
  2. IPV6_PKTINFO付随(sticky)オプション
    setsockoptの第4引数にin6_pktinfo構造体を指定。
  3. IPV6_MULTICAST_IFオプション(マルチキャスト限定)
  4. IPV6_NEXTHOP補助的データ
  5. IPV6_NEXTHOP付随オプション
  6. 実装依存
という感じ。

IPV6_PKTINFO 補助的(ancillary)データ

例えばこんな感じ。
/**
 * @brief メッセージを送信する
 * @param[in] dstaddr 送信先アドレス
 * @param[in] dstport 送信先ポート
 * @param[in] msg 送信するメッセージ
 * @param[in] msglen メッセージ長
 * @param[in] ifindex 送信するインタフェースインデックス
 */
static int send_with_ancillary_pktinfo(const struct in6_addr *dstaddr, int dstport, const void *msg, int msglen, int ifindex){
	int soc, err;
	char ifname[IFNAMSIZ];
	struct msghdr msghdr;
	struct iovec iov;
	struct cmsghdr *cmsg;
	int cmsglen;
	struct in6_pktinfo *pinfo;
	struct sockaddr_in6 dst;
	struct in6_addr srcaddr;
	char dstaddrstr[INET6_ADDRSTRLEN], srcaddrstr[INET6_ADDRSTRLEN];

	// ソケットを開く
	if((soc = socket(AF_INET6, SOCK_DGRAM, 0)) < 0){
		perror("socket");
		return -1;
	}
	
	// 送信元アドレスをifindexから決定する
	if(if_get_global_ipv6addr(if_indextoname(ifindex, ifname), &srcaddr) != 0){
		perror("if_get_global_ipv6addr");
		goto fail;
	}
	
	printf("Sending IPv6 packet by using %s.\n", __FUNCTION__);
	printf("Source     : %s\n", inet_ntop(AF_INET6, &srcaddr, srcaddrstr, sizeof(srcaddrstr)));
	printf("Destination: %s\n", inet_ntop(AF_INET6, dstaddr, dstaddrstr, sizeof(dstaddrstr)));
	printf("Interface  : %s (%d)\n", if_indextoname(ifindex, ifname), ifindex);

	// 制御メッセージ
	cmsglen = CMSG_SPACE(sizeof(struct in6_pktinfo));
	cmsg = (struct cmsghdr *)malloc(cmsglen);
	if(cmsg == NULL){
		perror("malloc");
		goto fail;
	}

	// 宛先
	memset(&dst, 0, sizeof(struct sockaddr_in6));
	dst.sin6_family = AF_INET6;
	memcpy(&dst.sin6_addr, dstaddr, sizeof(struct in6_addr));
	dst.sin6_port = htons(dstport);
	dst.sin6_scope_id = ifindex;    // アドレスがリンクローカルスコープの場合のみ必要?

	// メッセージヘッダ
	memset(&msghdr, 0, sizeof(struct msghdr));
	msghdr.msg_control = cmsg;
	msghdr.msg_controllen = cmsglen;
	msghdr.msg_iov = &iov;
	iov.iov_base = (void *)msg;
	iov.iov_len = msglen;
	msghdr.msg_iovlen = 1;
	msghdr.msg_name = &dst;
	msghdr.msg_namelen = sizeof(dst);

	// ヘッダ
	memset(cmsg, 0, cmsglen);
	cmsg->cmsg_len = CMSG_LEN(sizeof(struct in6_pktinfo));
	cmsg->cmsg_level = IPPROTO_IPV6;
	cmsg->cmsg_type = IPV6_PKTINFO;
	pinfo = (struct in6_pktinfo *)CMSG_DATA(cmsg);

	// in6_pktinfo構造体へのデータセット
	memcpy(&pinfo->ipi6_addr, &srcaddr, sizeof(struct in6_addr));
	pinfo->ipi6_ifindex = ifindex;
	
	if((err = sendmsg(soc, &msghdr, 0)) < 0){
		perror("sendmsg");
		goto out;
	}

	goto out;

fail:
	err = -1;
out:
	free(cmsg);
	close(soc);
	return err;
}

IPV6_PKTINFO 付随(sticky)オプション

一方、setsockoptを使った場合はこんな感じ。
/**
 * @brief メッセージを送信する
 * @param[in] dstaddr 送信先アドレス
 * @param[in] dstport 送信先ポート
 * @param[in] msg 送信するメッセージ
 * @param[in] msglen メッセージ長
 * @param[in] ifindex 送信するインタフェースインデックス
 */
static int send_with_sticky_pktinfo(const struct in6_addr *dstaddr, int dstport, const void *msg, int msglen, int ifindex){
	int soc, err;
	char ifname[IFNAMSIZ];
	struct in6_pktinfo pinfo;
	struct sockaddr_in6 dst;
	struct in6_addr srcaddr;
	char dstaddrstr[INET6_ADDRSTRLEN], srcaddrstr[INET6_ADDRSTRLEN];

	// ソケットを開く
	if((soc = socket(AF_INET6, SOCK_DGRAM, 0)) < 0){
		perror("socket");
		return -1;
	}
	
	// 送信元アドレスをifindexから決定する
	if(if_get_global_ipv6addr(if_indextoname(ifindex, ifname), &srcaddr) != 0){
		perror("if_get_global_ipv6addr");
		goto fail;
	}
	
	printf("Sending IPv6 packet by using %s.\n", __FUNCTION__);
	printf("Source     : %s\n", inet_ntop(AF_INET6, &srcaddr, srcaddrstr, sizeof(srcaddrstr)));
	printf("Destination: %s\n", inet_ntop(AF_INET6, dstaddr, dstaddrstr, sizeof(dstaddrstr)));
	printf("Interface  : %s (%d)\n", if_indextoname(ifindex, ifname), ifindex);

	// 宛先
	memset(&dst, 0, sizeof(struct sockaddr_in6));
	dst.sin6_family = AF_INET6;
	memcpy(&dst.sin6_addr, dstaddr, sizeof(struct in6_addr));
	dst.sin6_port = htons(dstport);
	dst.sin6_scope_id = ifindex;    // アドレスがリンクローカルスコープの場合のみ必要?

	// in6_pktinfo構造体へのデータセット
	memcpy(&pinfo.ipi6_addr, &srcaddr, sizeof(struct in6_addr));
	pinfo.ipi6_ifindex = ifindex;
	
	// IPV6_PKTINFOを設定
	if(setsockopt(soc, IPPROTO_IPV6, IPV6_PKTINFO, &pinfo, sizeof(struct in6_pktinfo)) == -1){
		perror("setsockopt");
		goto fail;
	}

	if((err = sendto(soc, msg, msglen, 0, (struct sockaddr *)&dst, sizeof(dst))) < 0){
		perror("sendto");
		goto out;
	}

	goto out;

fail:
	err = -1;
out:
	close(soc);
	return err;
}

IPV6_2292PKTINFO

どうやら、RFC3542策定前のIPV6_PKTINFOとしてRFC2292 Advanced Sockets API for IPv6 (Obsolete)で策定されていたものも残っているらしい。
これは、補助的(ancillary)データと付随(sticky)オプションを組合わせたような方式で、PKTINFOはCMSG_DATAに格納し、setsockoptで1を設定するだけ。
多分互換性のために残っているだけなので、あまり使う機会はなさそう。
/**
 * @brief メッセージを送信する
 * @param[in] dstaddr 送信先アドレス
 * @param[in] dstport 送信先ポート
 * @param[in] msg 送信するメッセージ
 * @param[in] msglen メッセージ長
 * @param[in] ifindex 送信するインタフェースインデックス
 */
static int send_with_2292pktinfo(const struct in6_addr *dstaddr, int dstport, const void *msg, int msglen, int ifindex){
	int soc, err;
	char ifname[IFNAMSIZ];
	struct msghdr msghdr;
	struct iovec iov;
	struct cmsghdr *cmsg;
	int cmsglen;
	struct in6_pktinfo *pinfo;
	const int on = 1;
	struct sockaddr_in6 dst;
	struct in6_addr srcaddr;
	char dstaddrstr[INET6_ADDRSTRLEN], srcaddrstr[INET6_ADDRSTRLEN];

	// ソケットを開く
	if((soc = socket(AF_INET6, SOCK_DGRAM, 0)) < 0){
		perror("socket");
		return -1;
	}
	
	// 送信元アドレスをifindexから決定する
	if(if_get_global_ipv6addr(if_indextoname(ifindex, ifname), &srcaddr) != 0){
		perror("if_get_global_ipv6addr");
		goto fail;
	}
	
	printf("Sending IPv6 packet by using %s.\n", __FUNCTION__);
	printf("Source     : %s\n", inet_ntop(AF_INET6, &srcaddr, srcaddrstr, sizeof(srcaddrstr)));
	printf("Destination: %s\n", inet_ntop(AF_INET6, dstaddr, dstaddrstr, sizeof(dstaddrstr)));
	printf("Interface  : %s (%d)\n", if_indextoname(ifindex, ifname), ifindex);

	// 制御メッセージ
	cmsglen = CMSG_SPACE(sizeof(struct in6_pktinfo));
	cmsg = (struct cmsghdr *)malloc(cmsglen);
	if(cmsg == NULL){
		perror("malloc");
		goto fail;
	}

	// 宛先
	memset(&dst, 0, sizeof(struct sockaddr_in6));
	dst.sin6_family = AF_INET6;
	memcpy(&dst.sin6_addr, dstaddr, sizeof(struct in6_addr));
	dst.sin6_port = htons(dstport);
	dst.sin6_scope_id = ifindex;    // アドレスがリンクローカルスコープの場合のみ必要?

	// メッセージヘッダ
	memset(&msghdr, 0, sizeof(struct msghdr));
	msghdr.msg_control = cmsg;
	msghdr.msg_controllen = cmsglen;
	msghdr.msg_iov = &iov;
	iov.iov_base = (void *)msg;
	iov.iov_len = msglen;
	msghdr.msg_iovlen = 1;
	msghdr.msg_name = &dst;
	msghdr.msg_namelen = sizeof(dst);

	// ヘッダ
	memset(cmsg, 0, cmsglen);
	cmsg->cmsg_len = CMSG_LEN(sizeof(struct in6_pktinfo));
	cmsg->cmsg_level = IPPROTO_IPV6;
	cmsg->cmsg_type = IPV6_2292PKTINFO;
	pinfo = (struct in6_pktinfo *)CMSG_DATA(cmsg);

	// in6_pktinfo構造体へのデータセット
	memcpy(&pinfo->ipi6_addr, &srcaddr, sizeof(struct in6_addr));
	pinfo->ipi6_ifindex = ifindex;
	
	// IPV6_2292PKTINFOを設定
	if(setsockopt(soc, IPPROTO_IPV6, IPV6_2292PKTINFO, &on, sizeof(int)) == -1){
		perror("setsockopt");
		goto fail;
	}
	
	if((err = sendmsg(soc, &msghdr, 0)) < 0){
		perror("sendmsg");
		goto out;
	}

	goto out;

fail:
	err = -1;
out:
	free(cmsg);
	close(soc);
	return err;
}

サンプル

というわけで、この3種類の方法でそれぞれパケットを出力できるサンプルpktinfo.cを作ってみました。
main関数の宛先アドレスとインタフェース名を適当に変えてやると、そのホストのUDP:1234に適当なパケットを投げてくれるはず。
$ gcc -Wall -o pktinfo -D _GNU_SOURCE pktinfo.c
$ ./pktinfo
Sending IPv6 packet by using send_with_ancillary_pktinfo.
Source     : 2001:xxxx:xxxx:xxxx:xxxx:xxxx:xxxx:xxxx
Destination: 2001:xxxx:xxxx:xxxx:xxxx:xxxx:xxxx:xxxx
Interface  : eth0 (2)
----------
Sending IPv6 packet by using send_with_sticky_pktinfo.
Source     : 2001:xxxx:xxxx:xxxx:xxxx:xxxx:xxxx:xxxx
Destination: 2001:xxxx:xxxx:xxxx:xxxx:xxxx:xxxx:xxxx
Interface  : eth0 (2)
----------
Sending IPv6 packet by using send_with_2292pktinfo.
Source     : 2001:xxxx:xxxx:xxxx:xxxx:xxxx:xxxx:xxxx
Destination: 2001:xxxx:xxxx:xxxx:xxxx:xxxx:xxxx:xxxx
Interface  : eth0 (2)
----------