NTTPCの激安VPS「Indigo」のAPIで遊んでみる

#はじめに
NTTPCの激安VPS「[Indigo](https://web.arena.ne.jp/indigo/)」を使い始めたのですが、APIが用意されているということで遊んでみました。

#やりたかったこと
基本的には自宅から各種実験用にVPSを使っていて、自宅からSSHでVPS上のインスタンスをに接続しています。
[Indigo](https://web.arena.ne.jp/indigo/)には、ファイアウォールが標準でついているので、

|方向|プロトコル|ポート|IP|
|:---|---------|------|--:|
|IN|TCP|22(SSH)|自宅IPアドレス|

ってな具合にやりたいわけですが、自宅IPアドレスは固定じゃないDDNS運用なので、IPアドレスが変更された場合にFWでDENYされちゃいます。

ということで、自宅IPアドレスが変わった場合にAPIでごにょごにょしてFWのルールを変更してしまいたいというわけです。

#前提条件
以下の内容は、NTT PC コミュニケーションズのVPS「Indigo」上の、
- KVM Instance 1 CPU 1 GB - Ubuntu 18.04
で、動作検証しています。

#やること
Indigoの[API](https://indigo.arena.ne.jp/userapi/)を使って、以下の処理を実装します。
1.DNSに問い合わせをかけて、IPアドレスが変わっていないか確認する。
2.APIキーから、アクセストークンを取得する。[accesstoken](https://indigo.arena.ne.jp/userapi/#generate_access_token)
3. Firewallの一覧を取得して、対象のファイアウォールのID取得する。[getfirewalllist](https://indigo.arena.ne.jp/userapi/#firewall_list)
4. 現状のファイアウォールの設定内容を取得する。[gettemplate](https://indigo.arena.ne.jp/userapi/#retrive_firewall)
5. 変更前のIPアドレスになっているものを、新しいIPアドレスに置き換える。
6. ファイアウォールの設定を更新する。[updatefirewall](https://indigo.arena.ne.jp/userapi/#update_firewall)


#事前準備
## APIキーの取得
コントロールパネルから、API鍵を事前に取得しておく必要がありますので、API鍵とシークレットを入手しておきます。

## node.jsのインストール
APIの入出力はJSONなようなので、node.jsを使います。

```
# apt-get install nodejs
```

## ライブラリのインストール
node.jsで使うライブラリをnpm経由でインストールします。

```
# apt-get install npm
# npm install request dns log-timestamp
```

#さぁ、つくりましょう。
とりあえず、書き殴ってみました。
実ははじめてnode.jsを使ってコードを書いたので変なとこあるかも。。

```indigo-firewall.js:
const IP_FILE = "/opt/indigo/ip.txt";
const FW_NAME = "FW_TEST";
const API_WAIT = 500;
const OAUTH_BASE = "https://api.customer.jp/oauth/v1";
const API_BASE = "https://api.customer.jp/webarenaIndigo/v1";
const HOST_FQDN = "hoge.hoge.com";
const API_CLIENT_ID = "XXXXXXXXXXXXXXXXXXXXXXXXXXXXXX";
const API_CLIENT_SECRET = "XXXXXXXXXXXXX";

require('log-timestamp');

main();

function main() {
	var dns = require('dns');
	dns.lookup(HOST_FQDN, function onNsLookup(err, address, family) {

		console.log("%s => %s", HOST_FQDN, address);

		var fs = require('fs');
		var oip;
		if (fs.existsSync(IP_FILE)) {
			oip = fs.readFileSync(IP_FILE, "utf8").trim();
			console.log("Previous address => %s", oip);
		} else {
			throw new Error("%s not found", IP_FILE);
		}

		if (oip != undefined && oip != address) {
			console.log("Address change detected %s => %s", oip, address);
			accessTokens(oip, address);
		} else {
			console.log("No address change detected");
		}
	});
}

function accessTokens(oip, nip) {

	var request = require('request');
	var options = {
		uri: OAUTH_BASE + "/accesstokens",
		json: {
			grantType: "client_credentials",
			clientId: API_CLIENT_ID,
			clientSecret: API_CLIENT_SECRET
		}
	};

	request.post(options, function(error, response, body) {
		if (!error) {
			console.log("Accesstoken => %s", body.accessToken);
			setTimeout(getFirewallList, API_WAIT, body.accessToken, oip, nip);
		} else {
			console.log("Failed to accesstoken. %s %s %s", error, response, body);
		}
	});
}

function getFirewallList(accessToken, oip, nip) {

	var request = require('request');
	var options = {
		uri: API_BASE + "/nw/getfirewalllist",
		headers: {
			"Authorization": "Bearer " + accessToken
		},
		json: true
	};

	request.get(options, function(error, response, body) {
		if (!error) {
			console.log("%d firewall template found", body.length);
			for (const i in body) {
				console.log("Firewall template %d => %s", body[i].id, body[i].name);
				if (body[i].name == FW_NAME) {
					console.log("Target firewall template found");
					setTimeout(getFirewall, API_WAIT, accessToken, body[i], oip, nip);
				}
			}
		} else {
			console.log("Failed to getfirewalllist. %s %s %s", error, response, body);
		}
	});
}

function getFirewall(accessToken, fw, oip, nip) {

	var request = require('request');
	var options = {
		uri : API_BASE + "/nw/gettemplate/" + fw.id,
		headers: {
			"Authorization": "Bearer " + accessToken
		},
		json: true
	};

	request.get(options, function(error, response, body) {
		if (!error) {
			var nfw = {
				templateid: fw.id,
				name: fw.name,
				inbound: [],
				outbound: []
			};

			for (const i in body) {
				const rule = body[i];

				console.log("Rule before => %s/%s/%s/%s", rule.type, rule.protocol, rule.port, rule.source);

				var filter = {
					type: rule.type,
					protocol: rule.protocol,
					port: rule.port,
					source: (rule.source == oip ? nip : rule.source)
				};

				if (rule.direction == "in") {
					nfw.inbound.push(filter);
				} else if (rule.direction == "out") {
					nfw.outbound.push(filter);
				} else {
					throw new Error("Unknown direction found. %s", rule.direction);
				}

				console.log("Rule after  => %s/%s/%s/%s", filter.type, filter.protocol, filter.port, filter.source);
			}

			setTimeout(updateFirewall, API_WAIT, accessToken, nfw, nip);
		} else {
			console.log("Failed to gettemplate. %s %s %s", error, response, body);
		}
	});
}

function updateFirewall(accessToken, nfw, nip) {

	var request = require('request');
	var options = {
		uri: API_BASE + "/nw/updatefirewall",
		headers: {
			"Authorization": "Bearer " + accessToken
		},
		json: nfw
	};

	request.put(options, function(error, response, body) {

		if (!error && body.success) {
			console.log("Firewall %d => %s", body.firewallId, body.message);

			console.log("Update %s => %s", nip, IP_FILE);
			var fs = require('fs');
			fs.writeFileSync(IP_FILE, nip);
		} else {
			console.log("Failed to updatefirewall. %s %s %s", error, response, body);
		}
	});
}
```
ってな具合ですかね。
冒頭の定数を環境に合わせて設定いただければと思います。

|定数|説明|設定例|
|:---|----|-----:|
|IP_FILE|現在のIPアドレスを保持しておくためのファイルのパス。書き込み権限があるところに設定してください。|/opt/indigo/ip.txt|
|FW_NAME|設定を書き換えるファイアウォールの名前。Indigoのコントロールパネルで指定したやつ。|FW_TEST|
|API_WAIT|連続でAPIを実行すると怒られるので、API実行時の待ち時間（msec）500|
|HOST_FQDN|DDNS運用している自宅のfqdn|hoge.hoge.com|
|API_CLIENT_ID|IndigoのAPI鍵||
|API_CLIENT_SECRET|IndigoのAPI鍵のシークレット|

#さぁ実行しよう
##スクリプトの配置
上のスクリプトをサーバ上に配置します。ここでは、`/opt/indigo/indigo-firewall.js`とします。

##現在のIPアドレスを書き出す。
スクリプトでIPアドレスの変更検知をするために現状のアドレスを保持するファイルを作成しておきます。

```
$ echo x.x.x.x > /opt/indigo/ip.txt
```

## Indigoのファイアウォールの設定確認
IndigoのFWの設定は以下のようになっているとします。

|方向|プロトコル|ポート|IP|
|:---|---------|------|--:|
|IN|TCP|22(SSH)|x.x.x.x|

## IPアドレスの変更
プロバイダとの回線を一旦切るなりして、IPアドレスを変えます。
合わせて、DDNSの登録も更新します。
ここでは、x.x.x.xから、y.y.y.yに変更されたとします。

##いざ実行
満を持して、スクリプトを実行します。

```
$ node /opt/indigo/indigo-firewall.js
[2020-06-20T09:31:01.591Z] hoge.hoge.com => y.y.y.y
[2020-06-20T09:31:01.595Z] Previous address => x.x.x.x
[2020-06-20T09:31:01.595Z] Address change detected x.x.x.x => y.y.y.y
[2020-06-20T09:31:01.860Z] Accesstoken => xxxxxxxxxxxxxxxxxxxxxxxxxxx
[2020-06-20T09:31:02.529Z] 1 firewall template found
[2020-06-20T09:31:02.530Z] Firewall template 1487 => FW_TEST
[2020-06-20T09:31:02.530Z] Target firewall template found
[2020-06-20T09:31:03.168Z] Rule before => Custom/TCP/22/x.x.x.x
[2020-06-20T09:31:03.168Z] Rule after  => Custom/TCP/22/y.y.y.y
[2020-06-20T09:31:08.932Z] Firewall 1487 => Firewall template is updated successfully.
[2020-06-20T09:31:08.932Z] Update y.y.y.y => /opt/indigo/ip.txt
```

Indigoのコントロールパネルから、設定が変わっているか確認します。

|方向|プロトコル|ポート|IP|
|:---|---------|------|--:|
|IN|TCP|22(SSH)|y.y.y.y|

おお、見事に変わりました！

## 定期的に実行
1時間に1回実行されるように、cronに登録しておきます。

```
$ crontab -e
0 * * * * node /opt/indigo/indigo-firewall.js >> /var/log/indigo-firewall.log 2>&1

$ touch /var/log/indigo-firewall.log
```

あとは、logrotatedの設定もよしなに。

#おわりに
始める前はめんどくさいなと思っていたけど、API自体はものすごくシンプルなので、やってみると意外とあっさりできました。それよりも、はじめてのnode.jsに戸惑ったわけですが。。

兎にも角にも、これで自宅からしかSSHできなくなったので、セキュリティ面でも安心できるようになりました！