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

Lua Copasを使ってみる

Copasとは、lua-socketを使う際に、非同期通信を行うためのライブラリです。
すごく便利な割に、使い方があまり出てこなかったので、ここで書こうかと思います。
http://keplerproject.github.io/copas/index.html

lua-socketを使用して、単純にTCPリクエストを受け付けるサーバをluaで書いてみると、以下のような感じになります。

local socket = require("socket")
local server = assert(socket.bind("*",80))
local ip, port = server:getsockname()

while true do
    local client = server:accept()
    local line, err = client:receive()
    print(line)
    if not err then client:send("OK\n") end
    client:close()
end

しかし、これだと一つのTCPのセッションが完了するまで、プログラムが待ち状態になってしまい、他のユーザからの通信を受け付けません。 この状況、luaのコルーチンとtimeout機能を使えば回避できるのですが、これをいちいち書いているのはめんど臭いです。
そこで登場するのがCopas
まずは使い方を見てみます。

luaはマルチスレッド環境に対応していませんので、コルーチンを作っても擬似的な並列処理になります。その点、Golangのゴルーチンは勝手にOSのスレッドに多重化してくれるので優秀ですね。

Copasのインストール

luarocks(luaのライブラリマネージャ)でcopasをインストール出来ます。

luarocks install copas

ubuntuだったらapt-getでも入ります。

apt-get install lua-copas

lua-socketも同じ方法で導入することが出来ます。

Copasの使い方

Copasでは通信を処理するHandler関数を定義します。
内部的には、このHandlerをコルーチンで呼び出し、Handlerの処理がビジーになったら別のHandlerに切り替える、といったことを行なっているようです。

以下プログラムのように適当にHandlerを定義します。
中身はなんでも良いです。 ちなみに引数のcの部分はどこかにバインドされたソケットが入ります。   次にcopas.addserver()で上記のHandlerををセットします。 これはなんらかの通信に対するディスパッチャの役割を担ってくれる関数になります。 copas.addserver(server, handler[, timeout])の引数は3つあり、1つ目がソケット、2つ目がHandler(またはHandlerを呼び出す関数)、3つ目がtimeoutの時間です。

copas.wrap(socket)はCopasでsocketを扱う際に便利な関数です。通常、copasで呼び出したhandler内でsend、receiveを行う際には、copas.send(socket) copas.receive(socket)みたいな感じで、いちいちcopasを呼び出さないといけませんが、copas.wrap(socket)としてしまえば、lua-socketと同じくsocket:send()という感じでデータの送受信が可能になります。

最後にcopas.loop()で全てのserverが呼び出されます。

local socket = require("socket")
local copas = require("copas")

function Handler(c,host,port)
    local peer = host .. ":" .. port
    print("connection form",peer)
    c:send("Hello\r\n")
    print("data from",peer,(c:receive"*l"))
end

local server = assert(socket.bind("*",80))
copas.addserver(server ,function(c) return Handler(copas.wrap(c),c:getpeername()) end)
copas.loop()

これで、一度に複数のリクエストが来ても対応できるようになりました。
Copasを使うことで、非同期通信のプログラムがものの数行で書けます。
すごく便利です。

少し応用

今度は、先ほどのプログラムの応用編ということで、通信するポートによって呼び出すハンドラを変えてみます。
以下のプログラムでは、80番ポートへの通信には英語で、90番ポートへの通信にはスペイン語で挨拶を返すプログラムです。
例では、ポート毎に応答を変えていますが、同じ書き方でsrc-ip毎に返答を変えることもできます。
このように、複数の条件でcopas.addserverを呼び出すことでポート番号や通信元アドレスに応じたHandler呼び出しができるようになります。

local socket = require("socket")
local copas = require("copas")

function Router(address, port, handler)
    local server = assert(socket.bind(address,port))
    return copas.addserver(server , function(c) return handler(copas.wrap(c),c:getpeername()) end )
end

function HandlerEnglish(c,host,port)
    local peer = host .. ":" .. port
    print("connection form",peer)
    c:send("hello!\r\n")
    print("data from",peer,(c:receive"*l"))
end

function HandlerSpanish(c,host,port)
    local peer = host .. ":" .. port
    print("connection form",peer)
    c:send("hola!\r\n")
    print("data from",peer,(c:receive"*l"))
end

Router("*",80,HandlerEnglish)
Router("*",90,HandlerSpanish)

copas.loop()

LuaでTOMLのパースプログラムを書く

今日はlua言語でTOMLフォーマットを読み込んで見ます。
luaってあまり聞かない言語ですが、使ってみるとすごい便利だったりします。
なにが良いかって、文字列が非常に扱いやすいこと、そして軽量。 C言語ライクにプログラムが書ける謎のスクリプト言語って感じです。    基本的なところは以下のサイトで勉強しました。 http://starcode.web.fc2.com

TOMLとは?

JSONのように設定ファイルを記述するフォーマットです。 まだ仕様が固まりきっていないみたいなのですが、人間が読みやすく、かつ、記述しやすいように作られています。 最近はやりのGolangでもよく使われていて、GoBGPなどで設定ファイルとして採用しています。

例えば、以下のように記述すると、

[hoge]
a = "HOGE"
[hoge.foo]
b = 1
[bar]
[[bar.barbar]]
c = 2
[[bar.barbar]]
d = 3

JSONてきにはこんな感じになります。

{"hoge":{"a" : "HOGE"}, {"foo" : {"b": 1 } } }
{"bar" : 
    { "barbar" : [
        { "c" = 2 },
        { "d" = 3 }
        ]
    }
}

二重括弧でくくるだけで配列のような構造ができるので、とても記述しやすいし、いちいち括弧の数を数えなくても良いので、とても読みやすく書きやすいです。

Luaで文字列操作

Luaでの文字列操作は、正規表現を使うことが多くなります。
逆に言えば、正規表現で指定した文字列を簡単に取り出したり、配列に入れたりができるので、使いやすいものです。 よく使う、3つくらいを紹介します。

line = string.gmatch(str,"[^\n]+")

これは、文字列中に含まれる改行コードとその前の文字列をを全て抜き取ってline配列に入れています。
(一行ずづ抜き出しています。)

x = string.split("hello world", /\s+/)

これは、文字列をスペースに区切って配列に入れます。

x = string.sub("ABCDE", 2, 4)

これはABCDE→ BCD に削ります。

Luaの配列みたいなの

LuaではPythonでいうリストのような形で、Tableという型が使われる。 これが面白く、

hoge = {} 
hoge["foo"] = 2
print(hoge.foo)

--[[ 
結果
2
]]--

こんな感じで、入れ子のTableにした場合、要素をドットでつなげることで参照できる。上記プログラム中の"foo"の部分を何も指定しなかった場合、1からインクリメントされる数字が勝手に入ってくるのでご注意。
この辺0からじゃなくて1からってのが、Luaが気持ち悪いって言われる所以なのかもしれません。
確かに気持ち悪いです。

LuaでTOMLを読み込み

ここまでくるとなんとなく、TOMLの読み込み方がわかってきました。
TOMLは行毎に意味を持つので、行毎に読み込んで、構造化し、LuaのTableに入れてるプログラムを書きます。

[Global]
    [Global.GlobalConfig]
    As = 1
    HoldTime = 180
    RouterId = "172.17.0.1"
[Neighbors]
    [[Neighbors.NeighborList]]
        NeighborAddress = "10.0.1.2"
    [[Neighbors.NeighborList]]
        NeighborAddress = "10.0.1.3"

上記のようなTOMLファイル(BGPの設定ファイルっぽいもの)を読み込むようなプログラムを作って見ます。

function split(line,delim)
    local ret = {}
    local pat = "[^" .. delim .. "]+"
    local i = 1
    for group in string.gmatch(line, pat) do
        ret[i] = group
        i = i + 1
    end
    return ret
end

function loadstr(str)
    InLabel = {}
    InLabelArray = {}
    ret = {}
    Label = ret
    ArrayTable = 0
    count = 0
    for line in string.gmatch(str,"[^\n]+") do
        line = string.format( "%s", line:match( "^%s*(.-)%s*$" ))
        if string.sub(line,1,1) == "[" then
            if string.sub(line,1,2) == "[[" then
                ArrayTable = 1
                line = string.sub(line,3,#line -2)
                if InLabelArray[line] == nil then
                    InLabelArray[line] = true
                    count = 0
                end
            else
                line = string.sub(line,2,#line -1)
                ArrayTable = 0
            end
            Label = ret
            arrays = split(line,".")
            for i, value in pairs(arrays) do
                if Label[value] ~= nil then --if in the value
                    if i == table.maxn(arrays) then
                        if InLabel[value] ~= nil then
                            InLabel[value] = nil
                        else
                            print("double")
                        end
                    end
                else
                    if i ~= table.maxn(arrays) then
                        InLabel[value] = true
                    end
                    Label[value] = {}
                end
                Label = Label[value]
            end
            if ArrayTable == 1 then
                count = count + 1
                Label[count] = {}
            end
        elseif line:match("=") ~= nil then
            formula = split(line,"=")
            formula[1] = string.format( "%s", formula[1]:match( "^%s*(.-)%s*$" ))
            formula[2] = string.format( "%s", formula[2]:match( "^%s*(.-)%s*$" ))
            if ArrayTable == 1 then
                Label[count][formula[1]]=formula[2]
            else
                Label[formula[1]]=formula[2]
            end
        end
    end
    return ret
end

以下のように呼び出すことができます。
このプログラムではパラメータを全て文字列で呼び出していますが、このあたりはまだ改変する必要があります。

file = io.open("bgp.conf", "r")
string = file:read("*all")
file:close()

test = loadstr(string)

print(test.Global.GlobalConfig.As)
print(test.Neighbors.NeighborList[1].NeighborAddress)
print(test.Neighbors.NeighborList[2].NeighborAddress)
--[[
実行結果
1
"10.0.1.2"
"10.0.1.3"
]]--

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

MacBook+Wiresharkで無線のパケットをキャプチャ

小ネタです。
無線パケットをキャプチャするにはMacBookProが優秀と言う話を聞いたので、やってみました。
キャプチャしたいのはビーコン信号などの無線のL2パケットです。
まずはMacbookWiresharkを開き、上のメニューから
「キャプチャ」→「オプション」
の順に開きます。
f:id:m-masataka:20170126202743p:plain
Wireshark・キャプチャインターフェース」
の画面が出てくるので、インターフェースを指定し、右側の方にある「モニターモード」の欄にチェックをつけます。
f:id:m-masataka:20170126203008p:plain
後は右下の「開始」ボタンを押せば無線のL2パケットのキャプチャが始まります。
このモードだと自分以外の無線のパケットをキャプチャできます。ただ、周りに大量の電波が飛んでいると、全て受け切れず(または電波干渉を起こし)パケットが壊れていることがよくあります。
こういったパケットをキャプチャするためには電波暗室とかあった方が良いんですね…

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

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

FreeradiusとHostapdでWi-Fi接続端末の証明書認証を行う。

FreeradiusとHostapdでWi-Fi接続端末の証明書認証を行う。

オレオレの証明書を使って簡易的にwi-fiの端末認証機能を作る。

参考にしたサイト
http://yasu-2.blogspot.jp/2010/02/debian-lennyhostapdfreeradiuseap-tls.html http://www.freesoftwaremagazine.com/articles/howto_incremental_setup_freeradius_server_eap_authentications

証明書の発行にはOpenSSLを使用する。OpenSSLはコマンドオプションが多くて覚えるのは大変だが、キーペアや証明書を簡単に作成してくれるので非常に便利でだった。
証明書を作成する前にまずはFreeRadiusとHostapdの環境構築をする。本来ならば、Radiusサーバ、CA認証局、APは別々のサーバで動くが、今回は面倒なのですべて一緒のサーバで動かす。

各種インストール

FreeRadiusもhostapdもapt-getやyumでインストール可能。今回はついでにfreeradius-mysqlもインストールし、mysqlによるユーザ管理を可能にする。

# apt-get install freeradius freeradius-mysql hostapd openssl

これで完了。

証明書の作成

まずは認証局の準備をする。
以下のようにCA.shのスクリプトを回す。現在のディレクトリの配下に、勝手にdemoCAというディレクトリが作成され、証明書作成に必要なファイルを生成してくれる。途中で何回か個人情報を聞かれるので、下の例を参考に適当に入力する。

root@ubuntu:# /usr/lib/ssl/misc/CA.sh -newca
CA certificate filename (or enter to create) ← Enterのみ入力

Making CA certificate ...
Generating a 2048 bit RSA private key
...................................+++
....................................+++
writing new private key to './demoCA/private/./cakey.pem'
Enter PEM pass phrase: [パスワードを入力]
Verifying - Enter PEM pass phrase:[パスワードを入力]
-----
You are about to be asked to enter information that will be incorporated
into your certificate request.
What you are about to enter is what is called a Distinguished Name or a DN.
There are quite a few fields but you can leave some blank
For some fields there will be a default value,
If you enter '.', the field will be left blank.
-----
Country Name (2 letter code) [AU]:JP
State or Province Name (full name) [Some-State]:Tokyo
Locality Name (eg, city) []:Meguro
Organization Name (eg, company) [Internet Widgits Pty Ltd]:PPP
Organizational Unit Name (eg, section) []:test

Common Name (e.g. server FQDN or YOUR name) []:nametes
Email Address []:ppp@gmail.com

Please enter the following 'extra' attributes
to be sent with your certificate request
A challenge password []:← Enterのみ入力
An optional company name []:← Enterのみ入力
Using configuration from /usr/lib/ssl/openssl.cnf
Enter pass phrase for ./demoCA/private/./cakey.pem: [先ほどのパスワードを入力]
Check that the request matches the signature
Signature ok
Certificate Details:
        Serial Number: 12891016871502196922 (0xb2e6178b8b74b0ba)
        Validity
            Not Before: Jul 11 05:04:36 2016 GMT
            Not After : Jul 11 05:04:36 2019 GMT
        Subject:
            countryName               = JP
            stateOrProvinceName       = Tokyo
            organizationName          = PPP
            organizationalUnitName    = testuser
            commonName                = nametes
            emailAddress              = ppp@gmail.com
        X509v3 extensions:
            X509v3 Subject Key Identifier:
                79:3E:D8:65:08:CE:48:1F:FB:E4:64:AD:12:48:E7:A9:59:F4:AD:74
            X509v3 Authority Key Identifier:
                keyid:79:3E:D8:65:08:CE:48:1F:FB:E4:64:AD:12:48:E7:A9:59:F4:AD:74

            X509v3 Basic Constraints:
                CA:TRUE
Certificate is to be certified until Jul 11 05:04:36 2019 GMT (1095 days)

Write out database with 1 new entries
Data Base Updated

root@ubuntu:~# ls
demoCA

サーバ用の鍵の作成

以下のコマンドでサーバ用の鍵の作成を行う。

root@ubuntu:~# openssl req -new -nodes -keyout newkey.pem -out newreq.pem -days 365
root@ubuntu:~# openssl ca -policy policy_anything -out newcert.pem  -infiles newreq.pem

他のサイトによると、WindowsXPをclientとして使う場合は追加のオプションがいるらしい。(今回はやらない)
詳しくはこちらhttp://yasu-2.blogspot.jp/2010/02/debian-lennyhostapdfreeradiuseap-tls.html
これでサーバ用のnewkey.pem(秘密鍵)とnewcert.pem(公開鍵)のペアが作成できる。
この鍵は後に作成するクライアント用のキーペアとごっちゃにならないよう、別のディレクトリに保管しておく。

root@ubuntu:~# mkdir wireless_server_keys
root@ubuntu:~# mv newcert.pem newkey.pem wireless_server_keys/
root@ubuntu:~# cp demoCA/cacert.pem wireless_server_keys/

さらに、freeradiusの設定をする際に、このサーバ用の鍵を使うので、/etc/freeradius/certs/フォルダの下にファイルをコピーしておく。

root@ubuntu:~# cp newcert.pem newkey.pem /etc/freeradius/certs/
root@ubuntu:~# cp demoCA/cacert.pem /etc/freeradius/certs/

残った"newreq.pem"(認証要求用のファイル)はいらないので削除しておく。

root@ubuntu:~# rm newreq.pem

クライアント用の鍵の作成

クライアント用の鍵を作成する。

root@ubuntu:~# openssl req -new -nodes -keyout newkey.pem -out newreq.pem -days 365
root@ubuntu:~# openssl ca -policy policy_anything -out newcert.pem  -infiles newreq.pem
root@ubuntu:~# openssl pkcs12 -in newcert.pem -inkey newkey.pem -certfile ./demoCA/cacert.pem -out newcert.p12 -export -name "My Certificate"

※CA.shを回したときと同様に、何回か情報を入力するが、このときに"Organizational Unit Name"はそのままcliantのユーザ名になるため、クライアントごとに固有のものを入力しておくのがよい。 最後のコマンドでは作成したclient用キーペアからwindowsに埋め込むための証明書を作っている。 このときに

Certificate Details:
(略)
Sign the certificate? [y/n]:y
failed to update database
TXT_DB error number 2

などのエラーが出たら、以下のコマンドでCAに対する証明書要求を一旦無効化することで解決する。

root@ubuntu:~# openssl ca -revoke demoCA/newcerts/XXXXXXXXXXXXX.pem

FreeRadiusの設定

freeradiusの設定ファイルの中でも今回はusers,eap.confの設定を変更する。 usersファイルの最終行にアクセスを許可するユーザ(testuser)とパスワード(password)を追加する。

# cd /etc/freeradius
# cat users
     ......
     ......
     ......
     testuser Cleartext-Password := "password"

また、今回はEAP-TLSによる認証を行うので、さらにeap.confの設定を追加する。
以下の項目が設定を変更した箇所である。

default_eap_type = tls
certdir = ${confdir}/certs
cadir = ${confdir}/certs
private_key_password = [サーバ用キーペア作成時に入力したパスワード]
private_key_file = ${certdir}/newkey.pem
certificate_file = ${certdir}/newcert.pem
CA_file = ${cadir}/cacert.pem
dh_file = ${certdir}/dh

Hostapdの設定

hostapdではhostapd.confの設定を変更する。 認証に関係しているのは以下の部分なので、これらを変更する。

ieee8021x=1
eapol_version=1
eapol_key_index_workaround=1
eap_reauth_period=3600
use_pae_group_addr=0
eap_server=0
own_ip_addr=127.0.0.1
radius_client_addr=127.0.0.1
auth_server_addr=127.0.0.1
auth_server_port=1812
auth_server_shared_secret=testing123
acct_server_addr=127.0.0.1
acct_server_port=1813
acct_server_shared_secret=testing123
radius_retry_primary_interval=600
radius_acct_interim_interval=600
wpa=1
wpa_key_mgmt=WPA-EAP
wpa_pairwise=TKIP

今回はhostapdもfreeradiusも同じサーバで動いているため、アドレスはすべてローカルIPである。
暗号化方式等は自由に選択してよい。