YAMAHAルーター DDNS更新用luaスクリプトを作ってみた

# はじめに

YAMAHAルーターでネットボランチDNSサービス以外のDDNSを使用したく思い、Luaスクリプトでどうにかできないか調べたところ [NVR-500でDynDNSの登録更新をしてみた](http://blog.goo.ne.jp/rabbit5151/e/91664e88e19bc310e0a5dd6e8f181afa) という記事を見つけたのでインスパイアされてみました。

以下の記録はすべて NVR700W でのものとなります。
元ネタが NVR500 向けなのでネットボランチ系のルーターであればおそらく同様に動きますが、HTTPクライアント機能に対応したバージョンのファームウエア（_RT_LUA_VERSION_NUM >= 102）を搭載している必要があります。

# 動作

ルーターの起動時にLuaスクリプトをスケジュール実行して常駐します。

syslogwatch 経由でPPPoEのlocal側 IPアドレスを監視し、変動があれば取得したIPアドレスでDDNSを更新します（IPアドレスが同じであれば更新しません）。DDNSサービスによっては長期間更新がないとホストの登録を抹消されることがあるので

`WatchInterval * UpdateInterval（秒）`

で設定された期間、IPアドレスが変更されない場合は強制的に更新を行います。
処理結果を syslog に出力します。

# 使用方法

雛形を `ddns-update.lua` 等のファイル名でルーターにアップロードしてください。
/lua  のようにルート直下にディレクトリを作成しておくとアクセスしやすいです。

ルーターにコンソールでログインして動作確認を行ってください。
Luaスクリプトが問題なく実行され、DDNSが更新されるのを確認できたらLuaスクリプトをスケジュール登録します。

```bash
schedule at XXX startup * lua /lua/ddns-update.lua
```

`XXX` の部分にはスケジュールIDを適宜指定してください。
（既にスケジュールの登録がある場合は上書きに注意してください）

ルーターを再起動後にLuaスクリプトが動いていることを確認してください。

```bash
show status lua
```

で、動作状況と履歴を確認できます。
スケジュール登録したLuaスクリプトが running 状態のままであれば成功です。

# テンプレート

雛形はNO-IP用に記述されていますが、 UpHost を書き換えることでそのままDynDNSにも使用できます。他のDDNSサービス向けに使用する場合は、各サービスに応じた UpType / UpHost / UpUrl パラメータを記述してください。

```lua
--[[

DDNSのIPアップデート

*** YAMAHAルーター専用 ***

PPPoEで設定されたlocal側のIPアドレスを取得し、既存IPアドレスと異なれば取得したIPアドレスでDDNSを更新する。
IPアドレスが同じであれば更新はしない。WatchInterval * UpdateInterval（秒）で設定された期間更新されない場合、
強制的にIPアドレスを更新する。

※下記サンプルはNO-IP用に記述されているが、UpHostを書き換えることでそのままDynDNSにも使用可能。
　他のDDNSサービスに使用する場合は、サービスに応じたUpType/UpHost/UpUrlパラメータを記述すること。

]]

----------------------## 設定 ここから ##----------------
-- debug（tru | false で指定）
debug    = false

-- DDNS service user account
username = "Enter your username"
password = "Enter your password"
ddnshost = "Enter your ddns hostname"

-- IP取得するPP番号
pp_num   = 1

-- 実行指定時間（hh:mm）
schedule = "01:00"

-- 実行間隔秒数と強制アップデート実施カウンターのしきい値
--
-- IP取得リトライ回数と間隔（秒数）
interval_retrypp = 3
interval_timespp = 10

-- syslogwatch time 86400/day, max 864000(10days)
-- interval_watch * interval_update
interval_watch  = 86400
interval_update = 7

-- syslogwatch タイマー誤差の補正値（NVR500の実績でNVR700Wでは不要の模様）
-- for NVR500 (-90秒)
--correction_time = -90
-- for NVR700W (補正不要)
correction_time = 0

-- DDNS update api address
--
-- UpUrl の末尾にIPが自動付加されるのでパラメータ構成に注意
-- ポート指定が必要な場合は UpHost パラメータを "FQDN:8080" 形式で記述のこと。
UpType = (_RT_LUA_VERSION_NUM >= 108) and "https" or "http"
UpHost = "dynupdate.no-ip.com"
UpUrl  = UpType .. "://" .. UpHost .. "/nic/update?hostname=" .. ddnshost .. "&myip="

----------------------## 設定 ここまで ##----------------


------------------------------------------------------------
-- syslog出力関数 --
------------------------------------------------------------
function putSyslog(msg)
  rt.syslog("info", "[LUA] ddns-update.lua DDNS " .. msg)
end

------------------------------------------------------------
-- 指定した時間までの秒数を返す関数 --
------------------------------------------------------------
function getDiffTime(t_dest)
  local t_adjust, t_diff
  local tmp  = {string.split(t_dest, /:/)}
  local now  = os.time()
  local time = os.date("*t", now)

  time.hour = tmp[1]
  time.min  = tmp[2]
  time.sec  = 0

  tmp = os.time(time)

  if os.difftime(tmp, now) <= 60 then
    time = os.date("*t", tmp + interval_watch)
  end

  t_diff   = os.difftime(os.time(time), now)
  t_adjust = (t_diff * correction_time) / interval_watch

  return (t_diff + t_adjust)
end

------------------------------------------------------------
-- 指定されたPPの local IP 取得関数 --
------------------------------------------------------------
function getLocalIp(id)
  local rtn, str, ipadr
  local cmd = "show status pp " .. tostring(id)
  local ptn = "PP IP Address Local:%s+(%d+%.%d+%.%d+%.%d+)"

  rtn, str = rt.command(cmd)

  if (rtn) and (str) then
    ipadr = str:match(ptn)

    if (ipadr == nil) then
      rtn   = false
      ipadr = "PPP not linked up"
    end
  else
    rtn   = false
    ipadr = cmd .. "Can not obtain IPaddr (command exec failure: " .. cmd .. ")\r\n"
  end

  return rtn, ipadr
end

------------------------------------------------------------
-- IPアドレスの変化検出関数 --
------------------------------------------------------------
function isNew(ip)
  local blip
  blip = os.getenv("GLOBALIP")

  if (blip) then
    if (blip == ip) then
      return false
    else
      rt.command("set GLOBALIP=" .. ip)
      return true
    end
  else
    rt.command("set GLOBALIP=" .. ip)
    return true
  end
end

------------------------------------------------------------
-- DDNS Update 関数 --
------------------------------------------------------------
function UpdateDDNS(ip)
  local url = UpUrl .. ip
  local resp_table
  local req_table = {
    url       = url,
    method    = "GET",
    auth_type = "basic",
    auth_name = username,
    auth_pass = password
  }

  putSyslog("update url = " .. url)
  resp_table = rt.httprequest(req_table)

  if (resp_table.rtn1) then
    putSyslog("new IP = " .. ip)
  else
    putSyslog("update failed - " .. "\nrtn1-> " .. tostring(resp_table.rtn1) .. "\nrtn2-> " .. tostring(resp_table.rtn2) .. "\nerr-> " .. tostring(resp_table.err) .. "\ncode-> " .. tostring(resp_table.code) .. "\nheader-> " .. tostring(resp_table.header) .. "\nbody-> " .. tostring(resp_table.body))
  end
end


------------------------------------------------------------
-- main --
------------------------------------------------------------
local rtn, str, lip, cnt, sec
cnt = 0

putSyslog("updater starting (lua = " .. _RT_LUA_VERSION .. " , using " .. UpType .. ") ...")
rtn, str = getLocalIp(pp_num)

-- IP取得できない場合（リンクアップ待ち、等）は指定の回数・間隔でリトライ
if (not rtn) then
  cnt = interval_retrypp

  repeat
    putSyslog("try to specify target ... retry " .. math.abs(cnt - interval_retrypp - 1))
    rt.sleep(interval_timespp)
    rtn, str = getLocalIp(pp_num)
    cnt = cnt - 1
  until (rtn or cnt < 1)
end

if (rtn) then
  putSyslog("target is (if = " .. string.format("PP[%02d]:", pp_num) .. str .. ")")
  lip = str

  if (isNew(lip)) then
    if (not debug) then UpdateDDNS(lip) else print("-- UpdateDDNS(lip)") end
  end
else
  putSyslog("target specify failure. aborted: " .. str)
  os.exit(1)
end

cnt = 0
sec = getDiffTime(schedule)
putSyslog("next ip-check is after " .. sec .. " seconds")

while (true) do
  local rtn, str, lip, gip
  rtn, str = rt.syslogwatch(string.format("PP%%[%02d%%]", pp_num) .. " PPP/IPCP up%s+%(Local:%s+(%d+%.%d+%.%d+%.%d+)", 1, sec)

  if (rtn == 0) then
    gip = os.getenv("GLOBALIP") or ""
    cnt = cnt + 1

    if (cnt == interval_update) then
      cnt = 0
      putSyslog("force update, new ip = " .. gip)

      if (not debug) then
        UpdateDDNS(gip)
      else
        print("Skip counter = " .. tostring(interval_update) .. ", DDNS update ip = " .. gip)
      end
    end
  else
    lip = string.match(str[rtn], "Local:%s+(%d+%.%d+%.%d+%.%d+)")
    putSyslog("detected new ip and update (old/new ip): " .. gip .. "/" .. lip)

    if (isNew(lip)) then
      if (not debug) then
        UpdateDDNS(lip)
      else
        print("DDNS update called in while loop\n")
      end

      cnt = 0
    end
  end

  sec = getDiffTime(schedule)
  putSyslog("next ip-check is after " .. sec .. " seconds")

end
```

# 参考情報

[Lua スクリプト機能 - RTpro](http://www.rtpro.yamaha.co.jp/RT/docs/lua/)
[スケジュールの設定 - RTpro](http://www.rtpro.yamaha.co.jp/RT/manual/nvr700w_nvr510/schedule/schedule_at.html)