読者です 読者をやめる 読者になる 読者になる

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

m-masataka.hatenablog.com

前回の続き
前回はDHCPリクエストのパケットをキャプチャするところまで成功しました。
そして今回はいよいよ、DHCPリクエストに返信をしてみます。
DHCPプロトコルを勉強するにあたって以下のサイトを参考にしました。

http://www.picfun.com/lan09a.html

DHCPプロトコルについて簡単に説明すると、 以下の図のような手順でアドレスをゲットしてきます。

client              server
        discover 
     ------------> 
        offer 
     <------------ 
        request 
     ------------> 
        PACK  
     <------------ 

IP アドレスGET!!

ブロードキャストされたdiscoverメッセージに対し、DHCPサーバがofferメッセージで応答。 Clientは気に入ったDHCPサーバを選択し、Requestメッセージをブロードキャストすることで、自分がどのDHCPサーバを選択したかを全サーバに知らせる。
選択されたDHCPサーバは、IP払い出し完了の応答(PACK)を返して、完了。
という流れになっています。
一気に全部作成するのは大変なので、まずはOFFERメッセージからまともに返せるようになろうぜ!
ということで、今日はOFFERパケットを作成します。
が、基本的にパラメータはハードコードで書いてます。 しょぼい (ある意味わかりやすい!)

パケット作成

パケットを作成する前に、まずはメモリの確保を行います。 ここでも受信時同様、L2パケットを作成する必要があるので、ether_header+iphdr+udphdr+dhcp_packetの合計サイズのメモリを確保して行きます。 メモリは一度ゼロクリアしないと、何かをミスった時に訳の分からない値が出てきてしまうので、ゼロクリアも忘れずに。

        int packetsiz;
        packetsiz = sizeof(struct ether_header)+sizeof(struct ip) + sizeof(struct udphdr)+sizeof(struct dhcp_packet);
        char *rep_buf;
        if ((rep_buf = (char *)malloc(packetsiz)) == NULL){
            perror("malloc");
        }
        memset(rep_buf, 0, packetsiz);

Ether Headerの作成

はじめに、Ether Headerの作成をします。パケットのどこから埋めていくかはどうでも良いのですが、 TCP/UDPなどのチェックサムが関わるところでは、順番に気をつけなければなりません。
ざっくり説明すると、先ほど確保した領域(rep_buf)をパケットキャプチャの時と同じ要領でether_header構造体にはめ、中の値を埋めてきます。 下のプログラムでは、DHCPクライアントから受け取ったパケット(buf)からmacアドレスやether_typeを読み取って、rep_bufに当てはめています。

int set_ethernet_header(void *rep_buf, int rep_size, void *buf, int size, unsigned char *smac){
    struct ether_header *rep_eth;
    struct ether_header *eth;
    rep_eth = (struct ether_header *)rep_buf;
    eth = (struct ether_header *)buf;
    memcpy(&rep_eth->ether_dhost,&eth->ether_shost,ETH_ALEN);
    memcpy(&rep_eth->ether_shost,smac,ETH_ALEN);
    rep_eth->ether_type = eth->ether_type;
    return 0;
}

チェックサムの計算

IP Header、UDP Headerの作成をする前に、チェックサムの計算方法について説明しておきます。
調べて思ったのですが、チェックサムって概要はたくさん説明があるけど、なにすれば良いのかなかなか掴みにくい。 下記のサイトを見ると、要はチェックサム対象フィールドを固定長に区切って足すだけ。

http://www.fenix.ne.jp/~thomas/memo/ip/checksum.html

http://www.fenix.ne.jp/~thomas/memo/ip/checksum.html

やっていること。 例:IP Header

  • IP Headerを16bit毎に区切る
  • 全部を足して
  • 最後にbit反転する。

足す時に桁が上がったら、くり上がった値を再び足します。

unsigned short csum(unsigned short *ptr,int nbytes) {
    register long sum;
    sum=0;
    while(nbytes>1) {
        sum+=*ptr++;
        nbytes-=2;
    }
    if(nbytes==1) {
        sum += *(u_int8_t *)ptr;
    }
    sum = (sum>>16)+(sum & 0xffff);
    sum = (sum>>16)+(sum & 0xffff);
    return ~sum;
}

IP Headerの作成

ここもEtherHeaderと同じです。適当にパラメータを設定します。
(ttlとか意図的に変更してるしw)
注意することは、以下の点

csum()の第二引数にはIP Headerの長さであるip->ihlを入れるのですが、そのまま入れるとint で5が入ってしまいます。
これは、2バイトのフィールドを無理やりunsigned intに当てはめているからで、実際のIP Header長は20のため、今回は愚直に4を掛けています。
(こんなんでいいのか?なんだか、普通に全部をビット列で扱った方が早い気がしてきた)

int set_ip_header(void *rep_buf, struct in_addr *src, struct in_addr *dst){
    struct iphdr *ip;
    ip = (struct iphdr *)(rep_buf + sizeof(struct ether_header));
    ip->version = 4;
    ip->ihl = 5;
    ip->tos = 16;
    ip->tot_len = htons(sizeof(struct iphdr)+sizeof(struct udphdr)+sizeof(struct dhcp_packet));
    ip->id = htons(0);
    ip->frag_off = htons(0);
    ip->ttl = 0x80;
    ip->protocol = IPPROTO_UDP;
    ip->saddr = src->s_addr;
    ip->daddr = dst->s_addr;
    ip->check = 0;
    ip->check = csum ((unsigned short *) (rep_buf+sizeof(struct ether_header)), ip->ihl*4);
    return 0;
}

UDP Header作成

ここも基本的には今までと変わらないのですが、UDPチェックサム計算は少し特殊です。
チェックサムの計算に、擬似UDPヘッダと呼ばれるものを付け足す必要があります。 csumで処理する値は、UDP擬似Header + UDP Header + データ(DHCPパケット)です。

int set_udp_header(void *rep_buf, int sport, int dport,struct in_addr *src, struct in_addr *dst){
    struct udphdr *udp;
    struct iphdr *ip;
    struct pseudo_header pse;
    udp = (struct udphdr *)(rep_buf + sizeof(struct ether_header)+sizeof(struct iphdr));

    udp->uh_sport = htons(sport);
    udp->uh_dport = htons(dport);
    udp->uh_ulen = htons(sizeof(struct udphdr)+sizeof(struct dhcp_packet));
    udp->uh_sum = 0;

    pse.source_address = src->s_addr;
    pse.dest_address = dst->s_addr;
    pse.placeholder = 0;
    pse.protocol = IPPROTO_UDP;
    pse.udp_length = htons(sizeof(struct udphdr) + sizeof(struct dhcp_packet));
    char *pseudogram;
    int psize = sizeof(struct pseudo_header) + sizeof(struct udphdr) + sizeof(struct dhcp_packet);
    pseudogram = malloc(psize);
    memcpy(pseudogram , (char*) &pse , sizeof (struct pseudo_header));
    memcpy(pseudogram + sizeof(struct pseudo_header) , udp , sizeof(struct udphdr) + sizeof(struct dhcp_packet));
    udp->uh_sum = csum( (unsigned short*) pseudogram , psize);
    free(pseudogram);
    return 0;
}

DHCP Packet

いよいよ本題のDHCPパケットを作成。
まず、決まっていることとして、dhcp->xidとcookieDHCP Discoverパケットと同じ値にする必要があります。
また、OFFERパケットとACKパケットのオプションコード(dhcp->op)は2です。 OFFERパケットでは、払い出す予定のアドレスをdhcp->yiaddr(Your IP address)に、サーバのIPをdhcp->siaddr (Server IP address)に、さらに、dhcp->chaddrにはクライアントのmacアドレスを入れます。
snameやfileは全てゼロで問題ありません。
そして重要なのが、最後のオプションコード。
全体としては以下の表のようになっている。これをOptionに埋め込んでいく必要があります。

Option Option code length(byte) data(example)
DHCP Message Type 53 1 OFFER=2, ACK=5
DHCP Server Identifier 54 4 192.168.10.1
IP Address Lease Time 51 4 600
Subnet Mask 1 4 255.255.255.0
Router 3 4 192.168.10.1
Domain Name 15 any example.org
End Flag 255

Linuxdhcpサーバを立てたことがある人ならば、なんとなく設定したことある値ばかりになっています。
少し注意が必要なのが、lease_timeの部分。int型をそのままメモリにコピーしようとすると上位バイトから値が入ってしまうため、下位バイトから埋めていくようにしました。

int set_dhcp(void *rep_buf, void *buf,int dhcp_type,struct in_addr *dst, struct in_addr *siaddr, struct in_addr *subnetmask){
    struct dhcp_packet *dhcp, *org_dhcp;
    dhcp = (struct dhcp_packet *)(rep_buf + sizeof(struct ether_header)+sizeof(struct iphdr)+sizeof(struct udphdr));
    org_dhcp = (struct dhcp_packet *)(buf + sizeof(struct ether_header)+sizeof(struct iphdr)+sizeof(struct udphdr));
    int i;
    memcpy(dhcp,org_dhcp,240);
    dhcp->op = 0x02;
    dhcp->ciaddr = (uint32_t)0;
    dhcp->yiaddr = (uint32_t)(dst->s_addr);
    dhcp->siaddr_nip = (uint32_t)(siaddr->s_addr);;
    dhcp->gateway_nip = (uint32_t)0;
    uint8_t option_code[60];
    memset(option_code, 0, sizeof(option_code));
    /***set DHCP Message Type****/
    option_code[0] = 53;
    option_code[1] = 1; 
    option_code[2] = (uint8_t)dhcp_type;
    /***DHCP Server identifier***/
    option_code[3] = 54;
    option_code[4] = 4; 
    memcpy(&option_code[5],&siaddr->s_addr,option_code[4]);
    /*** IP address lease time ***/
    int lease_time = 600;
    unsigned char* cp;
    option_code[9] = 51;
    option_code[10] = 4; 
    cp = (unsigned char *)&lease_time;
    for(i=0;i<4;i++){
        option_code[14-i]=(uint8_t)(*cp++);
    }
    /*** Subnet Mask ***/
    option_code[15] = 1; 
    option_code[16] = 4; 
    memcpy(&option_code[17],&subnetmask->s_addr,option_code[16]);
    /*** Router ***/
    option_code[21] = 3; 
    option_code[22] = 4; 
    memcpy(&option_code[23],&siaddr->s_addr,option_code[22]);
    /*** domain name ***/
    char *domain="example.org"; 
    option_code[27] = 15; 
    option_code[28] = strlen(domain); 
    memcpy(&option_code[29],domain,option_code[28]);
    /**LAST**/
    option_code[29+strlen(domain)]=255;
    
    memcpy(&dhcp->options,&option_code,60);
    return 0;
}

さあ、これでDHCPメッセージの完成!

パケットの送信

パケットの送信にはsendto()を使います。送信に使うソケットは受信の際に使ったソケットディスクリプタをそのまま使ってあげれば良いです。
また、送信のプロトコルも受信時のものをそのまま受け継ぎます。
今回はDiscoveryメッセージに対してはOfferを返し、Requestメッセージに対してはACKを返す、簡単なプログラムを作っています。
でもこれで、指定したIPを振ることができるはず。

        int dhcp_mt = check_dhcp_message_type(buf,sizeof(buf));
        printf("dhcp_option %d\n",dhcp_mt);
        struct in_addr src ,dst,subnetmask;
        inet_aton("192.168.10.1",&src);
        inet_aton("192.168.10.65",&dst);
        inet_aton("255.255.255.0",&subnetmask);
        switch(dhcp_mt){
            case 1: 
                printf("OFFER\n");
                set_dhcp(rep_buf,buf,2,&dst,&src,&subnetmask);
                break;
            case 3: 
                printf("ACK\n");
                set_dhcp(rep_buf,buf,5,&dst,&src,&subnetmask);
                break;
            default: printf("This message is not relevance to dhcp protocol");  
                break;
        }
        set_ethernet_header(rep_buf, sizeof(rep_buf), buf, sizeof(buf),ifr.ifr_hwaddr.sa_data);
        int dport=68, sport=67;
        set_ip_header(rep_buf, &src,&dst);
        set_udp_header(rep_buf,sport,dport,&src,&dst);

        messagedump(buf, sizeof(buf));
        messagedump(rep_buf,sizeof(rep_buf));
        int send_len;
        if(sendto(sockfd, rep_buf, packetsiz, 0, (struct sockaddr*)&senderinfo, sizeof(senderinfo))< 0){
            perror("send sockfd");
            break;
        }

結果確認

最後に、このDHCPサーバ(っぽい何か)に向かってDHCP ClientよりDiscoverメッセージを投げてみて、ちゃんと帰ってきてるか確かめます。
結果

# ip netns exec dhcp-client dhclient -v veth_in
Internet Systems Consortium DHCP Client 4.3.3
Copyright 2004-2015 Internet Systems Consortium.
All rights reserved.
For info, please visit https://www.isc.org/software/dhcp/

Listening on LPF/veth_in/b6:9d:80:e6:ae:be
Sending on   LPF/veth_in/b6:9d:80:e6:ae:be
Sending on   Socket/fallback
DHCPDISCOVER on veth_in to 255.255.255.255 port 67 interval 3 (xid=0x30c0f26c)
DHCPREQUEST of 192.168.10.65 on veth_in to 255.255.255.255 port 67 (xid=0x6cf2c030)
DHCPOFFER of 192.168.10.65 from 192.168.10.1
DHCPACK of 192.168.10.65 from 192.168.10.1
bound to 192.168.10.65 -- renewal in 232 seconds.

# ip netns exec dhcp-client ip addr
1: lo: <LOOPBACK> mtu 65536 qdisc noop state DOWN group default qlen 1
    link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
19: veth_in@if18: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue state UP group default qlen 1000
    link/ether b6:9d:80:e6:ae:be brd ff:ff:ff:ff:ff:ff link-netnsid 0
    inet 192.168.10.65/24 brd 192.168.10.255 scope global veth_in
       valid_lft forever preferred_lft forever

ちゃんとIPを取ることができました!!
あとは、アドレスの管理とか、色々やらないといけないけど、とりあえずここまでは完成!
今回までのコードは大きくなってきたのでGithubに載せます。

github.com