dhcpサーバを作る(C言語) その1

だいぶ前ですが、Torema DayというSDN的なものを勉強する勉強会に参加してきました。 発表の中で、”OVSのDHCPが気持ち悪い”っといった内容の発表があったのですが、発表者の方と話して見ると、 「仕事に飽きたら自前のDHCPサーバを実装している」らしい……
世の中には変わった人もいるもんですね…
その人曰く、「DHCPは簡単なプロトコルだから、すぐにできますよ」ということなので、 今回はDHCPサーバを作ってみます。

が、前段としてまずはDHCPの中身を詳しく知るために、DHCPのパケットキャプチャをする機能を実装します。
勉強も兼ねてGolangで実装しようかと思いましたが、GolangのnetパッケージではL2をいじれないらしい。基本的にDHCPはraw_socketでL2まで見なきゃ行けなくなかったっけ?
(L2を操作したければCGoを使ってねってことらしいけど、それなら初めからCで書いた方が良いよね) ってことで、今回はC言語で実装します。
ただ、ソケットプログラミングってほとんどやったことないので、基本的なところから復習していきます。

ソケットの作成

まずはソケットの作成から。 ソケットはint型で定義します。
Linuxでのソケットはint型で表現されるファイルディスクリプタ
らしいです。何か入出力するときのただの出入り口って感じですね。 今回はsocket()にAF_PACKET, SOCK_RAW, htons(ETH_P_ALL)を入れます。
第一引数はLinuxのパケットの流れの中で、どこからPacketを取り出すかを示しています。 よく使うTCPUDPの操作だったら、PF_INET(L4処理まで完了したパケット)を使うことがほとんどですが、今回はデバイスドライバがL2処理をした直後のパケットが欲しいので、PF_PACKETを使います。下記のページに詳しく書いてありました。
http://enakai00.hatenablog.com/entry/20120522/1337650181
第2引数ではどんな型のソケットを使うかを指定しています。SOCK_STREAMならばコネクション型(TCP等)、SOCK_DGRAMならコネクションレス型(UDP等)、今回使用するSOCK_RAWなら生のパケット(何も処理されていない)を使いますという意味になります。
第3引数ではプロトコルを指定します。TCPならIPPROTO_TCPUDPならIPPROTO_UDP、今回は全パケットを受信するETH_P_ALLを使用します。
ちなみに、htons()はホストバイトオーダーをネットワークバイトオーダーに変換してくれる関数です。

    int sockfd = 0, len = 0;
    sockfd  = socket(AF_PACKET,SOCK_RAW, htons(ETH_P_ALL));
    if(sockfd < 0){
        perror("socket error");
        return -1;
    }

エラーハンドリングも忘れずに…

特定のインターフェースを指定

これは任意です。
インターフェースを指定しなければデフォルトでeth0とかが指定されることになります。
ここではifreq構造体を使用します。ifreqはioctlでインターフェースを設定する際に受け渡しされる構造体です。 詳しくは以下を参照
http://twhs.hatenablog.com/entry/2014/09/11/ifreq_を調べる
ioctlはアプリケーションがデバイスドライバを制御するのに必要なシステムコールらしいです。ソケットプログラミング以外でも頻繁に利用される便利なものらしいのですが、詳しくはよくわらないですね。
ここでインターフェースのインデックスを取得し(SIOCGIFINDEX)先ほど作ったソケットと紐付けています。
(インターフェースインデックスの取得はなかなか特権ユーザ(root)じゃないとできないことになっています。)

    struct ifreq ifr;
    memset(&ifr,0,sizeof(ifr));
    strcpy(ifr.ifr_name,"br0");
    if(ioctl(sockfd,SIOCGIFINDEX, &ifr) < 0){
        perror("ioctl");
        return -1;
    }

他にもsetsockopt()を使ってインターフェースを指定することもできるらしいのですが、なんか失敗したので今回はioctlにしました。

ソケットのバインド

ソケットのバインドをする前に、パケットをどのような形で受け取るか(または送信するか)を決める構造体を宣言する必要があります。
TCPでよく使われるのはsockaddr_inという構造体で、以下のように定義されているものです。

/usr/include/netinet/in.h:
   struct in_addr {
      u_int32_t s_addr;
   };

   struct sockaddr_in {
      u_char  sin_len;    
      u_char  sin_family; 
      u_short sin_port; 
      struct  in_addr sin_addr;  
      char    sin_zero[8];  
   };

ここではポートやIPアドレスが定義されています。また、sin_familyにはソケットの作成時に使用したAF_INETなどを入れることになります。
ただ、今回はパケットのL2を処理したいので、sockaddr_llという構造体を使用することにしました。 sockaddr_llでは、以下のように物理層のアドレスが定義されています。

struct sockaddr_ll {
    unsigned short sll_family;   /* 常に AF_PACKET */
    unsigned short sll_protocol; /* 物理層のプロトコル */
    int            sll_ifindex;  /* インターフェース番号 */
    unsigned short sll_hatype;   /* ARP ハードウェア種別 */
    unsigned char  sll_pkttype;  /* パケット種別 */
    unsigned char  sll_halen;    /* アドレスの長さ */
    unsigned char  sll_addr[8];  /* 物理層のアドレス */
};

構造体を定義した後は、インターフェース、プロトコルファミリ(AF_PACKET)、プロトコルを埋めて、 作成したソケットとバインドして完了です。

    struct sockaddr_ll sa;
    int sockfd_index;
    sockfd_index = ifr.ifr_ifindex;
    sa.sll_family = AF_PACKET;
    sa.sll_protocol = htons(ETH_P_ALL);
    sa.sll_ifindex = sockfd_index;
    if(bind(sockfd,(struct sockaddr *)&sa, sizeof(sa))<0){
        perror("bind");
        close(sockfd);
        return -1;
    }

これでようやくパケットが受け取れるようになりました。

パケットの受け取り

パケットの受け取りにはrecv(), recvfrom(), recvmsg()やlisten()+accept()などの組み合わせがありますが、今回はrecvfromを使います。 これらは、TCPなどのConnection-orientedな通信かUDPなどのConnectionlessな通信かで使うものを決めるようです。
よく、recv() と recvfrom()の違いが話題に上がっていますが、簡単に言えば、Connectionlessで通信相手が複数いる場合はrecvfromを、connect()とかを使ってすでに通信相手が決まっている場合はrecv()を使えば良いらしいです。

The recv() call is normally used only on a connected socket (see connect(2)) and is identical to recvfrom() with a NULL src_addr argument.

recvfromは”ソケットとデータを受け取るbuffer、bufferサイズ、フラグ、送信元情報が入る構造体、アドレスの長さ”の6つの引数をとります。 あとはbufの中にパケットが入ってくるので、適当にパケット処理の関数を書くだけです。 (例のmessagedump()はパケットの中身を表示するために自分で書いた関数なので、お気になさらず)

    struct sockaddr_ll senderinfo;
    socklen_t addrlen;
    while(1){
        addrlen=sizeof(senderinfo);
        len = recvfrom(sockfd, buf, sizeof(buf), 0,(struct sockaddr *)&senderinfo, &addrlen);
        if(len < 0 ){
            perror("receive error\n");
            break;
        }
        /*適当にパケットの処理をする*/
        messagedump(buf, sizeof(buf));
       /***************************/
    }
    close(sockfd);

結構長くなってしまったので、本日はここまでにします。 次は入ってきたパケットの表示方法を書きます(messagedump(buf,sizeof(buf))の中身について)。

以下プログラム

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <netinet/in.h>
#include <netinet/ip.h>
#include <netinet/udp.h>
#include <sys/socket.h>
#include <sys/ioctl.h>
#include <linux/if.h>
#include <net/ethernet.h>
#include <error.h>
#include <sys/types.h>
#include <arpa/inet.h>
#include <resolv.h>
#include <ifaddrs.h>
#include <fcntl.h>
#include <arpa/inet.h>
#include <netpacket/packet.h>
#include <netinet/tcp.h>
#include <inttypes.h>

/* DHCP packet */
#define EXTEND_FOR_BUGGY_SERVERS 80
#define DHCP_OPTIONS_BUFSIZE    308

/* See RFC 2131 */
struct dhcp_packet {
    uint8_t op;
    uint8_t htype;
    uint8_t hlen;
    uint8_t hops;
    uint32_t xid;
    uint16_t secs;
    uint16_t flags;
    uint32_t ciaddr;
    uint32_t yiaddr;
    uint32_t siaddr_nip;
    uint32_t gateway_nip;
    uint8_t chaddr[16];
    uint8_t sname[64];
    uint8_t file[128];
    uint32_t cookie;
    uint8_t options[DHCP_OPTIONS_BUFSIZE + EXTEND_FOR_BUGGY_SERVERS];
} __attribute__((packed));



static void messagedump(void *, int);
static void dumpdhcp(void *, int);

int main(){
    char buf[ETHER_MAX_LEN] = {0};
    int sockfd = 0, len = 0;
    sockfd  = socket(AF_PACKET,SOCK_RAW, htons(ETH_P_ALL));
    if(sockfd < 0){
        perror("socket error");
        return -1;
    }
    struct ifreq ifr;
    memset(&ifr,0,sizeof(ifr));
    strcpy(ifr.ifr_name,"br0");
    if(ioctl(sockfd,SIOCGIFINDEX, &ifr) < 0){
        perror("ioctl");
        return -1;
    }
    /*struct packet_mreq mreq;
    mreq.mr_type = PACKET_MR_PROMISC;
    mreq.mr_ifindex = ifr.ifr_ifindex;
    mreq.mr_alen = 0;
    mreq.mr_address[0] = '\0';
    if(setsockopt(sockfd,SOL_PACKET, PACKET_ADD_MEMBERSHIP, (void *)&mreq,sizeof(mreq))<0){
        perror("setopt");
        close(sockfd);
        return -1;
    }*/
    struct sockaddr_ll sa;
    int sockfd_index;
    sockfd_index = ifr.ifr_ifindex;
    sa.sll_family = AF_PACKET;
    sa.sll_protocol = htons(ETH_P_ALL);
    sa.sll_ifindex = sockfd_index;
    if(bind(sockfd,(struct sockaddr *)&sa, sizeof(sa))<0){
        perror("bind");
        close(sockfd);
        return -1;
    }
    struct sockaddr_ll senderinfo;
    socklen_t addrlen;
    while(1){
        addrlen=sizeof(senderinfo);
        len = recvfrom(sockfd, buf, sizeof(buf), 0,(struct sockaddr *)&senderinfo, &addrlen);
        if(len < 0 ){
            perror("receive error\n");
            break;
        }
        messagedump(buf, sizeof(buf));
    }
    close(sockfd);
    return 0;
}

static void messagedump(void *buf, int size){
    /*view ether_header */
    struct ether_header *eth;
    char smac[20], dmac[20];
    char sip[16], dip[20];
    int protocol;
    eth = (struct ether_header *)buf;
    sprintf(dmac, "%02x:%02x:%02x:%02x:%02x:%02x",
            eth->ether_dhost[0], eth->ether_dhost[1],
            eth->ether_dhost[2], eth->ether_dhost[3],
            eth->ether_dhost[4], eth->ether_dhost[5]);
    sprintf(smac, "%02x:%02x:%02x:%02x:%02x:%02x",
            eth->ether_shost[0], eth->ether_shost[1],
            eth->ether_shost[2], eth->ether_shost[3],
            eth->ether_shost[4], eth->ether_shost[5]);
    protocol = ntohs(eth->ether_type);
    //printf("dmac:%s\n",dmac);
    //printf("smac:%s\n",smac);
    //printf("protocol:%d\n",protocol);
    /*view ip_header */
    struct iphdr *ip;
    struct in_addr saddr, daddr;
    ip = (struct iphdr *)(buf+sizeof(struct ether_header));
    //printf("ihl: %u\n",ip->ihl);
    //printf("version: %u\n",ip->version);
    //printf("tos: %u\n",ip->tos);
    //printf("tot_len: %u\n",ntohs(ip->tot_len));
    //printf("id: %u\n",ntohs(ip->id));
    //printf("frag_off: %u\n",ip->frag_off);
    //printf("ttl: %u\n",ip->ttl);
    //printf("ip_protocol: %u\n",ip->protocol);
    //printf("check: 0x%x\n",ntohs(ip->check));
    saddr.s_addr = ip->saddr;
    daddr.s_addr = ip->daddr;
    printf("src_ip %s\n",inet_ntoa(saddr));
    printf("dest_ip %s\n",inet_ntoa(daddr));
    /*view udp_header */
    struct udphdr *udp;
    if(ip->protocol == 17){
        udp = (struct udphdr *)(buf + sizeof(struct ether_header)+sizeof(struct iphdr));
        //printf("src port: %u\n",ntohs(udp->uh_sport));
        //printf("dest port: %u\n",ntohs(udp->uh_dport));
        //printf("uh_len: %u\n",ntohs(udp->uh_ulen));
        //printf("uh_sum: %u\n",ntohs(udp->uh_sum));
        dumpdhcp(buf, sizeof(buf));
    }
}

static void dumpdhcp(void *buf, int size){
    struct dhcp_packet *dhcp;
    dhcp = (struct dhcp_packet *)(buf + sizeof(struct ether_header)+sizeof(struct iphdr)+sizeof(struct udphdr));
    char* chaddr;
    printf("--------------------DHCP--------------------\n");
    printf("opcode: %" PRIu32 "\n",dhcp->op);
    printf("hw_type: %" PRIu8 "\n",dhcp->htype);
    printf("hw_len: %" PRIu8 "\n",dhcp->hlen);
    printf("gw_hops: %" PRIu8 "\n",dhcp->hops);
    printf("tx_id: %" PRIu32 "\n",dhcp->xid);
    printf("bp_secs; %" PRIu16 "\n",dhcp->secs);
    printf("bp_flags; %" PRIu16 "\n",dhcp->flags);
    printf("CIaddr; %" PRIu32 "\n",dhcp->ciaddr);
    printf("YIaddr; %" PRIu32 "\n",dhcp->yiaddr);
    printf("SIaddr; %" PRIu32 "\n",dhcp->siaddr_nip);
    printf("GIaddr; %" PRIu32 "\n",dhcp->gateway_nip);
    int i;
    printf("chaddr: ");
    for(i=0;i<16;i++){
        printf("%" PRIu8 ",",dhcp->chaddr[i]);
    }
    printf("\n");
    printf("sname: ");
    for(i=0;i<64;i++){
        printf("%" PRIu8 ",",dhcp->sname[i]);
    }
    printf("\n");
    printf("file: ");
    for(i=0;i<128;i++){
        printf("%" PRIu8 ",",dhcp->file[i]);
    }
    printf("\n");
    printf("cookie: %" PRIu32 "\n",dhcp->cookie);
    printf("options: ");
    for(i=0;i<sizeof(dhcp->options);i++){
        printf("%" PRIu8 ",",dhcp->options[i]);
    }
    printf("\n");
    printf("--------------------DHCP--END---------------\n");
}