ルーターからクラウドを操る

いかにも釣りっぽいタイトルで申し訳ないとしか言いようがないのですが、YAMAHA RTXシリーズからAWSAPIを叩く実験をしてみたので、そのメモです。

YAMAHAルーターLuaスクリプティング機能

最近のYAMAHAのファームにはLuaスクリプティング機能が付いていて、定期的にconfigをダウンロードして適用させたり、時間帯に応じてルーティングを変えたりといった柔軟な運用ができます。詳しくは本家のサイトに解説されています。この機能を使ってAWSAPIを叩けたらいろいろなことができるんじゃないかと思って、POC的に実験してみました。

YAMAHAファームが準拠しているのはLua 5.1.4ということになっていますが、フル仕様ではなくサブセットのみが実装されているので、やや工夫が必要でしたが、とりあえずDescribeSecurityGroupsでセキュリティ・グループのリストを取得するスクリプトを書いてみました。

AWS REST APIシグネチャ計算

HMAC-SHA1の計算とBASE64エンコーディングが必要になるので、以下の2カ所から先達のスクリプトを探してきました。
SHA-1 and HMAC-SHA1 Routines in Pure Lua (by Jeffrey Friedl. Public Domain)
Base Sixty Four (by Alex Kloss. LGPL2)

AWSのドキュメント(Making Query Requests)に従ってクエリを組み立てて、シグネチャを計算します。

access_key = "YOUR_ACCESS_KEY_ID"
secret_key  = "YOUR_SECRET_ACCESS_KEY"
api             = "DescribeSecurityGroups"

api_version   = "2011-07-15"
host              = "ec2.amazonaws.com"
http_method = "GET"

--ISO8601でタイムスタンプを取得
time_stamp_raw = os.date("!%Y-%m-%dT%H:%M:%S.000Z")
time_stamp        = string.gsub(time_stamp_raw, ":", "%%3A")

--アクセスキー以外の引数を昇順に並べたクエリを組み立てる
s2s = http_method .. "\n" .. host .. "\n/\nAWSAccessKeyId=" .. access_key .. "&Action=" .. api .. "&SignatureMethod=HmacSHA1&SignatureVersion=2&Timestamp=" .. time_stamp .. "&Version=" .. api_version

--HMAC-SHA1の計算結果をBASE64エンコーディングしてシグネチャを生成
signature = base64(hex_to_binary(hmac_sha1(secret_key, s2s)))

なお、オリジナルのHMAC-SHA1の計算ロジックをYAMAHALuaの制約に合わせて修正する必要がありました。引っかかった制約は、

  1. 数値が整数のみで浮動小数点数が扱えない
  2. Mathライブラリのmodf関数が実装されていない

の2点でした。

今回はクエリの文字数が一定であれば計算結果が変わらない部分だったので、計算リソースの節約の意味も兼ねて、結果をハードコーディングすることで回避しました。その他のAPIも同じスクリプトで扱うようにする場合は、APIコールと計算結果をtableで関連づけるような実装になるかと思われます。(Luaにおけるtableは、RubyのHashのようなものです。)

--浮動小数点数とmath.modfはYAMAHA Luaで実装されていない
--local B1, R1 = math.modf(msg_len_in_bits  / 0x01000000)
--local B2, R2 = math.modf( 0x01000000 * R1 / 0x00010000)
--local B3, R3 = math.modf( 0x00010000 * R2 / 0x00000100)
--local B4       = 0x00000100 * R3

--EC2 DescribeSecurityGroupsコールのためのdirty hack
if msg_len_in_bits == 2040 then
    B1, B2, B3, B4 = 0, 0, 7, 248
elseif msg_len_in_bits == 672 then
    B1, B2, B3, B4 = 0, 0, 2, 160
end

ちなみに、浮動小数点数さえ使えれば、math.modfを実装するのは簡単で、以下のようになるはずです。

--math.modf
function modf_(n)
    if n > 0 then
        f = n % 1
    else
        f = n % -1
    end
    i = n - f
    return i, f
end

HTTPリクエストの発行と結果の取得

YAMAHAルーターLuaでは、configの操作など、標準Lua以外のライブラリが実装されています。そのうちの一つであるrt.httprequestを使ってEC2エンドポイントにAPIコールを投げます。rt.httprequestでは結果のボディをファイルに書き込むことができるので、以下の例ではルーターの内蔵メモリに書き込んでいます。そのほか、機種によって差がありますが、ルーターに挿入したmicroSDやUSB接続されたストレージにファイルを書き込むことができます。

--シグネチャをつかってクエリを組み立てる
query = "Action=" .. api .. "&AWSAccessKeyId=" .. access_key .. "&Version=" .. api_version ..  "&Timestamp=" .. time_stamp_raw .. "&Signature=" .. signature .. "&SignatureVersion=2&SignatureMethod=HmacSHA1"

--rt.httprequestではSSLは未実装
req_url = "http://" .. host .. "/?" .. query

--リクエスト・パラーメータを格納したテーブルを作成してGETを発行
--rsp_t.codeでHTTPステータスコード、rsp_t.errでエラーメッセージを確認可能
file_path = "/sg_out.txt"
req_t      = {url=req_url, method="GET", save_file=file_path}
rsp_t      = rt.httprequest(req_t)

実行結果の確認

  1. tftpでスクリプトルーターにアップロード
  2. ルータースクリプトを実行
  3. 結果のファイルをローカルにダウンロードして確認

という手順で動作確認を行います。

Macですが以下のようにtftpでファイルをアップロードしました。直後にgetを行うため、個人的にはインタラクティブ・モード(?)が使いやすいと感じました。ROUTER_IP_ADDRESSとROUTER_ADMIN_PASSORDの部分は、それぞれ適切な値に置き換えてください。

$ tftp 
tftp> connect ROUTER_IP_ADDRESS
tftp> put desc_sg.lua /desc_sg.lua/ROUTER_ADMIN_PASSWORD
Sent 34235 bytes in 0.6 seconds

ルーターLuaスクリプトを実行します。administratorで実行している点に注意。

# lua desc_sg.lua 

結果をローカルPCにダウンロードして内容を確認します。

$ tftp 
tftp> connect ROUTER_IP_ADDRESS
tftp> get /sg_out.txt/ROUTER_ADMIN_PASSWORD sg_out.txt
Received 2130 bytes in 0.1 seconds
tftp> quit
$ cat sg_out.txt
846
<?xml version="1.0" encoding="UTF-8"?>
<DescribeSecurityGroupsResponse xmlns="http://ec2.amazonaws.com/doc/2011-07-15/">
    <requestId>########-####-####-####-############</requestId>
    <securityGroupInfo>
        <item>
            <ownerId>
  (以下省略)

制約とハマリどころ

繰り返しになる内容も多いですが、YAMAHA Lua上でAWS APIを叩く上で制約となりそうな点を列挙します。MacLinux用のものとは言語仕様やライブラリの実装状況に差があるので、こまめに実機でテストするのが無難だと感じました。自分はRuby (Sinatra) でスタブを書いて、デバッグを行いました。

  1. 浮動小数点が使えない
  2. 一部関数が実装されていない
  3. URL末尾に'/'(スラッシュ)がないとinvalid URLとなってリクエストが飛ばない
  4. URLが255文字までに制限されている。そのため、GETで叩けるAPIコールは制限が厳しい。
  5. rt.httprequestは勝手にURLエンコーディングしてくれるので、"="を"%3D"に置き換えておくと、さらに"%"をエスケープして"%253D"に変換されてしまうのでシグネチャが一致しないことになる。
  6. ボディの大きさにも制限がある。
  7. SSLが使えないので、Access Key IDやクエリのパラーメーターが平文で飛んでしまう。

まとめ

YAMAHAルーター上のLuaスクリプティング機能を使ってEC2 APIの1つであるDescribeSecurityGroupを発行することができました。色々と制約はありますが、スクリプトからVPCの作成とconfigまでできたりしてしまうとおもしろいかもしれないと感じました。

なお、Credentials (Access Key IDなど) の扱いには注意する必要があるかと思われます。rt.httprequestの制限によってSSLが使えないこともありますが、実際にはIAMを使って権限に制約をつけた鍵を利用するのがいいかと思われます。

(言わずもがなですが、私の勤め先などは本記事の内容に一切関知していません。)

gistに貼っておきました → https://gist.github.com/1174442