sendmsg(),recvmsg()を使ったUDP送受信プログラム
今日は突然ですが、ネットワークプログラミングの深い話です。興味ない方は読み飛ばしてください。。
UDPを受信する際に、普通にソケットから受信すると意外と困るのが、どのインターフェースから受信したのかがわからない点です。普通は1つのセグメントに1つのネットワークインターフェースで使うので気にならないことが多かったのですが、いまどきのサーバではクラスター構成にするためとか、NICの多重化などで同一セグメントに複数の仮想インターフェースを割り当てることも多くなりました。それでもTCPならコネクションの概念があるので、気にしなくても大抵大丈夫なのですが、UDPではクライアントから送信したパケットの宛先アドレスと、サーバから戻ってきた送信元アドレスが異なってしまうとまずいケースがあるものです。個別にIPアドレスをbind()しても良いのですが、柔軟性が無くなってしまうのであまりやりたくありません。
そんなときは、リンクレイヤーでイーサフレームから全て自分で管理するようなプログラムにしても良いのですが、そうすると今度はARP解決やルーティングも面倒になることがあります。それにリンクレイヤーのプログラミングはOSごとにかなり異なるので移植性を考えると面倒です。
そんなときに、recvmsg()を使えばできるらしい、ということをメンバーから聞き、実際にやってみました。
結論としてはできるのですが、インターフェースのIDとIPアドレス両方で管理しないと駄目、という感じでした。まあ、それでもプログラミングとしては便利な場面も多そうです。問題は、結構使い方が面倒なのと、情報が少ないことでしょうか。
ということで、recvmsg()を使った受信プログラムと、sendmsg()を使った送信プログラムのサンプルを作ってみたので、載せておきます。まあ、自分の備忘録という感じで・・・。
受信プログラム
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <net/if.h>
#include <arpa/inet.h>
#define BUF_LEN 512
int main(int argc,char *argv[])
{
int s,rc,port;
int one=1;
struct sockaddr_in my;
struct if_nameindex *nindex,*ptr;
if(argc<=1){
fprintf(stderr,"%s port-no\n",argv[0]);
return(-1);
}
port=atoi(argv[1]);
/* for debug */
nindex=if_nameindex();
for(ptr=nindex;ptr->if_index!=0;ptr++){
printf("%d:%s\n",ptr->if_index,ptr->if_name);
}
if_freenameindex(nindex);
/* for debug */
s=socket(PF_INET,SOCK_DGRAM,0);
if(s<0){
perror("socket");
return(1);
}
rc=setsockopt(s,IPPROTO_IP,IP_PKTINFO,&one,sizeof(one));
if(rc<0){
perror("setsockopt(IP_PKTINFO)");
return(1);
}
memset(&my,0,sizeof(my));
my.sin_family=AF_INET;
my.sin_addr.s_addr=htonl(INADDR_ANY);
my.sin_port=ntohs(port);
if(bind(s,(struct sockaddr *)&my,sizeof(my))==-1){
perror("bind");
return(-1);
}
while(1){
unsigned char buf[BUF_LEN];
struct msghdr msg;
struct iovec iov[1];
struct cmsghdr *cmsg;
char cbuf[512];
struct in_pktinfo *pktinfo;
struct sockaddr_in sin;
iov[0].iov_base=buf;
iov[0].iov_len=BUF_LEN;
memset(&msg,0,sizeof(msg));
msg.msg_name=&sin;
msg.msg_namelen=sizeof(sin);
msg.msg_iov=iov;
msg.msg_iovlen=1;
msg.msg_control=cbuf;
msg.msg_controllen=512;
rc=recvmsg(s,&msg,0);
if(rc<0){
perror("recvmsg");
continue;
}
pktinfo=NULL;
for(cmsg=CMSG_FIRSTHDR(&msg);cmsg!=NULL;
cmsg=CMSG_NXTHDR(&msg,cmsg)){
if(cmsg->cmsg_level==IPPROTO_IP&&
cmsg->cmsg_type==IP_PKTINFO){
pktinfo=(struct in_pktinfo *)CMSG_DATA(cmsg);
break;
}
}
if(pktinfo==NULL){
fprintf(stderr,"No pktinfo received.\n");
continue;
}
printf("*************************\n");
printf("buf=[%s]\n",buf);
printf("from %s:%d\n",inet_ntoa(sin.sin_addr),ntohs(sin.sin_port));
printf("pktinfo->ipi_ifindex=%d\n",pktinfo->ipi_ifindex);
printf("pktinfo->ipi_spec_dst=%s\n",inet_ntoa(pktinfo->ipi_spec_dst));
printf("pktinfo->ipi_addr=%s\n",inet_ntoa(pktinfo->ipi_addr));
printf("*************************\n");
}
close(s);
return(0);
}
struct in_pktinfoに受信インターフェースに関する情報が格納されます。
ifindex:if_nameinde()で得られるインターフェースのID番号
spec_dst:受信したIFのIPアドレス
addr:受信したパケットの宛先
という感じです。普通はspec_dstとaddrは同じになりますが、ブロードキャストの場合はaddrはブロードキャストアドレスになる、という感じです。
送信プログラム
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <net/if.h>
#include <arpa/inet.h>
#define BUF_LEN 512
int main(int argc,char *argv[])
{
int s,rc;
int one=1;
struct if_nameindex *nindex,*ptr;
struct in_addr myaddr;
char *if_name;
if(argc<=2){
fprintf(stderr,"%s if-name my-ipaddress\n",argv[0]);
return(-1);
}
if_name=argv[1];
myaddr.s_addr=inet_addr(argv[2]);
/* for debug */
nindex=if_nameindex();
for(ptr=nindex;ptr->if_index!=0;ptr++){
printf("%d:%s\n",ptr->if_index,ptr->if_name);
}
if_freenameindex(nindex);
/* for debug */
s=socket(PF_INET,SOCK_DGRAM,0);
if(s<0){
perror("socket");
return(1);
}
if(setsockopt(s,SOL_SOCKET,SO_BROADCAST,&one,sizeof(one))==-1){
perror("setsockopt");
return(-1);
}
rc=setsockopt(s,IPPROTO_IP,IP_PKTINFO,&one,sizeof(one));
if(rc<0){
perror("setsockopt(IP_PKTINFO)");
return(1);
}
printf("input \"host:port\"\n");
while(1){
unsigned char buf[BUF_LEN];
char buf2[BUF_LEN];
char *hostnm,*portnm,*ptr;
struct msghdr msg;
struct iovec iov[1];
struct cmsghdr *cmsg;
char cbuf[512];
struct in_pktinfo pktinfo;
struct sockaddr_in sin;
memset(buf,0,sizeof(buf));
fgets((char *)buf,sizeof(buf),stdin);
if(feof(stdin)){
break;
}
strcpy(buf2,(char *)buf);
if((hostnm=strtok(buf2,":"))==NULL){
fprintf(stderr,"Input-error\n");
fprintf(stderr,"host:port:nic\n");
break;
}
if((portnm=strtok(NULL,"\r\n"))==NULL){
fprintf(stderr,"Input-error\n");
fprintf(stderr,"host:port:nic\n");
break;
}
memset(&sin,0,sizeof(sin));
sin.sin_family=AF_INET;
sin.sin_addr.s_addr=inet_addr(hostnm);
sin.sin_port=ntohs(atoi(portnm));
if((ptr=strchr((char *)buf,'\n'))!=NULL){
*ptr='\0';
}
iov[0].iov_base=buf;
iov[0].iov_len=BUF_LEN;
memset(&msg,0,sizeof(msg));
msg.msg_name=&sin;
msg.msg_namelen=sizeof(sin);
msg.msg_iov=iov;
msg.msg_iovlen=1;
msg.msg_control=cbuf;
msg.msg_controllen=512;
memset(&pktinfo,0,sizeof(pktinfo));
pktinfo.ipi_ifindex=if_nametoindex(if_name);
fprintf(stderr,"ifindex=%d\n",pktinfo.ipi_ifindex);
pktinfo.ipi_spec_dst=myaddr;
// pktinfo.ipi_addr=myaddr; /* no need to */
cmsg=CMSG_FIRSTHDR(&msg);
cmsg->cmsg_level=IPPROTO_IP;
cmsg->cmsg_type=IP_PKTINFO;
cmsg->cmsg_len=CMSG_LEN(sizeof(struct in_pktinfo));
memcpy(CMSG_DATA(cmsg),&pktinfo,sizeof(pktinfo));
msg.msg_controllen=cmsg->cmsg_len;
rc=sendmsg(s,&msg,0);
if(rc<0){
perror("send");
continue;
}
}
close(s);
return(0);
}
struct in_pktinfoに送信インターフェースを指定します。ifindexにインターフェースID番号を指定すれば良いのですが、仮想IPを使っている場合はインターフェース番号だけでは区別できないので、spec_dstも指定します。addrは指定しても関係ないみたいです。
こういうものは、サンプルソースさえあれば意外と何とかなるものなので、ほとんど解説もなにもしません。。また、このソースはLinuxでしか使えません。少しの変更でBSDとかでもできるらしいです。
珍しく「プログラマー社長」っぽい記事でした・・・。
===追記===
実行結果を載せておきます。
Linuxで受信
「
eth0: fe80::221:5eff:fe46:3040/64
eth1: fe80::21b:21ff:fe35:61d0/64
1:lo
2:eth1
3:eth0
4:sit0
use IPV6_RECVPKTINFO
bind to :: 44444
*************************
buf=[fe80::221:5eff:fe46:3040 44444] <- eth0宛
from(0):fe80::21e:c2ff:feb8:9058%eth0:49382
pktinfo->ipi6_addr=fe80::221:5eff:fe46:3040
pktinfo->ipi6_ifindex=3
*************************
*************************
buf=[fe80::21b:21ff:fe35:61d0 44444] <- eth1宛
from(0):fe80::21e:c2ff:feb8:9058%eth1:55254
pktinfo->ipi6_addr=fe80::21b:21ff:fe35:61d0
pktinfo->ipi6_ifindex=2
*************************
*************************
buf=[ff02::1 44444] <- マルチキャスト宛
from(0):fe80::21e:c2ff:feb8:9058%eth0:55255
pktinfo->ipi6_addr=ff02::1
pktinfo->ipi6_ifindex=3
*************************
*************************
buf=[ff02::1 44444] <- マルチキャスト宛
from(0):fe80::21e:c2ff:feb8:9058%eth1:55255
pktinfo->ipi6_addr=ff02::1
pktinfo->ipi6_ifindex=2
*************************
」
Solarisで受信
「
bge0: fe80::209:3dff:fe13:3e3b/10
bge1: fe80::209:3dff:fe13:3e3c/10
1:lo0
2:bge0
3:bge1
use IPV6_RECVPKTINFO
bind to :: 44444
*************************
buf=[fe80::209:3dff:fe13:3e3b 44444] <- bge0宛
from(0):fe80::21e:c2ff:feb8:9058%bge0:53065
pktinfo->ipi6_addr=fe80::209:3dff:fe13:3e3b
pktinfo->ipi6_ifindex=2
*************************
*************************
buf=[fe80::209:3dff:fe13:3e3c 44444] <- bge1宛
from(0):fe80::21e:c2ff:feb8:9058%bge1:53066
pktinfo->ipi6_addr=fe80::209:3dff:fe13:3e3c
pktinfo->ipi6_ifindex=3
*************************
*************************
buf=[ff02::1 44444] <- マルチキャスト宛
from(0):fe80::21e:c2ff:feb8:9058%bge0:53067
pktinfo->ipi6_addr=ff02::1
pktinfo->ipi6_ifindex=2
*************************
」
BSD(MacOSX)で受信
「
en0: fe80::21e:c2ff:feb8:9058
en5: fe80::21d:73ff:fe68:4f3e
1:lo0
2:gif0
3:stf0
4:en0
6:en3
7:en2
5:en5
use IPV6_PKTINFO
bind to :: 44444
*************************
buf=[fe80::21e:c2ff:feb8:9058 44444] <- en0宛
from(0):fe80::221:5eff:fe46:3040%en0:54100
pktinfo->ipi6_addr=fe80::21e:c2ff:feb8:9058
pktinfo->ipi6_ifindex=4
*************************
*************************
buf=[fe80::21d:73ff:fe68:4f3e 44444] <- en5宛
from(0):fe80::221:5eff:fe46:3040%en5:37547
pktinfo->ipi6_addr=fe80::21d:73ff:fe68:4f3e
pktinfo->ipi6_ifindex=5
*************************
*************************
buf=[ff02::1 44444] <- マルチキャスト宛
from(0):fe80::221:5eff:fe46:3040%en5:47019
pktinfo->ipi6_addr=ff02::1
pktinfo->ipi6_ifindex=5
*************************
*************************
buf=[ff02::1 44444] <- マルチキャスト宛
from(0):fe80::221:5eff:fe46:3040%en0:47019
pktinfo->ipi6_addr=ff02::1
pktinfo->ipi6_ifindex=4
*************************
」
送信の動き
「
en0: fe80::21e:c2ff:feb8:9058
en5: fe80::21d:73ff:fe68:4f3e
1:lo0
2:gif0
3:stf0
4:en0
6:en3
7:en2
5:en5
use IPV6_PKTINFO
bind to :: 44444
*************************
buf=[fe80::21e:c2ff:feb8:9058 44444] <- en0宛
from(0):fe80::221:5eff:fe46:3040%en0:54100
pktinfo->ipi6_addr=fe80::21e:c2ff:feb8:9058
pktinfo->ipi6_ifindex=4
*************************
*************************
buf=[fe80::21d:73ff:fe68:4f3e 44444] <- en5宛
from(0):fe80::221:5eff:fe46:3040%en5:37547
pktinfo->ipi6_addr=fe80::21d:73ff:fe68:4f3e
pktinfo->ipi6_ifindex=5
*************************
*************************
buf=[ff02::1 44444] <- マルチキャスト宛
from(0):fe80::221:5eff:fe46:3040%en5:47019
pktinfo->ipi6_addr=ff02::1
pktinfo->ipi6_ifindex=5
*************************
*************************
buf=[ff02::1 44444] <- マルチキャスト宛
from(0):fe80::221:5eff:fe46:3040%en0:47019
pktinfo->ipi6_addr=ff02::1
pktinfo->ipi6_ifindex=4
*************************
komata-MacBook-Air:SendRecvMsgTest komata$ send6.txt
-bash: send6.txt: command not found
komata-MacBook-Air:SendRecvMsgTest komata$ cat send6.txt
/// from linux ///
//// ./send6 eth0 fe80::221:5eff:fe46:3040
*************************
buf=[fe80::21e:c2ff:feb8:9058 44444]
from(0):fe80::221:5eff:fe46:3040%en0:52108 <- eth0からen0宛に届いている
pktinfo->ipi6_addr=fe80::21e:c2ff:feb8:9058
pktinfo->ipi6_ifindex=4
*************************
*************************
buf=[ff02::1 44444] <- マルチキャスト宛
from(0):fe80::221:5eff:fe46:3040%en5:35483 <- eth0からen5宛に届いている
pktinfo->ipi6_addr=ff02::1
pktinfo->ipi6_ifindex=5
*************************
*************************
buf=[ff02::1 44444]
from(0):fe80::221:5eff:fe46:3040%en0:35483 <- 同時にen0宛にも届いている
pktinfo->ipi6_addr=ff02::1
pktinfo->ipi6_ifindex=4
*************************
//// ./send6 eth1 fe80::21b:21ff:fe35:61d0
*************************
buf=[fe80::21e:c2ff:feb8:9058 44444]
from(0):fe80::21b:21ff:fe35:61d0%en0:56278 <- eth1からen0宛に届いている
pktinfo->ipi6_addr=fe80::21e:c2ff:feb8:9058
pktinfo->ipi6_ifindex=4
*************************
*************************
buf=[ff02::1 44444] <- マルチキャスト宛
from(0):fe80::21b:21ff:fe35:61d0%en5:51383 <- eth1からen5宛に届いている
pktinfo->ipi6_addr=ff02::1
pktinfo->ipi6_ifindex=5
*************************
*************************
buf=[ff02::1 44444]
from(0):fe80::21b:21ff:fe35:61d0%en0:51383 <- 同時にen0宛にも届いている
pktinfo->ipi6_addr=ff02::1
pktinfo->ipi6_ifindex=4
*************************
/// from solaris ///
//// ./send6 bge0 fe80::209:3dff:fe13:3e3b
*************************
buf=[fe80::21e:c2ff:feb8:9058 44444]
from(0):fe80::209:3dff:fe13:3e3b%en0:42806 <- bge0からen0宛に届いている
pktinfo->ipi6_addr=fe80::21e:c2ff:feb8:9058
pktinfo->ipi6_ifindex=4
*************************
*************************
buf=[ff02::1 44444] <- マルチキャスト宛
from(0):fe80::209:3dff:fe13:3e3b%en5:42841 <- bge0からen5宛に届いている
pktinfo->ipi6_addr=ff02::1
pktinfo->ipi6_ifindex=5
*************************
*************************
buf=[ff02::1 44444]
from(0):fe80::209:3dff:fe13:3e3b%en0:42841 <- 同時にen0宛にも届いている
pktinfo->ipi6_addr=ff02::1
pktinfo->ipi6_ifindex=4
*************************
//// ./send6 bge1 fe80::209:3dff:fe13:3e3c
*************************
buf=[fe80::21e:c2ff:feb8:9058 44444]
from(0):fe80::209:3dff:fe13:3e3c%en0:42809 <- bge1からen0宛に届いている
pktinfo->ipi6_addr=fe80::21e:c2ff:feb8:9058
pktinfo->ipi6_ifindex=4
*************************
*************************
buf=[ff02::1 44444] <- マルチキャスト宛
from(0):fe80::209:3dff:fe13:3e3c%en5:42842 <- bge1からen5宛に届いている
pktinfo->ipi6_addr=ff02::1
pktinfo->ipi6_ifindex=5
*************************
*************************
buf=[ff02::1 44444]
from(0):fe80::209:3dff:fe13:3e3c%en0:42842 <- 同時にen0宛にも届いている
pktinfo->ipi6_addr=ff02::1
pktinfo->ipi6_ifindex=4
*************************
/// BSD ///
//// ./send6 en0 fe80::21e:c2ff:feb8:9058
*************************
buf=[fe80::21e:c2ff:feb8:9058 44444]
from(0):fe80::21e:c2ff:feb8:9058%en0:51278 <- en0からen0宛に届いている
pktinfo->ipi6_addr=fe80::21e:c2ff:feb8:9058
pktinfo->ipi6_ifindex=4
*************************
bind to :: 44444
*************************
buf=[ff02::1 44444] <- マルチキャスト宛
from(0):fe80::21e:c2ff:feb8:9058%en0:62491 <- en0からen0宛に届いている
pktinfo->ipi6_addr=ff02::1
pktinfo->ipi6_ifindex=4
*************************
*************************
buf=[ff02::1 44444]
from(0):fe80::21e:c2ff:feb8:9058%en5:62491 <- 同時にen5宛にも届いている
pktinfo->ipi6_addr=ff02::1
pktinfo->ipi6_ifindex=5
*************************
//// ./send6 en5 fe80::21d:73ff:fe68:4f3e
*************************
buf=[fe80::21e:c2ff:feb8:9058 44444]
from(0):fe80::21d:73ff:fe68:4f3e%en0:51279 <- en5からen0宛に届いている
pktinfo->ipi6_addr=fe80::21e:c2ff:feb8:9058
pktinfo->ipi6_ifindex=4
*************************
*************************
buf=[ff02::1 44444] <- マルチキャスト宛
from(0):fe80::21d:73ff:fe68:4f3e%en5:62492 <- en5からen5宛に届いている
pktinfo->ipi6_addr=ff02::1
pktinfo->ipi6_ifindex=5
*************************
*************************
buf=[ff02::1 44444]
from(0):fe80::21d:73ff:fe68:4f3e%en0:62492 <- 同時にen0宛にも届いている
pktinfo->ipi6_addr=ff02::1
pktinfo->ipi6_ifindex=4
*************************
」
IPv4に比べるとOSによる違いは少ないようです。Solaridでマルチキャスト宛が一つしか届かないのは、IPMPになっているためかも知れません。