Elmでぷよぷよ

前提として、Javascript関数型言語もほとんどやったことないので、慣れてない部分が多くコードも綺麗じゃないのはご勘弁いただきたい。

また、途中まで qiita.com の丸パクリなので、最初は↑読んだ方が良いかも。

とりあえずコードは

GitHub - m-masataka/elm-puyo

に上げる。

盤面を定義

まずは、Elmでどうやって盤面を定義・表示するかを考えてみる。

ElmではModel(アプリの状態)、Veiw(画面表示)、Update(状態の更新)の3つの要素で構成されるらしい。 The Elm Architecture · An Introduction to Elm まずはわかりやすいようにUpdateは考えずに、「Modelを定義して、初期値入れて、Viewで表示する」ってところまでやってみたい。

Modelの定義します。後々の処理をわかりやすくするために、盤面は2次元配列的な感じで定義する。 下のような感じ。

type alias Model =
    { board: Board
    }

type alias Board =
    Array Row


type alias Row =
    Array Cell


type Cell
    = Tile Int
    | Empty

Boardは2次元配列になっていて、各配列にはCell情報が入っている。

CellはTileとEmptyの2つの状態を持っていて、Emptyはその名の通り何もないCell、Tileは何らかのぷよがある状態。

Tile Intとすることで、Intの部分でぷよの種類を識別するようにしている。

次にModelに初期値をセットする。

init : () -> ( Model, Cmd Msg )
init _ =
    let
        board =
            Array.repeat 10 <| Array.repeat 8<| Empty
    in
    ( Model board
      , Cmd.none
    )

let 〜 inの部分で初期値となる2次元配列(board)を生成する。Array.repeatはelm coreで用意されている関数で、指定した長さの配列を自動で生成してくれる。

Elmでは処理内容に応じて多くのmoduleが用意されているのでどんどん使っていく。

続いてView

先ほどModelで定義した2次元配列をひたすらdiv囲って表示することで何となく盤面っぽいのを作る。

-- VIEW
view : Model -> Html Msg
view model =
   div []
       [ div [] [ viewBoard model.board ]
       ]


viewBoard : Board -> Html Msg
viewBoard board =
    div
        Styles.boardStyle
    <|
        Array.toList (Array.map viewRow board)


viewRow : Row -> Html Msg
viewRow row =
    div [] <|
        Array.toList (Array.map viewCell row)

viewCell : Cell -> Html Msg
viewCell cell =
    case cell of
        Tile number ->
            div
                Styles.cellStyle
                [ text t
                ]
        Empty ->
            div
                Styles.cellStyle
                [ text ""
                ]

少しわかりにくいが、viewBoardでboardをRowに切り分けてviewRowへ。viewRowではRowをCellに分けてviewCellへ。 viewCellではboardに定義されたcellの状態をcaseで分類して、何を表示するか決めている。

Style.cellStyleは外出ししたスタイルシートを読み込んでいるだけなので、今は無視でOK。

最後に(最初に書くべきだが)main文を書く

-- MAIN
main =
    Browser.element
        { init = init
        , update = update
        , subscriptions = subscriptions
        , view = view
        }

もはや呪文に近いが、副作用のあるコードを書く場合はBrowser.elementというBrowserモジュールを利用する。

Browserモジュールについては

Elm 0.19 の初期化方法 6 種類 - Qiita

がわかりやすかった。

今の状態では副作用の無いコードだが、後々副作用を入れてくのでこれにする。

subscriptionsはまだ使わないので側だけ定義

-- SUB
subscriptions model =
    Sub.none

ここまでのコードをまとめ

import Browser
import Array exposing (Array)
import Html exposing (..)
import Html.Attributes exposing (..)
import Styles

-- Model

type alias Model =
    { board: Board
    }


type alias Board =
    Array Row

type alias Row =
    Array Cell

type Cell
    = Tile Int
    | Empty

init : () -> ( Model, Cmd Msg )
init _ =
    let
        board =
            Array.repeat 10 <| Array.repeat 8<| Empty
    in
    ( Model board
      , Cmd.none
    )

-- Update
type Msg
    = Demo

update : Msg -> Model -> ( Model, Cmd Msg )
update msg model =
    case msg of
        Demo ->
            ( model
            , Cmd.none
            )


-- VIEW
view : Model -> Html Msg
view model =
   div []
       [ div [] [ viewBoard model.board ]
       ]


viewBoard : Board -> Html Msg
viewBoard board =
    div
        Styles.boardStyle
    <|
        Array.toList (Array.map viewRow board)


viewRow : Row -> Html Msg
viewRow row =
    div [] <|
        Array.toList (Array.map viewCell row)

viewCell : Cell -> Html Msg
viewCell cell =
    case cell of
        Tile number ->
            div
                Styles.cellStyle
                [ text "foo"
                ]
        Empty ->
            div
                Styles.cellStyle
                [ text ""
                ]
-- SUB
subscriptions model =
    Sub.none

-- MAIN
main =
    Browser.element
        { init = init
        , update = update
        , subscriptions = subscriptions
        , view = view
        }

とりあえず盤面はこれで完成。

f:id:m-masataka:20190801154523p:plain

盤面の変更

次に盤面の変更を行う。

今は全部Emptyなので、盤面を変更する。

やることは、2次元配列に値をセットするだけ。ただし、Elmで配列を参照する際は、必ず結果がMaybeで帰ってくるので、そこだけ注意して書く必要がある。

setBoard : (Int, Int) -> Cell -> Board -> Board
setBoard ( x, y ) cell board =
    Array.get y board
        |> Maybe.map (\oldRow -> Array.set x cell oldRow)
        |> Maybe.map (\newRow -> Array.set y newRow board)
        |> Maybe.withDefault board

最初のArray.get yで縦軸のRowをgetし、Maybe.mapでMaybe Rowを受け取る。

(\oldRow -> Array.set x cell oldRow)はコールバック関数で、バックスラッシュは無名関数の意味を持つ。ということでここではcellを新しい値に更新し、新しいRowを作成。

2こ目のMaybe.mapで更新されたRowをBoardにセットし、更新されたBoardが完成。

Maybe.withDefaultではMaybeの値がNothingになった場合の値をセットしている。ここではNotingになったら元のboardを返すという処理になる。

Maybeについては

Elmの練習5 | ぬわーーーーーーー!!!

がわかりやすかった。

setBoard関数をinitで使うと

board =
            setBoard (0,0) (Tile 1)
            <| Array.repeat 10 <| Array.repeat 8<| Empty

こんな感じで盤面の特定のCellを更新できる。

f:id:m-masataka:20190801154650p:plain

次はいよいよぷよぷよ的な盤面の動きを実装していく。

kubernetesを使ってみる。

mysql clusterをdockerで構築できるvitessというプロダクトがあるらしい。

youtubeオープンソースで公開している、mysqlをスケールアウトさせるためのデータベースツール。 kubernetesで簡単にスケールアウトができるらしい。

vitessはdockerを使って簡単に試すこともできるが、クラスターはkubernetesを利用して作成する。
ということで、ローカルな環境にkubernetesをデプロイするところから始める。

kubernetesはkubeadmを使用すればかなり簡単にデプロイすることができる。
とりあえず今回はCentOSでkubernetesクラスターを作成する。

準備

今回の構成はmaster1台とslave node2台で構築する。
すべてのノードに対して以下の操作を行う。

必要な資材はyumでインストールできるのだが、新し目のkubernetesをインストールするためにリポジトリを更新しておく。
/etc/yum.repos.d/kubernetes.repoを以下のようにして作成。

[kubernetes]
name=Kubernetes
baseurl=http://yum.kubernetes.io/repos/kubernetes-el7-x86_64
enabled=1
gpgcheck=1
repo_gpgcheck=1
gpgkey=https://packages.cloud.google.com/yum/doc/yum-key.gpg
        https://packages.cloud.google.com/yum/doc/rpm-package-key.gpg

したらkubeadmをインストール

$ yum -y update
$ yum install -y docker-registry docker kubeadm

あとからいろいろと怒られないようにネットワークのフォアディングとブリッチを許可しておく。

$ sysctl -w net.ipv4.ip_forward=1
$ sysctl -w net.bridge.bridge-nf-call-iptables=1

ひとまず準備はこれで終了

kubeadmの実行

masterノード

masterノードで以下のコマンドを実行

[root@master ~]# kubeadm init --pod-network-cidr=10.244.0.0/16 --apiserver-advertise-address=192.168.56.201
[kubeadm] WARNING: kubeadm is in beta, please do not use it for production clusters.
[init] Using Kubernetes version: v1.7.7
[init] Using Authorization modes: [Node RBAC]
...(中略)...
  mkdir -p $HOME/.kube
  sudo cp -i /etc/kubernetes/admin.conf $HOME/.kube/config
  sudo chown $(id -u):$(id -g) $HOME/.kube/config

You should now deploy a pod network to the cluster.
Run "kubectl apply -f [podnetwork].yaml" with one of the options listed at:
  http://kubernetes.io/docs/admin/addons/

You can now join any number of machines by running the following on each node
as root:

  kubeadm join --token 8a2d44.2cc20d08cf7967f3 192.168.56.201:6443

これでマスターノードが起動し、コマンド実行結果の最後に表示されているkubeadm join --token ....を外のノードで実行すれば完了

slaveノード

[root@node01 ~]# kubeadm join --token 8a2d44.2cc20d08cf7967f3 192.168.56.201:6443

これを全ノードで実行

masterノードで確認

[root@master ~]# kubectl get node
NAME                STATUS     AGE       VERSION
master.atomichost   NotReady   8m        v1.7.5
node01.atomichost   NotReady   3m        v1.7.5
node02.atomichost   NotReady   1m        v1.7.5

kubeadmにより立ち上がっているPodを確認

[root@master ~]# kubectl get pods --all-namespaces
NAMESPACE     NAME                                        READY     STATUS    RESTARTS   AGE
kube-system   etcd-master.atomichost                      1/1       Running   0          8m
kube-system   kube-apiserver-master.atomichost            1/1       Running   0          8m
kube-system   kube-controller-manager-master.atomichost   1/1       Running   0          8m
kube-system   kube-dns-2425271678-ch19z                   0/3       Pending   0          9m
kube-system   kube-proxy-0msfg                            1/1       Running   0          1m
kube-system   kube-proxy-b8pkz                            1/1       Running   0          3m
kube-system   kube-proxy-c9rrc                            1/1       Running   0          9m
kube-system   kube-scheduler-master.atomichost            1/1       Running   0          8m

WARNING: Running with swap on is not supported. Please disable swap or set kubelet's --fail-swap-on flag to false.
こんなエラーが出てきたら/etc/systemd/system/kubelet.service.d/10-kubeadm.confの最後の起動コマンドに--fail-swap-on=falseと付け加えて再実行する。 手順はこんな感じ。

[root@master ~]# kubeadm reset
[root@master ~]# cat /etc/systemd/system/kubelet.service.d/10-kubeadm.conf
[Service]
...(中略)...
Environment="KUBELET_CERTIFICATE_ARGS=--rotate-certificates=true"
ExecStart=
ExecStart=/usr/bin/kubelet $KUBELET_KUBECONFIG_ARGS $KUBELET_SYSTEM_PODS_ARGS $KUBELET_NETWORK_ARGS $KUBELET_DNS_ARGS $KUBELET_AUTHZ_ARGS $KUBELET_CADVISOR_ARGS $KUBELET_CGROUP_ARGS $KUBELET_CERTIFICATE_ARGS $KUBELET_EXTRA_ARGS --fail-swap-on=false
[root@master ~]# systemctl daemon-reload
[root@master ~]# kubeadm join --token 8a2d44.2cc20d08cf7967f3 192.168.56.201:6443

ネットワーク設定

今回はflannelでkubernetesクラスター間のネットワークを構築する。
公式サイトにはcoreosが提供しているkube-flannel.ymlでサービスを起動すればOKって書いてある。今回は適当にgithubにあがってたflannelのymlファイルを持ってきた。

wget  https://raw.githubusercontent.com/coreos/flannel/master/Documentation/kube-flannel.yml
wget https://raw.githubusercontent.com/coreos/flannel/317b7d199e3fe937f04ecb39beed025e47316430/Documentation/k8s-manifests/kube-flannel-rbac.yml

たしかにこれでOKなのだが、各ノードの接続に使用しているネットワーク・インターフェース(kubeadmでadvertise設定したip)がeth0とかデバイスの先頭のものでないときはymlファイルを弄る必要がある。
このため、kube-flannel.ymlを以下のように変更した。

---
kind: ClusterRole
apiVersion: rbac.authorization.k8s.io/v1beta1
metadata:
...(中略)...
containers:
      - name: kube-flannel
        image: quay.io/coreos/flannel:v0.9.0-amd64
        command: [ "/opt/bin/flanneld", "--ip-masq", "--kube-subnet-mgr", "--iface=eth1" ]

...(後略)...

あとは立ち上げるだけ。

[root@master ~]# kubectl apply -f kube-flannel-rbac.yml
[root@master ~]# kubectl apply -f kube-flannel.yml

これでネットワークが設定されると、無事にkune-dnsも立ち上がる。

[root@master ~]# kubectl get pod --all-namespaces
NAMESPACE     NAME                                        READY     STATUS    RESTARTS   AGE
kube-system   etcd-master.atomichost                      1/1       Running   0          4h
kube-system   kube-apiserver-master.atomichost            1/1       Running   0          4h
kube-system   kube-controller-manager-master.atomichost   1/1       Running   0          4h
kube-system   kube-dns-545bc4bfd4-zmgg8                   3/3       Running   0          4h
kube-system   kube-flannel-ds-4tshg                       1/1       Running   0          1h
kube-system   kube-flannel-ds-crpt7                       1/1       Running   0          1h
kube-system   kube-flannel-ds-fkd6b                       1/1       Running   0          1h
kube-system   kube-flannel-ds-n6jhv                       1/1       Running   0          1h
kube-system   kube-proxy-c6px7                            1/1       Running   0          4h
kube-system   kube-proxy-r7ppv                            1/1       Running   0          1h
kube-system   kube-proxy-w6m84                            1/1       Running   0          1h
kube-system   kube-proxy-zv42h                            1/1       Running   0          1h
kube-system   kube-scheduler-master.atomichost            1/1       Running   0          4h

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