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である。
暗号化方式等は自由に選択してよい。

WebSSHっぽいことをしてみる。

WebSocket経由でSSHの操作を行います。 といっても、今回行うのはChromeのSource ShellのようにWebブラウザをSSHとして使うのではなく、 あくまでも、ウェブ経由でサーバ側のSSHプロセスを操作するというものです。

今回参考にしたのは、githubにあるこのリポジトリ
https://github.com/aluzzardi/wssh
WSSHってなんかマルウェアの名前みたい、というか使い方によってはまるっきりマルウェアなんでしょうけど...
ここで必要なの物は以下の3つ、なんとすべてpythonパッケージで構築ができます。 ・Webサーバ -> Flask ・WebSocket -> genevt ・sshクライアント -> paramiko Flaskは軽量なWebフレームワークライブラリ(有名どころでいうと、djangoRuby on Rails見たいなもの)
geventは並行ライブラリ、socketを利用したネットワークアプリケーションを作成するときに便利です。
paramikoはpythonSSH接続ができるようになるライブラリ、pythonのttyライブラリと組み合わせることで、 linuxSSHクライアントと同等の動作が可能になります。

Flask

基本的な使い方はこう

from flask import Flask, request, abort, render_template
from jinja2 import FileSystemLoader
import os
app = Flask(__name__) #インスタンスの作成

@app.route('/')#ルートにアクセスした場合の処理
def index():
    return render_template('index.html') #index.htmlがブラウザに表示される。

if __name__ == "__main__":
    #rootpathの指定と、何かしらファイルを参照する際のtemplatesフォルダを指定する。
    #index.htmlはtemplatesフォルダに入れておくと、render_template(~~~)で簡単に参照できる。
    root_path = os.path.dirname('/')
    app.jinja_loader = FileSystemLoader(os.path.join(root_path,'templates'))
    app.run() #サーバの立ち上げ

デフォルトではlocalhost:5000向けにサーバが立ち上がります。 テンプレートディレクトリの指定などはdjangoに似ていてわかりやすいです。

gevent

pythonでウェブソケットを実装しようとして始めて知ったが、このgeventがかなり使いやすいです。 基本的には、複数の処理を並列して実行可能にするライブラリと考えて良いと思います。 バックエンドで何らかの待ちうけ処理を行い、かつ、その裏で他のプログラムを実行したい場合に超有効。 ネットワークアプリなんかは典型的な例です。 今回はwebsocket -> sshssh -> websocketの入出力をgeventで並列実行する。 簡単に使い方を紹介。

from flask import Flask, request, abort, render_template
from werkzeug.exceptions import BadRequest
from gevent import monkey
monkey.patch_all()
import gevent

import socket
import time

app = Flask(__name__)


class WSSHBridge(object):
    def __init__(self,websocket):
        self._websocket = websocket
        self._tasks = []

    def _forward_inbound(self):
        try:
            while True:
                data = self._websocket.receive()
                if not data:
                    return
                print data
        finelly:
            self.close()
    
    def _forward_outbound(self):
        try:
            while True:
                time.sleep(5)
                self._websocket.send("return message")

    def _bridge(self):
        self._tasks = [
            gevent.spawn(self._forward_inbound),
            gevent.spawn(self._forward_outbound)
        ]
        gevent.joinall(self._tasks)
    
@app.route('/')
def index():
    bridge = WSSHBridge(request.environ['wsgi.websocket'])
    bridge._bridge()

if __name__ == "__main__":
    #app.run()
    from gevent.pywsgi import WSGIServer
    from geventwebsocket.handler import WebSocketHandler
    from jinja2 import FileSystemLoader
    import os
    root_path = os.path.dirname('/')
    app.jinja_loader = FileSystemLoader(os.path.join(root_path,'templates'))
    http_server = WSGIServer(('0.0.0.0',5000),app,log=None,handler_class=WebSocketHandler)
    try:
        http_server.serve_forever()
    except KeyboardInterrupt:
        pass

上記はwebsocketでクライアントから送られてくるメッセージをprintで標準出力し、サーバ側は5秒おきにOKの返事を返す仕組みです。
geventで並列動作しているので、両者が独立して動くきます。
(何のためのプログラムなんでしょうか...まあなんかいろいろできそうですね。) wscatを使えば簡単にウェブソケットを試せるのでお勧めです。
(ちょっと試験したいだけなのに、javascriptで書くはめんどくさいので。。。)

wscat --connect ws://localhost:5000/

ssh

いよいよSSH接続をして見ます。 pythonSSH接続をするにはparamikoがお勧めです。
何よりも簡単だし、SSHのプロセスを理解していない私でも使えます。 ちなみに、paramikoを使えばSCPやSFTPなんかもできちゃいます。 下記は指定のサーバにssh接続して、コマンドを実行する例。

import paramiko
from paramiko import PasswordRequiredException
from paramiko.dsskey import DSSKey
from paramiko.rsakey import RSAKey
from paramiko.ssh_exception import SSHException


def open(self, hostname, port=22, username=None,password=None,private_key=None, key_passphrase=None, allow_agent=False,timeout=None):
    pkey=None
    self._ssh.connect(
        hostname=hostname,
        port=port,
        username=username,
        password=password,
        pkey=pkey,
        timeout=timeout,
        allow_agent=allow_agent,
        look_for_keys=False)


ssh = pramiko.SSHClient()
ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy())
open('localhost',22,'root','password')
stdin, stdout, stderr = ssh.exec_command('ls -l')

set_missing_host_key_policyは接続した際に、サーバホストキーがなかった場合の挙動を定義しています。 よく、最初にサーバへsshするときに、「~~~~yes no」みたいに聞かれますよね? あれです。今回はここで処理がとまると面倒なので、paramiko.AutoAddPolicy()で自動的にホストキーを追加しています。 いつもの質問に自動的にyesって答えるということです。
connect()でSSHのコネクションを張ります。 connectの中のパラメータはサーバ側に設定したものにあわせて設定、今回は鍵の設定をしていないのでほとんどNoneですが
最後、コマンドの実行はssh.exec_command()です。
実行結果はstdoutに帰ってきます。

Websocket経由でSSH

パーツが揃ったので、いよいよ本題の「WebSocket経由でSSH」を実行してみます。
といってもやることは簡単で、websocketで受け取った入力をコマンドとしてparamikoで実行して、 その結果をまたwebsocketでクライアントに返すだけです。
下記のプログラムを実行すると、5000番ポートでHTTPリクエストを待ち受け、ブラウザ(index.html)とpythonプロセスとの間でwebsocketを使ったやり取りをしています。

from gevent import monkey
monkey.patch_all()

from flask import Flask, request, abort, render_template
from werkzeug.exceptions import BadRequest
import gevent
from gevent.socket import wait_read, wait_write

import paramiko
from paramiko import PasswordRequiredException
from paramiko.dsskey import DSSKey
from paramiko.rsakey import RSAKey
from paramiko.ssh_exception import SSHException

import socket
import time

app = Flask(__name__)

@app.route('/')
def index():
    return render_template('index.html')

@app.route('/web/')
def webS():
    bridge = WSSHBridge(request.environ['wsgi.websocket'])
    bridge.execute()

class WSSHBridge(object):
    def __init__(self,websocket):
        self._websocket = websocket
        self._ssh = paramiko.SSHClient()
        self._ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy())
        self.open('localhost',22,'root','password')
        self._transport = self._ssh.get_transport()
        self._channel = self._transport.open_session()
        self._channel.get_pty('xterm')
        self._tasks = []

    def open(self, hostname, port=22, username=None,password=None,private_key=None, key_passphrase=None, allow_agent=False,timeout=None):
        pkey = None
        self._ssh.connect(
            hostname=hostname,
            port=port,
            username=username,
            password=password,
            pkey=pkey,
            timeout=timeout,
            allow_agent=allow_agent,
            look_for_keys=False)

    def _forward_inbound(self):
        try:
            while True:
                data = self._websocket.receive()
                self._channel = self._transport.open_session()
                self._channel.get_pty('xterm')
                self._channel.exec_command(data)
                while True:
                    data = self._channel.recv(1024)
                    self._websocket.send(data)
                    if len(data) == 0:
                        break
        finally:
            self.close()

    def _bridge(self):
        self._channel.setblocking(False)
        self._channel.settimeout(0.0)
        self._tasks = [
            gevent.spawn(self._forward_inbound)
        ]
        gevent.joinall(self._tasks)

    def execute(self):
        self._bridge()
        self._channel.close()

    def close(self):
        gevent.killall(self._tasks, block = True)
        self._tasks = []
        self._ssh.close()

if __name__ == "__main__":
    from gevent.pywsgi import WSGIServer
    from geventwebsocket.handler import WebSocketHandler
    from jinja2 import FileSystemLoader
    import os
    root_path = os.path.dirname('/')
    app.jinja_loader = FileSystemLoader(os.path.join(root_path,'templates'))
    http_server = WSGIServer(('0.0.0.0',5000),app,log=None,handler_class=WebSocketHandler)
    try:
        http_server.serve_forever()
    except KeyboardInterrupt:
        pass

さらに、このpythonプロセスとwebsocketで通信するjavascriptのプログラム(index.html)が以下
これはただ単純にwebsocketのチャットプログラムで、チャット相手がSSHサーバになったと考えればよい感じ。

<!DOCTYPR html>
<html>
<head>
    <title>WebSSH</title>
    <script type="text/javascript" src="//ajax.googleapis.com/ajax/libs/jquery/1.11.1/jquery.min.js"></script>
    <script type="text/javascript">
        var uri = "ws://"+ location.host +"/web/";
        console.log(uri);
        var webSocket = null;

        function init(){
            $("[data-name='message']").keypress(press);
            open();
        }

        function open(){
            if(webSocket == null){
                webSocket = new WebSocket(uri);
                webSocket.onopen = onOpen;
                webSocket.onmessage=onMessage;
                webSocket.onclose = onClose;
                webSocket.onerror = onError;
            }
        }

        function onOpen(event){
            chat("connected");
        }
        function onMessage(event){
            if(event && event.data){
                chat(event.data);
            }
        }

        function onError(event){
            chat("error");
        }

        function onClose(event){
            chat("connection closed. "+ event.code);
            webSocket = null;
            setTimeout("open()",3000);
        }

        function press(event){
            if(event && event.which == 13){
                $("[data-name='chat']").append("<div>root@ubuntu# "+$("[data-name='message']").val()+"</div>");
                var message = $("[data-name='message']").val();
                if(message && webSocket){
                    webSocket.send(""+message);
                    $("[data-name='message']").val("");
                }
            }
        }

        function chat(message){
            var chats = $("[data-name='chat']").find("div");
            while (chats.length >= 100){
                chats = chats.last().remove();
            }
            var msgtag = $('<div>').text(message);
            $("[data-name='chat']").append(msgtag);
        }
        $(init);
    </script>
</head>
<style>
body{
    background-color:#000000;
    color:#00ff00;
    font-family:'arial black';
}
input[type="text"], textarea {
    background-color : #000000;
    color:#00ff00;
    font-family:'arial black';
}
input{
    border:#000000;
}
</style>
<body>
    <div data-name="chat"></div>
    root@ubuntu#
    <input type="text" data-name="message" size="100"/>
    <hr />
</body>
</html>

こんな感じで動きます。ttyを転送してるわけじゃないから、viエディタとか使えませんけどね... f:id:m-masataka:20161102170735p:plain

OpenWRT Closs Compile メモ

OpenWRTをインストールできたので、OpenWRT上で動くアプリケーションをコンパイルする。
OpenWRTが動いているルータ(TP-Link WDR4300 N750)はディスク容量もメモリもかなり少ないため、ルータ上でプログラムをコンパイルすることができない。 そのため、別のLinuxサーバでクロスコンパイル環境を構築し、ソースコードをビルドする必要がある。
今回はOpenWRT上で最新版のHostapdを動かすことを目標に、クロスコンパイルを実行していく。

OpenWRT SDKの準備

OpenWRT SDKとはOpenWRT用のクロスコンパイル環境が一式そろったものである。 それぞれのバージョン・アーキテクチャごとにSDKが用意されているので、自分のバージョンに合ったものをダウンロードする。
(ファームウェアイメージと同じ場所にある)

linux上でファイルを解凍。   名前が長いので変更しました。

root@ubuntu # tar -xvf OpenWrt-SDK-15.05-ar71xx-generic_gcc-4.8-linaro_uClibc-0.9.33.2.Linux-x86_64.tar.bz2
    ...
    ...
    ...
root@ubuntu # mv OpenWrt-SDK-15.05-ar71xx-generic_gcc-4.8-linaro_uClibc-0.9.33.2.Linux-x86_64.tar.bz2 openwrt
root@ubuntu # ls -al
    drwxr-xr-x 12 root root     4096 Aug 17 14:40 openwrt

実はクロスコンパイル環境はこれで準備完了。
SDKが用意されているのは便利ですね。
本来ならば、自分の環境のSDKはgitのソースから自分でビルドする必要があります。
やり方は公式ページのDeveloper Guideを参照
https://wiki.openwrt.org/doc/playground/developer

"Hello,World!"のコンパイル

まずは簡単なプログラムからビルドする。

root@ubuntu # cd openwrt
root@ubuntu # mkdir helloworld
root@ubuntu # cd helloworld

適当に作業用ディレクトリを作成し、その中にc言語のプログラムを作成

#include <stdio.h>
int main(void)
{
        printf("Hello World\n");
        return 0;
}

このコードをコンパイルするのだが、Host環境のgcc(/usr/bin/gcc)でコンパイルしてはいけない。
今回はSDKの中にあるgccコンパイルを実行する。

root@ubuntu # ../staging_dir/toolchain-mips_34kc_gcc-4.8-linaro_uClibc-0.9.33.2/bin/mips-openwrt-linux-gcc -o helloworld.o helloworld.c
root@ubuntu # ls
    helloworld.o helloworld.c 
root@ubuntu # ./helloworld.o
    -su: ./helloworld.o: cannot execute binary file: Exec format error

出来上がったファイルをHost環境で実行するとformat errorで実行できない。 これはOpenWRT環境で動くファイルなので当然である。 出来上がったファイルをOpenWRT化されているルータに移す。

root@ubuntu # scp helloworld.o root@192.168.1.1:

OpenWRTルータ内でファイルを実行

root@OpenWRT # chmod a+x helloworld.o
root@OpenWRT # ./helloworld.o
     Hello World

実行できた。

Hostapdのコンパイル

OpenWRTで使われていたhostapdは最新versionでは無く、hostapd_cliも使えない。
何か開発しようとすると使い勝手が悪いので、今回は最新版のhostapdをOpenWRT上で動かせるようにコンパイルを行う。

hostapdをダウンロードして解凍。わかりやすいようにopenwrtディレクトリ配下に入れておく。

root@ubuntu # wget https://w1.fi/releases/hostapd-2.5.tar.gz
root@ubuntu # tar -zxvf hostapd-2.5.tar.gz
root@ubuntu # mc hostapd-2.5 openwrt/hostapd

ほとんどのMakefile

ifndef CC
CC=/usr/bin/gcc
endif
...
...
...

という感じで、明示的にコンパイラを指定しない限りはHostのコンパイラを使用するようになっている。
したがってMakefileがクロスコンパイル環境のgccを読み込んでくれるようにCCを明示しておく。

root@ubuntu # export OPENWRT_HOME=[openwrt直下のディレクトリ]
root@ubuntu # export CC=$OPENWRT_HOME/staging_dir/toolchain-mips_34kc_gcc-4.8-linaro_uClibc-0.9.33.2/bin/mips-openwrt-linux-gcc

さて、
これでmakeを実行すればhostapdのコンパイル完了。
というわけにはいかなかった。
hostapdのmakeopenwrt/hostapd/hostapd/直下で行う。

root@ubuntu # cd hostapd/hostapd/
root@ubuntu # make
mips-openwrt-linux-gcc: warning: environment variable 'STAGING_DIR' not defined

まずこんなエラーがたくさん出てくるので、言われているとおりにSTAGING_DIRを設定する。

root@ubuntu # export STAGING_DIR=$OPENWRT_HOME/staging_dir/toolchain-mips_34kc_gcc-4.8-linaro_uClibc-0.9.33.2

再びmake

root@ubuntu # make
....
....
  CC  ../src/drivers/driver_hostap.c
../src/drivers/driver_nl80211.c:17:31: fatal error: netlink/genl/genl.h: No such file or directory
 #include <netlink/genl/genl.h>
                               ^
compilation terminated.
make: *** [../src/drivers/driver_nl80211.o] Error 1

やはりというか、途中でとまりました。 クロスコンパイル環境なので、いろいろとファイルが足りないことがある。 今回のようにヘッダーファイルが無いだけならHost環境の/usr/include/などからひっぱてくればよい。
以下のようにssl系とnetlink系のヘッダファイルが無いというエラーがでたので、これらを引っ張ってくる。

root@ubuntu # cp -r /usr/include/netlink/ ../../staging_dir/toolchain-mips_34kc_gcc-4.8-linaro_uClibc-0.9.33.2/usr/include/
root@ubuntu # cp -r /usr/include/openssl/ ../../staging_dir/toolchain-mips_34kc_gcc-4.8-linaro_uClibc-0.9.33.2/usr/include/
root@ubutnu # cp /usr/include/x86_64-linux-gnu/openssl/opensslconf.h ../../staging_dir/toolchain-mips_34kc_gcc-4.8-linaro_uClibc-0.9.33.2/usr/include/openssl/

問題なのは共有ライブラリが無かった場合。

root@ubuntu # make 
.....
.....
../../staging_dir/toolchain-mips_34kc_gcc-4.8-linaro_uClibc-0.9.33.2/bin/../lib/gcc/mips-openwrt-linux-uclibc/4.8.3/../../../../mips-openwrt-linux-uclibc/bin/ld: cannot find -lnl
../../staging_dir/toolchain-mips_34kc_gcc-4.8-linaro_uClibc-0.9.33.2/bin/../lib/gcc/mips-openwrt-linux-uclibc/4.8.3/../../../../mips-openwrt-linux-uclibc/bin/ld: cannot find -lssl
../../staging_dir/toolchain-mips_34kc_gcc-4.8-linaro_uClibc-0.9.33.2/bin/../lib/gcc/mips-openwrt-linux-uclibc/4.8.3/../../../../mips-openwrt-linux-uclibc/bin/ld: cannot find -lcrypto
collect2: error: ld returned 1 exit status
make: *** [hostapd] Error 1

普通このようなエラーが出た場合はapt-getとかでライブラリをとってくれば解決なのだが、クロスコンパイル環境では頑張ってネットからライブラリを探すか、ソースからビルドする必要がある。
ちなみに共有ライブラリは

  • -lnl => libnl.so
  • -lssl => libssl.so
  • -lcrypto => libcrypto.so

というように読み替える。この.soファイル(バイナリファイル)を参照できるようにする必要がある。   libssl.solibcrypto.soはOpenwrt SDKの中に見つかったので、参照できる場所にコピーする。

root@ubuntu # ls -l ../../staging_dir/target-mips_34kc_uClibc-0.9.33.2/usr/lib/libssl*
lrwxrwxrwx 1 root root     15 Jul 25  2015 ../../staging_dir/target-mips_34kc_uClibc-0.9.33.2/usr/lib/libssl.so -> libssl.so.1.0.0
-r-xr-xr-x 1 root root 381267 Jul 25  2015 ../../staging_dir/target-mips_34kc_uClibc-0.9.33.2/usr/lib/libssl.so.1.0.0
root@ubuntu # ls -l ../../staging_dir/target-mips_34kc_uClibc-0.9.33.2/usr/lib/libcrypto*
lrwxrwxrwx 1 root root      18 Jul 25  2015 ../../staging_dir/target-mips_34kc_uClibc-0.9.33.2/usr/lib/libcrypto.so -> libcrypto.so.1.0.0
-r-xr-xr-x 1 root root 1765806 Jul 25  2015 ../../staging_dir/target-mips_34kc_uClibc-0.9.33.2/usr/lib/libcrypto.so.1.0.0
root@ubuntu # cp ../../staging_dir/target-mips_34kc_uClibc-0.9.33.2/usr/lib/libssl.so.1.0.0 ../../staging_dir/toolchain-mips_34kc_gcc-4.8-linaro_uClibc-0.9.33.2/usr/lib/
root@ubuntu # cp ../../staging_dir/target-mips_34kc_uClibc-0.9.33.2/usr/lib/libcrypto.so.1.0.0 ../../staging_dir/toolchain-mips_34kc_gcc-4.8-linaro_uClibc-0.9.33.2/usr/lib/
root@ubuntu # cd ../../staging_dir/toolchain-mips_34kc_gcc-4.8-linaro_uClibc-0.9.33.2/usr/lib/libcrypto.so.1.0.0
root@ubuntu # ln -s libssl.so.1.0.0 libssl.so
root@ubuntu # ln -s libcrypto.so.1.0.0 libcrypto.so

共有ライブラリは新しいバージョンの.soファイルにリンクする形になっているので、ここでも同じ形式で管理するのがよい。

libnl.soは見つからない。調べてみると、OpenWRT自体libnlは以下のライブラリしか扱っていないらしい。 Name Size Description libnl-core 37K Common code for all netlink libraries libnl-genl 8K Generic Netlink Library Functions libnl-nf 25K Netfilter Netlink Library Functions libnl-route 91K Routing Netlink Library Functions

libnl-3を使ってもコンパイルが失敗する。 仕方が無いので、libnlのversion1のソースをダウンロードしてコンパイルすることにする。

root@ubuntu # wget https://www.infradead.org/~tgr/libnl/files/libnl-1.1.4.tar.gz
root@ubuntu # tar -zxvf libnl-1.1.4.tar.gz
root@ubuntu # cd libnl-1.1.4/
root@ubuntu # ./configure
root@ubuntu # make 

これもちゃんとクロスコンパイル環境のgccコンパイルする。 出来上がった.soファイルをその他2つと同じようにSTAGING_DIR/usr/lib/の中に入れて再びmakeをすればOK。
あとは出来上がったhostapdhostapd_cliをOpenWRTルータに移して実行できる。

結構長かったけど、お疲れ様でした。

makeファイルをデバック

makeファイルをデバックしたいときはこんな感じでコンソールに出力できる。
読み込んでいるライブラリとかをまとめて確認したいときは便利。 dummy := $(shell echo 表示したい内容 1>&2)

Go でAPIを書く

目標は
JSONを受け取る→{何らかの処理}→処理結果をJSON形式で返す
これだけのAPIを作ります。 以下のページを参考にしました。 thenewstack.io

パッケージ

まずはインポートするパッケージ

    import (
            "fmt"  
            "html"  
            "log" 
            "net/http"  
            "encoding/json"  
            "github.com/gorilla/mux"  
    )

"fmt"は標準出力に関するパッケージ。
"net/http"は簡単にhttpサーバを実装してくれるパッケージ。
"html"はhtml文をトークン化&解析するもの。
"encoding/json"はスライスなどをJSONエンコードしてくれるパッケージ。
そして最後に"github.com/gorilla/mux"。これは外部パッケージで、httpリクエストが来た時に、リクエストをGoで定義された関数までルーティングしてくれる。無くてもよいのだけど、とても便利なパッケージである。
外部パッケージを使う場合は現在プログラムを書いているディレクトリで

    go get

を実行する必要があります。 ※"gorilla/mux"を入れない場合はデフォルトのmux(multiplexer)によってルーティングを書くのだけど、少しめんどくさそう...詳しく知りたい人は以下を参照

m0t0k1ch1st0ry.com

メイン文

メイン文はこんな感じ。

    func main(){
            router := mux.NewRouter().StrictSlash(true)
            router.HandleFunc("/",Index)
            router.HandleFunc("/todos/{todoId}",TodoShow)
            log.Fatal(http.ListenAndServe(":8080",router))
    }

外部パッケージの"gorilla/mux"を使ってhttpルータを定義します。
router.HandleFuncによってhttpリクエストをそれぞれの関数にルーティングしてあげます。

    router.HandleFunc("{リクエストURL}",{関数名})

って感じの使い方です。
http.ListenAndServe(":8080",router)で8080番ポートでhttpサーバを起動させます。ここの第2引数routerの部分はhandlerを入れる部分、"gorilla/mux"等のhttpルータを使ってなければnilで埋めれば良いらしい。

関数の作成

    func Index(w http.ResponseWriter, r *http.Request){
            fmt.Fprintf(w,"Hello, %q",html.EscapeString(r.URL.Path))
    }
    func TodoShow(w http.ResponseWriter , r *http.Request){
            vars := mux.Vars(r)
            todoId := vars["todoId"]
            fmt.Fprintln(w,"Todo show:",todoId)
    }

今回はリクエストに応じて2つの関数を作りました。(参考ページに乗っているサンプル通りだけど...)
はじめの関数IndexはURLのパスを返すだけの関数。ここにhtmlのレスポンスを書けば、ウェブページが作れる。
2つ目の関数TodoShowはmain文で受け取ったURLの{todoId}を表示する関数になっている。
URLの{todoId}の部分は任意の文字列を入れることができる。
たとえば http://localhost:8080/todos/hogehoge というようにリクエストを送ると、todoIdの部分にはhogehogeが入ってきて、Todo show:hogehogeという文字列が出力される。

JSONでデータを受け取る

ここからは少し応用編
JSON形式のデータを受け取る部分に入る。
http.requestの中で送られてくるデータはhttp.Request.Bodyの中に入っている。 今回はBodyの中身を一度Stringで読み込んで、それをJSONファイルとしてデコードする。(あまりスマートじゃない気がする)

    import (
            "fmt"
            "html"
            "log"
            "net/http"
            "encoding/json"
            "io"
            "bufio"
     
            "github.com/gorilla/mux"
    )
    
    type POSTJSON struct{
            Board string "json:\"board\""
            Player int "json:\"player\""
    }
       
    func TodoIndex(w http.ResponseWriter,r *http.Request){
            request := ""
            rb := bufio.NewReader(r.Body)
            for {
                    s, err := rb.ReadString('\n')
                    request = request + s
                    if err == io.EOF { break }
            }
            u := new(POSTJSON)
            json.Unmarshal(([]byte)(request),u)
            fmt.Println(u.Board)
    }

まず、importパッケージの中に"io""bufio"を追加している(データ読み込みのため)。
forの中身でbufio.NewReader(r.Body)をstringへ変換している。ここまででstringにデータを変換できたので、無理してJSONとして読み込む必要はないきがするが、、、
JSONデータを受けるためには、受け皿になる構造体が必要なので、type POSTJSON structでそのPOSTJSON(名前は何でも良い)構造体を定義している。
u := new(POSTJSON)によって構造体を初期化。
ここで重要な関数Unmarshalの登場。この関数により、stringをbyteに変換し構造体の中にマップする。
これで、送られてきたJSONデータを構造体へと変換することができたので、データへの簡単なアクセスが可能になった。 ちなみに、htmlのFORMで送られてきたデータを受け取りたいだけなら

    r.ParseForm()

でリクエストの中身を解析して、

    for k, v := range r.Form {
            fmt.Println("key:", k)
            fmt.Println("val:", strings.Join(v, ""))
        }

こんな感じで中身を取り出すことができるらしい。JSONでもこんな感じでできないか探してます。

フォームの入力を処理する | build-web-application-with-golang

JSONデータを返す

あとはJSONデータを返すだけ。これは先ほどとは逆に構造体→JSONという形で変換ができます。

    type Todo struct{
            Name string
            Completed bool
            Num int
    }
    type Todos []Todo

    func TodoIndex(w http.ResponseWriter,r *http.Request){
        type Todos []Todo
    
        todos := Todos{
                    Todo{Name: "Host meetup", Completed: true , Num: 1},
                    Todo{Name: "KEET", Completed: false , Num: 2},
            }
            json.NewEncoder(w).Encode(todos)
    }

上記では構造体配列を使っている。このようにjson.NewEncoder(w).Encode(todos)により構造体配列も簡単にJSONに変換することができる。さらに、http.ResponseWriterに変換したJSONを入れているので、リクエスト先にJSONでレスポンスが返される。

これでJSONを受け取ってJSONを返すAPIが完成?!
お疲れ様でした。

TP-Link WDR4300 N750にOpenWRTを入れてみる。

前回、Buffalo WSR-600DHPをOpenWRT化することに成功したが、Wi-Fi設定の部分で見事に躓いてしまった。
* WSR-600DHPはMediaTekチップセット(MT7603E)を使っており、このチップセット対応のファームウェアがWSR-600DHP用に作成されていない。つまり、WSR-600DHPで5GHz帯のWi-Fiを使いたいのであれば、自分でファームウェアをビルドする必要がある。(難しかったので断念...) そこで、Buffalo ルータはサクッと諦めて、別のルータを購入しOpenWRT化する。
今回使ったのはTP-Link WDR4300 N750
(5GHz帯がOpenWRTで使用できるルータはかなり少ないし、日本で売っているのかは謎。TP-Linkの最新の無線LANルータはArcher 7である。Archer 7チップセットファームウェアとしてAth10kを使用しているが、Ath10kはまだOpenWRTでサポートされていない。よって、今回はTP-Linkの最新版からは3世代ほど前の型であるWDR4300を使用した。)

WDR4300のWebUIからOpenWRT化 → 失敗

OpenWRTの公式ページにはTP-LinkのWebUIからFirmware UpgradeでOpenWRTの...factory.binファイルを選択し、Upgradeボタンを押すとOpenWRTにアップグレードされるよと書いてある。
しかし、TP-linkはそんなに甘くは無いんだな。 見事に失敗

Upgrade unsuccessfully because the version of the upgraded file was incorrect. Please check the file name.  

こんなエラーメッセージが....
どうしたものかなこれ
いろんなバージョン.binファイルを試したが、同じエラーが出続ける。挙句の果てにはTP-Link公式ページからダウンロードしてきたファームウェアもアップグレード失敗(T.T)....
軽く怒りがこみ上げてくるが、こんな事では諦めない。

TFTPサーバからOpenWRT化 → 失敗

続いてはTFTPサーバからのブートを試す。

assign 192.168.0.66 to your local network interface (the router uses 192.168.0.86)
publish a firmware image via tftp: cp openwrt-ar71xx-generic-tl-wdr4300-v1-squashfs-factory.bin/srv/tftp/wdr4300v1_tp_recovery.bin
configure your tftp server
wait for the firmware transfer (about 20s), firmware flash (about 90s) and subsequent reboot (about 30s)

OpenWRTの公式にはこんなことが書いてあるので、その通りに実装(Tftp64っていうソフトでWindowsを簡単にTFTPサーバにできます。)
が、ぜんぜんだめ

[http://www.friedzombie.com/tplink-stripped-firmware/]

この記事によると、TP-Linkは最近になって正規のファームウェアしかアップデートできないようなってるようである...
これは難敵、YouTubeでアップデート手順を見てみると、

  • ルータをこじ開け
  • 無理やりシリアルコンソールを繋ぐ
  • シリアルポートからブートを強制中断
  • 無理やりブート対象を書き換え

なんていう荒業をしている動画があった
(できればそんなことやりたくないね。)


Bricked TP-Link WDR4300 Router Recovery Using UART Serial Converter Part #1

さらに検索検索。

ネット上にこんな一言が、
「DD-DRTにならアップグレードできるよ」

そんなことがあるのか?

DD-WRT化の後にOpenWRT化 → 成功

とりあえず、DD-WRTのbinファイルを公式ホームページから取ってくる。よくよくネットを見ていると、最新バージョンより2~3個前のほうが安定しているらしいので、最新よりも2~3個前のDD-WRTのbinファイルを取ってくる。DD-WRTはファームの作成年月日が細かく分かれていて、使いやすそう。
※同じTP-Linkルータでも、ファームウェアversionが違うことがあるらしく(今回はver1.7)versionによってアップデートできるファームウェアが違うっぽい。今回は下記のDD-WRTファームウェアでアップデートできたが、Versionによっていろいろ試す必要がありそう。

[ftp://ftp.dd-wrt.com/betas/2016/07-01-2016-r30082/tplink_tl-wdr4300v1/]

今回使用したのは上記のURLの中にある"factory-to-ddwrt-us.bin"っていうファイル。これをTP-LinkのWebUIからアップデート。

なんとアップデート成功。後は下記のリンクのやり方にしたがって、ファームをDD-WRTからOpenWRTに書き換えるだけ。

[http://www.madox.net/blog/2013/03/10/how-to-install-openwrt-on-a-tl-wr703n-that-came-with-dd-wrt/]

で、telnet 192.168.1.1でDD-WRTコマンドラインに入れるのだけれど、sshで入りたかったらUIで設定できるみたい。 あとは、wgetかなんかでopenwrtのファームウェアをダウンロードして(....factory.binとかいうやつ)
以下のコマンドでファームウェアアップグレード

root@DD-WRT:/tmp# mtd -r write openwrt-ar71xx-generic-tl-wdr4300n-v1-squashfs-factory.bin linux
Unlocking linux ...
Writing from openwrt-ar71xx-generic-tl-wr703n-v1-squashfs-factory.bin to linux ... [e]
Connection closed by foreign host.

http://d.hatena.ne.jp/from_kyushu/20080917/1221581890

また、アップグレードしたい場合は

sysupgrade -n openwrt-ramips-mt7621-wsr-600-squashfs-sysupgrade.bin

非常に長かったが、最後はあっけなかった。お疲れ様でした。

OpenWRT wireless 設定編

OpenWRTのWiFiを設定してみる

Buffalo WSR-600DHPをOpenWRT化することができたので、早速動かしてみようと思ったが、無線機能が動いていない!?
ここからWi-Fiができるようになるまで奮闘する。
※奮闘した結果、まだ動いていませんorz....... OpenWRTの操作の参考になればと思います。 OpenWRT wiki にある通りに

#uci show wireless

uciを見ても何も表示されない。/etc/config/wirelessに設定を書く見たいだけど、そもそも/etc/config/wirelessのファイルが無かった。 これでは何もおきないはずなので、とりあえずこのファイルを作成することに。
wikiではこんな感じになってる。

#config 'wifi-device' 'wl0'
    option 'type'    'broadcom'
    option 'channel' '6's

typeとかwifi-deviceとかは自分の環境に合わせる必要がありそう。 必要そうなパッケージは入れておく。

#opkg install pciutils
#opkg install iwinfo

とりあえずルータの持っているwireless deviceを調べてみる。

root@OpenWrt:~# lspci
00:00.0 PCI bridge: Device 0e8d:0801 (rev 01)
00:01.0 PCI bridge: Device 0e8d:0801 (rev 01)
01:00.0 Network controller: Ralink corp. RT3091 Wireless 802.11n 1T/2R PCIe
02:00.0 Network controller: MEDIATEK Corp. Device 7603

チップセットは正しく認識されている様子。
「Ralink corp. RT3091 Wireless」用のモジュールをインストール

root@OpenWrt:~# opkg install kmod-rt2800-pci wireless-tools

上記でインストールされない場合は

https://downloads.openwrt.org/chaos_calmer/15.05.1/ramips/mt7621/packages/base/ からkmod-rt2800-pci_3.18.23+2015-03-09-3_ramips_1004kc.ipkをダウンロードして
同じく" opkg install ファイル名 "でインストールできるはず。
iw listコマンドでデバイスが正しく認識されていることがわかる。

root@OpenWrt:~# iw list
Wiphy phy0
        max # scan SSIDs: 4
        max scan IEs length: 2257 bytes
        Retry short limit: 7
        Retry long limit: 4
        Coverage class: 0 (up to 0m)
        Available Antennas: TX 0 RX 0
        Supported interface modes:
        .....
        .....

さらにlsmodで今まで「mac80211 381619 1 mt76pci」だったのが
「mac80211 381619 4 rt2800lib」になっていることが確認できる。
これで無線が使用できる。
はずだったが....
wireless signal がゼロになっているorz
以下デバックのために便利だったコマンド一覧

uci set wireless.@wifi-device[0].disabled=0; uci commit wireless; wifi
iw
iwconfig
iwinfo
iw dev wlan info
iw phy phy0 list
wifi up
wifi down
dmesg
logread /dev/log
uci show wireless