ルーター設定をPythonで自動化してみた

この記事は[フリューAdvent Calendar 2023](https://qiita.com/advent-calendar/2023/furyu)の18日目の記事となります。

# はじめに

ルーター設定なんて普段何度も繰り返しすることもあまりなく、そもそも自動化するほどではないのでは？と思うところではありますが、今回それが一度にたくさんしないといけない状況に陥ったため、手動でするなんて馬鹿げてる！！ってなったので、python使って自動化してみたという話です。

# まじめな背景
海外にある機械と通信する必要があり、その通信をよりセキュアな通信にしなければいけないということで、WireGuardを用いたVPN接続するという方針に決まりました。

ただし、WireGuard機能をサーバに構築し、そのまま設定するのではなく、Virtual SIMを用いてWireGuard接続に対応してくれている通信事業者の回線を使用する事になりました。

WIreGuardを、とある機器のPCにインストールするのが一般的ですが、その対応も大変だったので、WireGuard対応ルーターを見つけて使うことになりました。

そのWireGuard対応ルーターにVirtual SIM設定をして現地で設置する必要があったわけですが、このルーターを国内から持っていくにも輸出制限に引っかかるということで:scream:現地で調達＋設定する必要が出たわけです:airplane:

それが1つ2つではなく、３0個くらい用意する必要があったため、現地出張対応するにしても手動で１つセットアップするのに３０分くらいかかる。
ということは、３０分×４０台＝２０時間かかる計算になるわけで

「え？海外まで行って、ホテルに缶詰ですか？」

となりますよね。そこで通信設定担当だった方から、サービス開発担当だった私にヘルプ依頼が来ました。

「手分けしてもらえませんか？」と:sweat:

2人でやっても、半分だとして10時間…。
不毛な状況を聞いてしまっては断れるわけもない中で、普段インフラ周りの仕事をしていないので、一旦作業内容を聞いたところで

「全部webの設定画面でやるだけ？これ自動化したらいいんじゃない？」

となるわけです。

# 技術選定＋実行環境
個人的に日々自動テストとか書くのが好きでやってるのもあって、馴染みのあるseleniumを使おう＋仰々しくしたくないのでスクリプトでさくっとやりたいという安易な気持ちで以下に決定。

* MacOS
* Python 3.11.6
* selemiun
* 対象ルータ：GL.iNet GL-AR300M16

# 事前準備
* 設定ファイル（管理用の機器IDとか、それに設定したいIPアドレスなど設定に必要な情報） ※今回はエクセルで用意
* PCにGoogleCromeがインストール済であること

# ルーターの基本設定部分
設定作業の分担がそもそも基本的なルーターの管理パスワード変更だったり、FWアップグレードだったりというところを私がやるってことから依頼が始まったので、まずはそこだけ自動化しました。
```python:setup.py
#! python3

from selenium import webdriver
from selenium.webdriver.chrome.options import Options
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC
from selenium.webdriver.common.by import By
import time

# 管理画面にアクセス
url = "http://192.168.8.1/"

options = Options()
options.add_argument('--incognito')
driver = webdriver.Chrome()

driver.implicitly_wait(10) # 待機時間の設定(秒)
driver.get(url)

driver.maximize_window() # ウィンドウを最大化

# 言語を選択する
dropdown = driver.find_element(By.XPATH, '//*[@id="welcome_page"]/div[1]/div[2]/table/tr/td/div[1]/div')
dropdown.click()
japanese = driver.find_element(By.XPATH, '//*[@id="welcome_page"]/div[1]/div[2]/table/tr/td/div[1]/div/div[2]/div/ul/li[8]')
driver.execute_script("arguments[0].scrollIntoView(true);", japanese);
time.sleep(3)
japanese.click()
# 続きに進む
next = driver.find_element(By.XPATH, '//*[@id="welcome_page"]/div[1]/div[2]/table/tr/td/div[2]/button')
next.click()

# パスワードを変更する
### 新しいパスワード
new_password = driver.find_element(By.XPATH, '//*[@id="NewAdminPwd"]')
new_password.send_keys('新しいパスワード')
### 新しいパスワードの確認
new_password = driver.find_element(By.XPATH, '//*[@id="NewAdminPwdConfirm"]')
new_password.send_keys('新しいパスワード')
## 提出するボタン押下
driver.find_element(By.XPATH, '//*[@id="idNewPwdSmtBtn"]').click()

# FWアップグレード
## 「アップグレード」を選択
driver.find_element(By.XPATH, '//*[@id="navbar-ex1-collapse"]/ul/li[4]/a').click()
## 「Downlaod」を選択
driver.find_element(By.XPATH, '//*[@id="downloadFirmwareBtn"]').click()
# 10秒待機
time.sleep(10)
## 「インストール」を選択
driver.find_element(By.XPATH, '//*[@id="installBtnPanel"]/button').click()

### インストールに５分くらいかかります
#### ログイン画面に戻ってくるはず
WebDriverWait(driver, 600).until(EC.presence_of_element_located((By.XPATH, '//*[@id="inputBox"]/span/input')))

# ログインする
login_password = driver.find_element(By.XPATH, '//*[@id="inputBox"]/span/input')
login_password.send_keys("設定したパスワード")
driver.find_element("xpath", '//*[@id="LoginAdminpw"]/button').click()
time.sleep(3)

## ルーターの詳細を設定するために、OpenWrt管理画面(LuCI) の追加インストール
# 「その他の設定」を選択。
other_settings = driver.find_element(By.XPATH, '//*[@id="navbar-ex1-collapse"]/ul/li[8]/div')
other_settings.click()
# 「高級機能」を選択。
more_settings = driver.find_element(By.XPATH, '//*[@id="moresetting"]/li[10]/a')
more_settings.click()
# 「Luci インストールされていません」と表示されている。
# この画面でしばらく待つと、「インストール」を押下することができるので、押せるようになったら押す。
install = driver.find_element(By.XPATH, '//*[@id="page-wrapper"]/div[2]/div[1]/div[1]/div/div[2]/div[2]/span[1]/button')
WebDriverWait(driver, 600).until(EC.element_to_be_clickable(install))
install.click()
# ５分くらい待つと、インストールが完了する ⇛ 閉じるボタンが出るまで待機
WebDriverWait(driver, 600).until(EC.element_to_be_clickable((By.XPATH, '//*[@id="gl-modal"]/div/div[3]/button')))
# 「閉じる」を押す
driver.find_element(By.XPATH, '//*[@id="gl-modal"]/div/div[3]/button').click()

# 4.8	DNSリバインディングの設定
## 「192.168.8.1/cgi-bin/luci」のリンクをクリック。
# driver.find_element(By.XPATH, '//*[@id="page-wrapper"]/div[2]/div[1]/div[1]/div/div[2]/span[2]/a').click()
# time.sleep(3)
driver.get('http://192.168.8.1/cgi-bin/luci')
## Password に先ほど設定したパスワードを入力して「Login」を押す。
password = driver.find_element(By.XPATH, '//*[@id="maincontent"]/form/div[1]/div[2]/div/div[2]/div/input')
password.send_keys("設定したパスワード")
driver.find_element("xpath", '//*[@id="maincontent"]/form/div[2]/input[1]').click()
## 「Network」→「DHCP and DNS」を選択。
driver.find_element("xpath", '//*[@id="topmenu"]/li[3]/a').click()
driver.find_element("xpath", '//*[@id="topmenu"]/li[3]/ul/li[3]/a').click()
time.sleep(3)
## 「General Settings」の「Rebind protection」のチェックを外す。
driver.find_element("xpath", '/html/body/div[1]/div[2]/div[1]/div[2]/div/div[1]/div[7]/div/div[1]/input').click()
## 下部にある「Save & Apply」を押す。
driver.find_element("xpath", '//*[@id="view"]/div[2]/div/ul/li[1]').click()

# 無線接続をきる
# 「無線」を開く
driver.get('http://192.168.8.1/#/wlan')
## 無線をOFFにする
driver.find_element(By.XPATH, '//*[@id="gl_vue_0"]/div/label').click()
## 設定反映まで10秒まつ必要あり
time.sleep(10)

# ローカルIPの設定
time.sleep(2)
## 「その他の設定」→「LAN IP」を選択。
driver.find_element(By.XPATH, '//*[@id="navbar-ex1-collapse"]/ul/li[8]/div').click()
driver.find_element(By.XPATH, '//*[@id="moresetting"]/li[2]/a').click()
# 5秒待機
time.sleep(5)
## 以下を設定する。
### 　LAN IP： XXX.XXX.XXX.XXX
lanIP = driver.find_element(By.XPATH, '/html/body/div/div/div/div/div/div[2]/div[2]/div[1]/div[1]/div[1]/div/div[2]/div/span[1]/div/div/input')
lanIP.clear()
lanIP.send_keys("XXX.XXX.XXX.XXX")
### 　開始IPアドレス： 2
startIP = driver.find_element(By.XPATH, '/html/body/div/div/div/div/div/div[2]/div[2]/div[1]/div[1]/div[1]/div/div[2]/div/span[2]/div/div/span[2]/input')
startIP.clear()
startIP.send_keys("2")
### 　終了IPアドレス： 21
endIP = driver.find_element(By.XPATH, '/html/body/div/div/div/div/div/div[2]/div[2]/div[1]/div[1]/div[1]/div/div[2]/div/span[3]/div/div/span[2]/input')
endIP.clear()
endIP.send_keys("21")
## 「適用」を押す。
driver.find_element(By.XPATH, '//*[@id="lanipStaticBind"]/div/div[1]/button').click()

# 10秒待機
time.sleep(30)
```

プログラムを実行するとChromeが立ち上がって、いい感じに設定されていきます。
（どうしてもFWのアップグレードやOpenWrt管理画面(LuCI) の追加インストールなどで10分はかかる）

## Virtual SIMの設定
元々はここは担当ではありませんが、この際全部自動化するほうが
時間短縮じゃないか？
設定内容を手動でやる方がミスする可能性あるんじゃないか？
という気持ちになり、結局ここから先も自動化してみました。
（半分出来たところで実際の作業完了でもないですしね:sweat_smile:）
```python:wireguard_setup.py
#! python3

import sys
from selenium import webdriver
from selenium.webdriver.chrome.options import Options
from selenium.webdriver.common.by import By
import time

import pandas as pd

args = sys.argv
SETTING_KEY = args[1]

# 必要な情報を読み込み
df = pd.read_excel('./用意した設定ファイル.xlsx',header=1)
settings = df[df['管理番号']==SETTING_KEY]

IP_ADDRESS = settings['Address'].iloc[-1]
PRIVATE_KEY = settings['PrivateKey'].iloc[-1]
PUBLIC_KEY = settings['PublicKey'].iloc[-1]
ALLOWED_IPS = settings['AllowedIPs'].iloc[-1]
ENDPOINT = settings['Endpoint'].iloc[-1]

# 管理画面にアクセス
# ルーターに設定したローカルIPアドレス
url = "http://XXX.XXX.XXX.XXX/"

options = Options()
options.add_argument('--incognito')
driver = webdriver.Chrome()

driver.implicitly_wait(10) # 待機時間の設定(秒)
driver.get(url)

driver.maximize_window() # ウィンドウを最大化

# ログインする
login_password = driver.find_element(By.XPATH, '//*[@id="inputBox"]/span/input')
login_password.send_keys("設定したパスワード")
driver.find_element("xpath", '//*[@id="LoginAdminpw"]/button').click()
time.sleep(3)

# 5	バーチャルSIMの設定手順
## 5.1	WireGuardのクライアント設定
## 「VPN」→「WireGuardクライアント」を選択。
driver.find_element(By.XPATH, '//*[@id="navbar-ex1-collapse"]/ul/li[6]/div').click()
driver.find_element(By.XPATH, '//*[@id="vpn"]/li[3]/a').click()
time.sleep(3)
## 「+手動でWireGuardを設定します」を選択
driver.find_element(By.XPATH, '//*[@id="ModifyConfigs"]').click()

# 5.2	「手動入力」タブに下記の５項目を入力する。
driver.find_element(By.XPATH, '//*[@id="gl-modal"]/div/div[2]/div/ul/li[3]/a').click()
### インターフェース
## IPアドレス
ip_address = driver.find_element(By.XPATH, '//*[@id="gl-modal"]/div/div[2]/div/div/div[3]/span[1]/span[2]/span/input')
ip_address.send_keys(IP_ADDRESS)
## 秘密鍵
private_key = driver.find_element(By.XPATH, '//*[@id="gl-modal"]/div/div[2]/div/div/div[3]/span[2]/span[2]/span/input')
private_key.send_keys(PRIVATE_KEY)
### ピア
## 公開鍵
public_key = driver.find_element(By.XPATH, '//*[@id="gl-modal"]/div/div[2]/div/div/div[3]/span[6]/span[2]/span/input')
driver.execute_script("arguments[0].scrollIntoView(true);", public_key);
public_key.send_keys(PUBLIC_KEY)
## エンドポイントホスト
endpoint_host = driver.find_element(By.XPATH, '//*[@id="gl-modal"]/div/div[2]/div/div/div[3]/span[7]/span[2]/span/input')
driver.execute_script("arguments[0].scrollIntoView(true);", endpoint_host);
endpoint_host.send_keys(ENDPOINT)
## 許可されたIP
allowed_ip = driver.find_element(By.XPATH, '//*[@id="allowedIpsAnimate"]/span[2]/span/input')
driver.execute_script("arguments[0].scrollIntoView(true);", allowed_ip);
allowed_ip.send_keys(ALLOWED_IPS)

## 「続き」を押す。
driver.find_element(By.XPATH, '//*[@id="gl-modal"]/div/div[3]/button[2]').click()

# 5.3	WireGuardクライアント設定の名前を付ける
## 「続き」を押すと下記の入力画面が出てくるので「HOGEHOGE※好きな名前をつける」と入力する
name = driver.find_element(By.XPATH, '//*[@id="gl-modal"]/div/div[2]/span/span[2]/div/span/input')
name.send_keys('HOGEHOGE')
## 「追加」を押す。
driver.find_element(By.XPATH, '//*[@id="gl-modal"]/div/div[3]/button[3]').click()

# 5.4	接続する
# 「接続」を押す。
time.sleep(3)
driver.find_element(By.XPATH, '//*[@id="wgclient-status"]/div/button[1]').click()

# 5.5	DNSの設定
## 「その他の設定」→「カスタムDNSサーバー」を選択。
driver.find_element(By.XPATH, '//*[@id="navbar-ex1-collapse"]/ul/li[8]/div').click()
driver.find_element(By.XPATH, '//*[@id="moresetting"]/li[6]/a').click()
time.sleep(3)

## 「DNS再バインド（DNS Rebinding）攻撃防御」が何故かONになってしまうのでOFFにする
dns_rebinding = driver.find_element(By.CSS_SELECTOR, '#idDNSPanel > div.panel-body.panel-status > div > span:nth-child(1) > div.pull-right.btn-toggler-right > div > label > input[type=checkbox]')
if dns_rebinding.is_selected():
    driver.find_element(By.CSS_SELECTOR, '#idDNSPanel > div.panel-body.panel-status > div > span:nth-child(1) > div.pull-right.btn-toggler-right > div > label').click()

## 「手動DNSサーバー設定」を選択
driver.find_element(By.CSS_SELECTOR, '#idDNSPanel > div.panel-body.panel-status > div > span:nth-child(6) > div.pull-right.btn-toggler-right > div > label').click()
time.sleep(3)
### DNSサーバ1
dns1 = driver.find_element(By.CSS_SELECTOR, '#inputDNS1Check > span.gl-input > input[type=text]')
dns1.clear()
dns1.send_keys('DNSサーバのIPアドレス')
### DNSサーバ2
dns2 = driver.find_element(By.CSS_SELECTOR, '#inputDNS2Check > span.gl-input > input[type=text]')
dns2.clear()
dns2.send_keys('DNSサーバのIPアドレス')
## 「適用」ボタンを押下
driver.find_element(By.XPATH, '//*[@id="idApplyBtn"]').click()
time.sleep(3)
```

本来そんな設定押してないのになーみたいなところに反応しちゃうとかもあったので
その辺も状況に応じて調整する処理も入れつつ、あとはselemiumあるあるなのか、要素の待機処理が結構言う事聞かないので…sleep処理で地道な調整しつつ…っていう感じで自動化してみました。
（これだけだと処理時間3分もかからない）

# 一連の作業実行
大きく分けて上記2つの工程を踏まえて、最後にやっぱりちゃんと設定出来てるか？の動作確認は必要ですよね。ココまでくれば、当然それも自動化しておきますよねー。
というわけで、動作確認用のシェルも作りつつ、設定から確認までの流れを1つのシェルスクリプトにしました。
* 動作確認内容
```shell:check.sh
#! /bin/zsh

echo '経路の確認 ---------'
echo 'きちんと想定している経路を通ってるか確認するためのIPアドレスを指定'
ping -c1 XXX.XXX.XXX.XXX
echo $?

echo '実サービスの確認 ---------'
echo 'DNSの設定がうまくいっているか確認するためのドメイン名を指定'
ping -c1 XXXX.com
echo $?

echo 'インターネットに繋がらないことの確認 ---------'
echo '※普通にインターネット経由されたらセキュリティ強化目的の意味がなくなるので確認'
ping -c1 8.8.8.8
echo $?
```
* 最終実行形態
```shell:setup.sh
#! /bin/zsh

echo '実行環境ない人用に事前準備 ---------'
if [ -d ./setup ]; then
	source setup/bin/activate
else
	python3 -m venv setup
	source setup/bin/activate
	pip3 install -r requirements.txt ※必要なら依存関係は解決してね
fi

echo '基本設定 ---------'
./setup.py

echo 'wireguard設定 ---------'
./wireguard_setup.py $1

echo '動作確認 ---------'
./check.sh

deactivate
```

この全部まとめたシェルを以下のように実行します
```
`./setup.sh 設定するルーターの管理番号`

`例 ./setup.sh 設定ファイルに書いた情報を取るための管理番号`
```

# 実際の結果
これだけやって、1つにつき30分が15分ほどに短縮〜〜（半減）:tada:
それだけでなく、人がやらなくていいってことは環境さえあれば、余裕の並行実行〜〜〜（最高）:raised_hands:
というわけで、当初20時間くらいかかる予定だったことを実際に海外の地に赴き:airplane:
それでも紆余曲折あって…（並行稼働しすぎてホテルのDHCPのIP枯渇にぶち当たる…:expressionless:）
結果3〜4時間程度で完了。

# まとめ
やっぱり人の手でやる必要のないことはやらない。これ重要:point_left:
さらに自分はmacユーザだったけど、そもそもの担当の方はWindowsだったので、実はこれwindows環境用にしたものも用意したりもした（windowsって面倒だ…:grimacing:）

プログラムの素晴らしさに感動はしたものの、別の要因で引っかかったり、設定ファイル指定するところは手動にしてしまってたので、設定する機器との線の差し替え含め1人でPC3台同時操作は無理があった…:cry:
（結果2台のPCでマルチに並行処理させた。３台操作していたときに人間の限界を感じました…:confounded:）

もう少しどうにか出来れば、３〜４時間といわず、1〜2時間で終わらせられたんじゃないかなという欲を残しつつ
大した処理書かなくても効果は絶大だったので、今後も人の手で本当にやるの？っていうのは意識して、楽して生きていきたいと思います！