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

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

m-masataka.hatenablog.com

前回の続きで、今回はRAW_SOCKETから受け取ったパケットを表示する方法について書きます。 目標のDHCPサーバの作成まではまだまだ程遠いですが、あしからず…
まず、生のパケットの処理から。
パケットは例えばethernet→IP→TCP→…のように順々にデータが格納されています。 ですので、パケットの皮を剥がしていくイメージで、ヘッダーを読み込んでいく必要があります。
ひとまず今回はdhcpの内容が見れる部分にたどり着くまでやります。

ethernet header

まず、一番外側にあるethernet headerを見ていきます。 bufferをバイナリのまま見ても良いのですが、linuxで定義されている構造体に当てはめていくとWiresharkで見るような値の表示をすることができます。 ethernet headerは以下のような構造体で定義されています。
linux/if_ehter.h

 struct ethhdr
 {
    unsigned char   h_dest[ETH_ALEN];   /* destination eth addr */
    unsigned char   h_source[ETH_ALEN]; /* source ether addr    */
    unsigned short  h_proto;        /* packet type ID field */
 };

下記のプログラムのように
eth = (struct eher_header *) buf;
としてあげれば、構造体にパケットがハマりますので、後は表示をしてあげるだけです。 ただ、パケットの内容はintとかstringとかいつも使ってるやつとは型が違うので、ntohs()やinet_ntoa()を使用して見やすい文字列に変換してあげる必要があります。

    /*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);

IP header

IPヘッダーも要領は同じです。 注意したいのは、(struct iphdr )bufとしてしまうと、先頭にあるether headerを読み込んでしまうので、
ip =(struct iphdr
)(buf + sizeof(struct ether_header));
としてあげる必要があります。 IP headerの構造体は以下のように定義されています。
netinet/ip.h

 struct iphdr
   {
 #if __BYTE_ORDER == __LITTLE_ENDIAN
     unsigned int ihl:4;
     unsigned int version:4;
 #elif __BYTE_ORDER == __BIG_ENDIAN
     unsigned int version:4;
     unsigned int ihl:4;
 #else
 # error "Please fix <bits/endian.h>"
 #endif
     u_int8_t tos;
     u_int16_t tot_len;
     u_int16_t id;
     u_int16_t frag_off;
     u_int8_t ttl;
     u_int8_t protocol;
     u_int16_t check;
     u_int32_t saddr;
     u_int32_t daddr;
     /*The options start here. */
   };

後はethernet headerの時と同じなんですが、IPアドレスに関してはu_int32_t型で定義されているので、そのまま表示すると4バイトのIPアドレスが10進数に変換されて表示されます。 これでは正直わかりにくいので、これを普段見るような「192.168.10.11」とかに変換するため、一度in_addr構造体に変換して、さらにinet_ntoa()でstringに変換してもらいます。

    /*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));

ここのプロトコル(ip->protocol)を見ることで、IP headerの次にくるプロトコルUDPとかIPとかICMPとか)が分かります。 今回はDHCPのパケットをキャプチャすることが目的なので、protocol 17番のUDPプロトコルの時だけパケットを表示するプログラムを作成します。

UDP header

ここはもうほとんど言うことはありませんが、以下のように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));
        if(ntohs(udp->uh_dport) == 68 || ntohs(udp->uh_dport) == 67){
            dumpdhcp(buf, sizeof(buf));
        }
    }

DHCPはポート68と67番を使うので、この二つのポート番号の場合は次のDHCPパケットのキャプチャに移ります。

DHCP Packet

やっとDHCPのパケットまでたどり着きました。 DHCPに関してはlinuxのどこに構造体が定義されているかわからなかったので、適当にググってそれらしい構造体をコピペしました。 プロトコルについては下記のサイトが分かりやすかったです。
http://www.picfun.com/lan09a.html
また、以下のようにDHCPの中を整理するための構造体です。

/* 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));

この中で気になるのはattribute*1の部分。
使用するCPUによって何バイトづつ値を読み込むかが違いにより発生するエラーをなくすためのものらしい。 (従来構造体の間にある空白のバイトをなくして、1バイトづつ読み込ませることで、アライメントのエラーを防ぐ)
詳しくは下のサイトを参考にすると良いが、ハードウェアの事とか知らないからよろしくやってくれれば良いと言う方は、気にせずにつけてれば良さそうです。
http://www.kumikomi.net/archives/2008/05/08hard2.php?page=1
後は例のごとく上記の構造体にバッファを当てはめれば良いのですが、uint32_tとかどうやって出力すれば良いの?って言う疑問は残りますね。 ここでは#include <inttypes.h>と言う便利なものをインクルードします。これを使う事で、printfの際にPRlu8とか入れるだけでバイトコードを全部intで表示してくれます。 ただ、DHCPのパケットを理解しようと思ったらそこはバイナリに直さないといけないんですけどね…
とりあえずキャプチャの結果が正しそうかだけでも分かれば良いのでこれで良いでしょう。

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");
}

ちなみにこんな感じで出力されます。

dmac:ff:ff:ff:ff:ff:ff
smac:b6:9d:80:e6:ae:be
protocol:2048
ihl: 5
version: 4
tos: 16
tot_len: 328
id: 0
frag_off: 0
ttl: 128
ip_protocol: 17
check: 0x3996
src_ip 0.0.0.0
dest_ip 255.255.255.255
src port: 68
dest port: 67
uh_len: 308
uh_sum: 47077
--------------------DHCP--------------------
opcode: 1
hw_type: 1
hw_len: 6
gw_hops: 0
tx_id: 2070773066
bp_secs; 0
bp_flags; 0
CIaddr; 0
YIaddr; 0
SIaddr; 0
GIaddr; 0
chaddr: 182,157,128,230,174,190,0,0,0,0,0,0,0,0,0,0,
sname: 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
file: 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
cookie: 1666417251
options: 53,1,3,50,4,192,168,10,60,12,16,105,112,45,49,55,50,45,51,49,45,49,55,45,49,49,53,55,13,1,28,2,3,15,6,119,12,44,47,26,121,42,255,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
--------------------DHCP--END---------------

これでパケットキャプチャは終了です。 これでDHCPリクエストのパケットは解析できるようになったので、次はいよいよDHCPサーバとして機能するように、応答を返します。

ソースコード

ここまでのコードを載せます。

#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));
        if(ntohs(udp->uh_dport) == 67 || ntohs(udp->uh_dport) == 68){
            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");
}

*1:packed