「Office 365」と「Windows Update」と「G Suite」のインターネットブレイクアウト + インターネット回線を2本利用

管理番号:YMHRT-18635 
(最終更新日: 2023/7/26)

本設定例では、Luaスクリプト機能とFQDNフィルター機能を使用しています。

Luaスクリプト機能の対応機種は、RTX5000RTX3510RTX3500RTX1300RTX1220RTX1210RTX830NVR700WNVR510vRXです。
FWX120は、一部の機能に対応していないため、対応機種から除いています。

FQDNフィルター機能の対応機種は、RTX5000(Rev.14.00.26以降)、RTX3510RTX3500(Rev.14.00.26以降)、RTX1300RTX1220RTX1210(Rev.14.01.26以降)、RTX830(Rev.15.02.03以降)、NVR700W(Rev.15.00.10以降)、NVR510(Rev.15.01.13以降)、FWX120(Rev.11.03.25以降)、vRXです。

構成図

トラフィックが増加する中、インターネット回線が1本だけの場合、輻輳を招く恐れがあります。クラウドアプリケーションやWindows Update だけ、別のインターネット回線でアクセス(インターネットブレイクアウト)することで、輻輳を回避して負荷を軽減できます。
本設定例では、Luaスクリプトを利用した、Office 365、Windows Update、G Suite のインターネットブレイクアウトをご紹介します。

ルーターでサンプルLuaスクリプトを実行すると、以下のように動作します。
  (1) ネットワーク経由で、マイクロソフト社が公開しているOffice 365のURLリストを取得
  (2) Office 365のURLリストのバージョンを、Luaスクリプトで記憶
  (3) 取得したリストに記載されているFQDN・IPアドレスから、Office 365用のフィルターをルーターに定義
  (4) マイクロソフト社が公開しているWindows UpdateのFQDN(固定)から、Windows Update用のフィルターをルーターに定義
  (5) Google社が公開しているG SuiteのFQDN(固定)から、G Suite用のフィルターをルーターに定義
  (6) ルーターに経路を設定 (フィルター型ルーティング)
  (7) 定期的にOffice 365のURLリストのバージョンを確認し、(2) のバージョンと比較
  (8) バージョンが上がっていた場合、(3)~(6) を実施

※ Office 365 および Windowsは、米国 Microsoft Corporation の米国およびその他の国における登録商標または商標です。
※ G Suiteは、Google LLCの登録商標または商標です。

光回線に接続するためには、別途ONUが必要です。

対応機種のうち、設定例を掲載している機種は、以下のとおりです。

機種 掲載内容 備考
RTX1300 RTX1220 RTX1210 コマンド設定例
Luaスクリプト例
Luaスクリプト機能、FQDNフィルター機能

事前準備

1. ルーターコンソールを開きます。

[解説]
ルーターコンソールはシリアルケーブルやtelnetで接続して使用します。
使用方法については取扱説明書をご参照ください。

2. 管理者権限でログインして、EMFSファイルを作成します。

[ルーターコンソール]
# embedded file (Luaスクリプトファイル名) <<EOF


: Luaスクリプト(本体を貼り付ける)


EOF
[解説]
コマンドの詳細はEMFSファイルの作成、削除をご参照ください。
Luaスクリプトファイル名は、任意の文字列を設定してください。

3. Luaスクリプトがルーターに作成されたことを確認します。

[ルーターコンソール]
# show file list emfs:/
2019/06/21 14:42:24 12742 (Luaスクリプトファイル名)
[解説]
コマンドの詳細はファイル情報の一覧の表示をご参照ください。

ルーターの設定例

※ネットワーク機器を安全にお使いいただくために、管理パスワードの変更を奨励します。

ゲートウェイの設定 ip route default gateway pp 1
LANインターフェースの設定
(LAN1ポートを使用)
ip lan1 address 192.168.100.1/24
WANインターフェースの設定1
(LAN2ポートを使用)
pp select 1
pp always-on on
pppoe use lan2
pp auth accept pap chap
pp auth myname (ISP1に接続するID) (ISP1に接続するパスワード)
ppp lcp mru on 1454
ppp ipcp ipaddress on
ppp ipcp msext on
ppp ccp type none
ip pp secure filter in 1020 1030 2000
ip pp secure filter out 1010 1011 1012 1013 1014 1015 3000 dynamic 100 101 102 103 104 105 106
ip pp nat descriptor 1
pp enable 1
WANインターフェースの設定2
(LAN3ポートを使用)
pp select 2
pp always-on on
pppoe use lan3
pp auth accept pap chap
pp auth myname (ISP2に接続するID) (ISP2に接続するパスワード)
ppp lcp mru on 1454
ppp ipcp ipaddress on
ppp ipcp msext on
ppp ccp type none
ip pp nat descriptor 2
pp enable 2
フィルターの設定 ip filter 1010 reject * * udp,tcp 135 *
ip filter 1011 reject * * udp,tcp * 135
ip filter 1012 reject * * udp,tcp netbios_ns-netbios_ssn *
ip filter 1013 reject * * udp,tcp * netbios_ns-netbios_ssn
ip filter 1014 reject * * udp,tcp 445 *
ip filter 1015 reject * * udp,tcp * 445
ip filter 1020 reject 192.168.100.0/24 *
ip filter 1030 pass * 192.168.100.0/24 icmp
ip filter 2000 reject * *
ip filter 3000 pass * *
ip filter dynamic 100 * * ftp
ip filter dynamic 101 * * www
ip filter dynamic 102 * * domain
ip filter dynamic 103 * * smtp
ip filter dynamic 104 * * pop3
ip filter dynamic 105 * * tcp
ip filter dynamic 106 * * udp
NATの設定 nat descriptor type 1 masquerade
nat descriptor type 2 masquerade
DHCPの設定 dhcp service server
dhcp server rfc2131 compliant except remain-silent
dhcp scope 1 192.168.100.2-192.168.100.100/24
DNSの設定 dns host lan1
dns server select 5001 pp 1 any . restrict pp 1
dns server select 5002 pp 2 any . restrict pp 2
dns private address spoof on
Luaスクリプトのスケジュール設定 schedule at 1 startup * lua emfs:/(Luaスクリプトファイル名)

Luaスクリプト例

設定値

-- コマンドパラメーター --
-- デフォルト経路に使用するインターフェース
gateway_default = "pp 1"
-- Office 365、Windows Update、G suiteに使用するインターフェース
gateway_offload = "pp 2"

-- FQDNフィルターに使用するフィルターIDのプレフィックス
filter_prefix = 2000000

-- Office 365 インターネットブレイクアウトのSYSLOGのプレフィックス
offload_prefix = "[O365 Offload]"

-- サーバーへの問い合わせ間隔(秒)
idle_time = 3600 -- 1時間

-- ルーター起動時の PPPoE 接続の待機時間(秒)
check_connection_time = 10 -- 10秒

-- ルーター起動時の PPPoE 接続の確認回数(回) check_connection_count = 5 -- 5回

-- HTTPS リクエスト --
-- ホスト名
office_host = "endpoints.office.com"

-- パス
path_version = "version/worldwide?clientrequestid=%s&Format=CSV"
path_endpoint = "endpoints/worldwide?clientrequestid=%s&Format=CSV&NoIPv6=true"

-- url
url_template = "https://%s/%s"

-- リクエストテーブル
req_tbl = {
 url = "",
 method = "GET"
}

-- Windows Update の接続先URLリスト(テーブル)
urls_wu = {"update.microsoft.com", "*.update.microsoft.com", "download.windowsupdate.com", "*.download.windowsupdate.com", "download.microsoft.com", "*.download.microsoft.com", "windowsupdate.com", "*.windowsupdate.com", "ntservicepack.microsoft.com", "login.live.com", "mp.microsoft.com", "*.mp.microsoft.com", "*.do.dsp.mp.microsoft.com", "*.dl.delivery.mp.microsoft.com", "*.emdl.ws.microsoft.com"}
-- インターネットブレイクアウトしない場合は、下の行のコメント"--"を外す ★
--urls_wu = {}

-- G Suite の接続先URLリスト(テーブル)
urls_gs = {"accounts.google.com", "www.googleapis.com", "oauth2.googleapis.com", "*.googleapis.com", "www.google.com", "apps-apis.google.com", "accounts.youtube.com", "fonts.gstatic.com", "ssl.gstatic.com", "www.gstatic.com", "classroom.googleapis.com", "people.googleapis.com", "sheets.googleapis.com", "slides.googleapis.com", "gsuite.google.com", "mail.google.com", "calendar.google.com", "chat.google.com", "meet.google.com", "drive.google.com", "cloud.google.com", "docs.google.com", "sites.google.com", "developers.google.com", "keep.google.com", "jamboard.google.com", "admin.google.com", "ediscovery.google.com"}
-- インターネットブレイクアウトしない場合は、下の行のコメント"--"を外す ★
--urls_gs = {}

-- 出力する SYSLOG のレベル(info, debug, notice)
log_level = "(SYSLOGレベル)"

-- メッセージ --
msg_fail_ver = "Failed to get version from server"
msg_fail_server_http = "Failed to data transmission to server, HTTP STATUS: %d"
msg_fail_server_err = "Failed to data transmission to server, err: %s"
msg_fail_server = "Failed to data transmission to server"
msg_fail_get_url = "Failed to get urls"
msg_no_pppoe_connection = "PPPoE not connected"
msg_new_ver = "New version of service instance endpoints has been detected"
msg_cancel = "Service instance endpoints checking is canceled"
msg_update = "Service instance endpoints have been successfully updated"

ログメッセージをSYSLOGに出力する関数

function write_to_log(msg_fmt, ...)
 local log_msg

 log_msg = string.format(msg_fmt, ...)
 log_msg = string.format("%s %s", offload_prefix, log_msg)
 rt.syslog(log_level, log_msg)
end

UUIDを生成する関数

function generate_uuid()
 local template = "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx"

 math.randomseed(os.time())

 return string.gsub(template, "[xy]", function (c)
  local v = (c == "x") and math.random(0, 0xf) or math.random(8, 0xb)
  return string.format("%x", v)
 end)

end

Officeサーバーからバージョンを取得する関数

function get_version_from_server(uuid)
 local path, v, cur_ver, res_tbl
 local ptn = "%w+,%w+,%w+%s+%w+,(%w+),%w+"
 local cur_ver = 0

 path = string.format(path_version, uuid)
 req_tbl.url = string.format(url_template, office_host, path)
 res_tbl = rt.httprequest(req_tbl)

 if (res_tbl.rtn1) and (res_tbl.code) and (res_tbl.code == 200) then
  -- HTTPレスポンスを正常に受信できた
  v = res_tbl.body:match(ptn) -- バージョンを取得
  if (v) then
   v = tonumber(v)  -- 数値に変換
   if (v) then
    cur_ver = v -- 数値変換に成功した場合、cur_verに保存
   end
  end
  if (cur_ver == 0) then
   write_to_log(msg_fail_ver)
  end
 else
  if (res_tbl.code) then
   write_to_log(msg_fail_server_http, res_tbl.code)
  elseif (res_tbl.err) then
   write_to_log(msg_fail_server_err, res_tbl.err)
  else
   write_to_log(msg_fail_server)
  end
 end

 return cur_ver
end

Officeサーバーからエンドポイントを取得する関数

function get_endpoints_from_server(uuid)
 local res_tbl, path, body

 path = string.format(path_endpoint, uuid)
 req_tbl.url = string.format(url_template, office_host, path)
 res_tbl = rt.httprequest(req_tbl)

 if (res_tbl.rtn1) and (res_tbl.code) and (res_tbl.code == 200) then
  -- 送信成功
  if is_transenc_chunked(res_tbl.header) then
   body = res_tbl.body:gsub(/\r\n[0-9a-fA-F]+\r\n|^[0-9a-fA-F]+\r\n/, "")
   return body
  else
   return res_tbl.body
  end
 else
  if (res_tbl.code) then
   write_to_log(msg_fail_server_http, res_tbl.code)
  elseif (res_tbl.err) then
   write_to_log(msg_fail_server_err, res_tbl.err)
  else
   write_to_log(msg_fail_server)
  end
 end

 return nil
end

CSV形式の1行をテーブルに変換する関数

function ParseCSVLine(line)
 local res = {}
 local pos = 1
 local sep = ','
 while (true) do
  local c = string.sub(line,pos,pos)
  if (c == "") then break end
  if (c == '"') then
   -- quoted value (ignore separator within)
   local txt = ""
   repeat
    local startp,endp = string.find(line,'^%b""',pos)
    txt = txt..string.sub(line,startp+1,endp-1)
    pos = endp + 1
    c = string.sub(line,pos,pos)
    if (c == '"') then txt = txt..'"' end
    -- check first char AFTER quoted string, if it is another
    -- quoted string without separator, then append it
    -- this is the way to "escape" the quote char in a quote. example:
    -- value1,"blub""blip""boing",value3 will result in blub"blip"boing for the middle
   until (c ~= '"')
   table.insert(res,txt)
   assert(c == sep or c == "")
   pos = pos + 1
  else
   -- no quotes used, just look for the first separator
   local startp,endp = string.find(line,sep,pos)
   if (startp) then
    table.insert(res,string.sub(line,pos,startp-1))
    pos = endp + 1
   else
    -- no separator found -> use rest of string and terminate
    table.insert(res,string.sub(line,pos))
    break
   end
  end
 end

 return res
end

チャンク転送であるか否かを取得する関数

function is_transenc_chunked(hdr)
 ptn = "Transfer%-Encoding%:%s+chunked"

 if hdr:match(ptn) then
  return true
 end

 return false
end

レスポンスデータからURLリストを取得する関数

function get_urls(body)
 local lines, line
 local i, v
 local url_num = 0
 local cat_num = 0
 local ip_num = 0
 local urls_list = {}

 if (body ~= nil) then
  lines = { string.split(body, /\v+/) }
  for i, v in ipairs(lines) do
   line = ParseCSVLine(v)
   -- 1行目のパラメーター名から、urls と category の位置を取得
   if (i == 1) then
    for i, v in ipairs(line) do
     if (v == "urls") then
      url_num = i
     elseif (v == "category") then
      cat_num = i
     elseif (v == "ips") then
      ip_num = i
     end
    end
    if (url_num == 0) or (cat_num == 0) or (ip_num == 0) then
     break
    end
   else
    if (line[cat_num] == "Allow") or (line[cat_num] == "Optimize") then
     -- urls を保存
     if (line[url_num] ~= nil) and (string.len(line[url_num]) > 0) then
      table.insert(urls_list, line[url_num])
     elseif (line[ip_num] ~= nil) and (string.len(line[ip_num]) > 0) then
      table.insert(urls_list, line[ip_num])
     end
    end
   end
  end
 end

 return urls_list
end

URLリストから重複を削除、ソートする関数

function del_duplicate_urls(urls_list)
 local url_splited = {}
 local flist = {}
 local flist_temp = {}
 local urls, url
 local k, v
 local str_urls = ""

 for urls in each(urls_list) do
  url_splited = { string.split(urls, ",") }
  for url in each(url_splited) do
   flist_temp[url] = 1
  end
 end

 for k, v in pairs(flist_temp) do
  table.insert(flist, k)
 end

 table.sort(flist)

 return flist
end

URLのリストを一定の条件で連結する関数

function concatenate_urls(urls_list)
 local url
 local str_urls = ""
 local flist = {}
 local urls_len = 256 -- 文字列の長さの閾値(0~)

 for url in each(urls_list) do
  if (str_urls == "") then
   str_urls = url
  else
   str_urls = str_urls .. "," .. url
  end

  if (string.len(str_urls) > urls_len) then
   table.insert(flist, str_urls)
   str_urls = ""
  end
 end

 if (str_urls ~= "") then
  table.insert(flist, str_urls)
 end

 return flist
end

FQDNフィルターを削除する関数

function exec_no_filter_cmd(num)
 local cmd, i

 for i = 0, (num - 1) do
  cmd = string.format("no ip filter %d", filter_prefix + i)
  rtn, str = rt.command(cmd)
  if (rtn == false) then
   if (str) then
    write_to_log(str)
   end
  end
 end

end

2つのリストを結合する関数

function joint_tables(table_1, table_2)
 local table_new = {}

 for v in each(table_1) do
  table.insert(table_new, v)
 end

 for v in each(table_2) do
  table.insert(table_new, v)
 end

 return table_new
end

FQDNフィルターを設定する関数

function exec_filter_cmd(urls_list)
 local v, cmd, rtn, str
 local i = 0;
 local flist = {}

 for v in each(urls_list) do
  cmd = string.format("ip filter %d pass * %s", filter_prefix + i, v)
  rtn, str = rt.command(cmd)
  if (rtn == false) then
   if (str) then
    write_to_log(str)
   end
   break
  else
   table.insert(flist, tostring(filter_prefix + i))
   i = i + 1;
  end
 end

 return flist
end

ip routeコマンドを設定する関数

function exec_ip_route_cmd(flist)
 local cmd, v, rtn, str
 local buf = ""

 if (#flist == 0) then
  return
 end

 for v in each(flist) do
  buf = buf .. " " .. v
 end

 cmd = string.format("ip route default gateway %s filter %s gateway %s", gateway_offload, buf, gateway_default)
 rtn, str = rt.command(cmd)
 if (rtn == false) then
  if (str) then
   write_to_log(str)
  end
 end

end

メインルーチン

local buf, cur_ver
local urls
local filters = {}
local pre_ver = 0
local err = 0
local count
local cmd_pppoe = "show status "..gateway_default.." | grep \"接続されています\""
local rtn_pppoe
local str_pppoe

-- PPPoE 接続を待つ
count = 0
while (count < check_connection_count) do
 rtn_pppoe, str_pppoe = rt.command(cmd_pppoe, "off")
 if (str_pppoe ~= nil) then
  break
 end
 count = count + 1
 rt.sleep(check_connection_time)
end

-- PPPoE 接続していない場合に、エラーメッセージを出力し、サーバーへの問い合わせ間隔分待機する
if (count >= check_connection_count) then
 write_to_log(msg_no_pppoe_connection)
 rt.sleep(idle_time)
end

-- UUIDを生成
local uuid = generate_uuid()

while (true) do

 -- Officeサーバーからバージョンを取得
 cur_ver = get_version_from_server(uuid)

 -- 保存したバージョンと比較
 -- 新たに取得したバージョンが新しければ、設定を更新
 if (cur_ver > pre_ver) then
  err = 1
  write_to_log(msg_new_ver)

  -- Officeサーバーからエンドポイントを取得
  buf = get_endpoints_from_server(uuid)
  if (buf ~= nil) then
   -- URLリストを取得
   urls = get_urls(buf)

   if (urls ~= nil) then
    -- URLリストから重複を削除
    urls = del_duplicate_urls(urls)

    if (urls ~= nil) then
     -- Windows UpdateとOffice 365のURLのリストを結合する
     urls = joint_tables(urls_wu, urls)

     -- G SuiteのURLのリストを結合する
     urls = joint_tables(urls_gs, urls)

     -- URLのリストをフィルター単位に分割する
     urls = concatenate_urls(urls)

     -- FQDNフィルターを削除
     exec_no_filter_cmd(#filters)

     -- FQDNフィルターを設定
     filters = exec_filter_cmd(urls)

     if (filters ~= nil) then
      -- 静的経路を設定
      exec_ip_route_cmd(filters)

      -- コンフィグを保存
      rt.command("save")

      err = 0
     end
    end
   end
  end
 end

 if (cur_ver == 0) or (err == 1) then
  write_to_log(msg_cancel)
 elseif (cur_ver > pre_ver) then
  pre_ver = cur_ver
  write_to_log(msg_update)
 end

 rt.sleep(idle_time)
end

【ご注意】
本設定例は、設定の参考例を示したもので、動作を保証するものではございません。
ご利用いただく際には、十分に評価・検証を実施してください。

メール

ご相談・お問い合わせ