Linux上に自前DDNSのクライアントを構築する

# はじめに
- そのまま使えるコードはありません
    - 実際の環境にばらつきがあると思われるため…
- 方式がいろいろ考えられるため、ほんの一例
- 異常系や例外は未考慮…

# 今更DDNS
DDNSサービスはいろいろあるが、自分のルーターにはDDNSサービスがバンドルされていない。
DDNSを自前でやるにも、方式がいろいろありそう。
どうやるのが良いのだろう。

# 前提
- サーバーは、BIND9([RFC2136](https://tools.ietf.org/html/rfc2136)に対応していれば、何でもよさそう)
- クライアントは、Ubuntu 16.04(systemdが動けば、何でもよさそう)
- ルーターは、SEIL/x86(IPアドレスを取得する方式によっては、何でもよさそう)
- ホスト名をつける対象は、PPPoE接続のグローバルIPv4アドレス

# コンセプト
- なるべくOSやルーターに依存しない、汎用的な方法を探る
- なるべくDDNS専用のサーバープロセスなどを立てない
- つまり、手抜き

# DDNSクライアント
DDNSクライアントの機能は、ざっくりこんな感じと想像。

1. 外部のグローバルIPアドレスを得る
2. そのIPアドレスをDNSサーバーに送る
3. DNSレコードを書き換える

このうち、2.と3.は基本的にnsupdateでできるはず。
問題は1.。ルーターがSEIL/x86なので、ルーターにバンドルされたDDNSサービスが無い。
DDNSサービスに応じたクライアントはいろいろあるが、サービスにある程度依存していると思われる[^1]。
今回はサーバーがただのBIND9なので、それに応じたクライアントを考えてみる。

# 主な凡例
- 203.0.113.1: 外部のグローバルIPアドレス
- 192.168.0.1: ルーターのプライベートIPアドレス
- hoge.dip.example.org: 外部のグローバルIPアドレスに対応したホスト名
- 203.0.113.254: DNSサーバーアドレス

# 外部のグローバルIPアドレスを得る方法
## ルーターから取得する
### ルーターに定期的にログインする

- メリット: ルーターはPPPoEで得たIPアドレスを確実に知っている
- デメリット: ポーリングのために定期的にログインする(そしてlogも荒れる)

SEILなら、`show status ppp pppoe0`で表示できるのは知っていた。
SNMP非対応のRT57iを使っていた時にお世話になった、telnetで得た情報をMRTG化する[記事](https://tech-public.blogspot.jp/2009/01/mrtgyamaha-rt57i.html)を思い出した。
SEILにsshでコマンド送れるのかな…と思って実行したら、できてしまった。

```shell-session
$ ssh -i ~/.ssh/gw.local user@192.168.0.1 'show status ppp pppoe0'
7ffeffff: Interface: pppoe0
7ffeffff:   LCP state: opened
7ffeffff:   IPCP state: opened
7ffeffff:   IPv6CP state: initial
7ffeffff:   BCP state: initial
7ffeffff:   LCP negotiated options:
7ffeffff: 	none
7ffeffff:   IPCP negotiated options:
7ffeffff: 	address 203.0.113.1
7ffeffff: 	primary dns address 198.51.100.1
7ffeffff: 	secondary dns address 198.51.100.2
7ffeffff:   IPv6CP negotiated options:
7ffeffff: 	none
7ffeffff:   BCP negotiated options:
7ffeffff: 	none
7ffeffff:   keepalive: 30 seconds interval
00000000: success
7ffe0000: exit(0).
$ ssh -i ~/.ssh/gw.local user@192.168.0.1 'show status ppp pppoe0' | sed -nr 's/^.*\s\s+address\s(.*)\s*$/\1/p'
203.0.113.1
```

sshの鍵をパスフレーズ無しで作ると、シェルスクリプトからも情報が取得できそうだ。sedの正規表現は適当。
ただ、色々謎がある。

- 7ffeffff: って?
- `show log`に`error telnet telnetd:  sppp__ioctl:40: socket: Operation not permitted`が残る

同じことをRTX1200にやると、`Received disconnect from 192.168.0.1 port 22:2: Channel request 'exec' is not supported`というエラー。VyOSでは`Invalid command: [show]`というエラー。
sshでなくtelnet+expectならうまく行く可能性もあるが、どちらにせよ無理やり感が残る。

### SNMPで取得する
- メリット: ルーターの情報を取得する一般的な方法
- デメリット: 自分の知識不足

RT57iでは、SNMPが使えないのでtelnetで情報を得ていたのだった。
SEILならSNMPが使える。
そこで、snmpwalk。

```shell-session
$ sudo apt install snmp
:
$ snmpwalk -v 2c -c HogeHoge 192.168.0.1 | grep 203.0.113.1
:
iso.3.6.1.2.1.4.21.1.7.0.0.0.0 = IpAddress: 203.0.113.1
:
$ snmpget -v 2c -c HogeHoge 192.168.0.1 iso.3.6.1.2.1.4.21.1.7.0.0.0.0 | sed -nr 's/.*IpAddress: (.*)\s*$/\1/p'
203.0.113.1
```

アドレスが取れる。トラップを使えば割り込みにできるかもしれない。
ただ、ここで取得できた情報は、何を示しているのか。[こちら](https://l3net.wordpress.com/2013/05/12/installing-net-snmp-mibs-on-ubuntu-and-debian/)を見ながらMIBを入れてみた後、

```
$ snmptranslate iso.3.6.1.2.1.4.21.1.7.0.0.0.0
:
RFC1213-MIB::ipRouteNextHop.0.0.0.0
```

すると、ipRouteNextHopとのこと[^2]。インターフェースのアドレスというわけではなさそうだ。
PPPoEが接続できなかった場合は、どうなるのか…[^3]。

##外部サーバーから取得する
- メリット: 簡単そう
- デメリット: 外部のサーバーに依存することになる

外側のグローバルIPアドレスは、インターネットにあるwebサーバーなどにアクセスすると、ログに残る。
ならば、webサーバーに教えてもらえばよいのでは。

[こちらの記事](http://qiita.com/sharow/items/66d89136180884a2f7b7)によると、こういったサービスがいくつかあるようだ。
試しにifconfig.coを使ってみた

```shell-session
$ curl ifconfig.co
203.0.113.1
```

外部のサービスに依存したくない場合、自分のwebサーバーにアドレスを返すページを置いておいて、curlで取得する手もある。
PHPでは、`<?php echo $_SERVER["REMOTE_ADDR"]; ?>`などで、返すことができるようだ。
ただ、この方法でも、webサーバーは必要となる。
とりあえず、この方式にするかな。

# IPアドレスを送ってDNSレコードを書き換える
nsupdateで更新するにしても、外部のグローバルIPアドレスを取得した後、多少ロジックが必要だ。
アドレスの変化をポーリングする場合、例えば

1. 今の外部のグローバルIPアドレスを取得する
2. 今までのIPアドレスを取得する
3. 両者が異なっていたら、nsupdateする

といったことを定期的に動かす必要がある。
定期的に動かすには、サービスにする必要がある。手抜きしたい…。

## cron
最初に思った。もちろん使える。

## systemd
systemdでも、サービスを簡単に作れるのでは。
探すと、[こちら](http://qiita.com/sharow/items/e8f7d3e0628d7ee925db)に詳しい記事が。
`--user`で、ユーザーモードで動かせて、`enable`もできる。
起動n分後に実行などもできそうなので、SEIL/x86の仮想マシンが起動して、PPPoEでアドレスを取得完了後、といったタイミングも狙えそう。
これならシェルスクリプトを定期実行できそうなので、クライアント全体はシェルスクリプトでで実現しようかな。

## シェルスクリプトからのnsupdate
検索して出てくるnsupdateの例は、インタラクティブモードで、プロンプトからコマンドを入力しているものが多い。
[nsupdateのマニュアル](https://ftp.isc.org/isc/bind9/cur/9.9/doc/arm/man.nsupdate.html)を見ると、インタラクティブモードでなく、標準入力やファイルでもコマンドを渡せる模様。
コマンドのテンプレートを用意しておき、アドレスをsedで置き換えて標準入力から渡してみる。

このマニュアルによると、アドレスの変更という操作は存在せず、一旦deleteしてからaddする必要がある模様。

# 設置ファイル
```~/.config/systemd/user/dip.service
[Unit]
Description=Dynamic IP update service

[Service]
Type=simple
ExecStart=/bin/sh /home/hogehoge/bin/dip.sh

[Install]
WantedBy=default.target
```

```~/.config/systemd/user/dip.timer
[Unit]
Description=Dynamic IP update service

[Timer]
OnBootSec=3min
OnUnitActiveSec=30min
Unit=dip.service

[Install]
WantedBy=timers.target
```

```bash:/home/hogehoge/bin/dip.sh
#!/bin/sh
#PPP_IP=`ssh -i /home/hogehoge/.ssh/192.168.0.1 user@192.168.0.1 'show status ppp pppoe0' | sed -nr 's/^.*\s\s+address\s(.*)\s*$/\1/p'`
#PPP_IP=`snmpget -v 2c -c HogeHoge 192.168.0.1 iso.3.6.1.2.1.4.21.1.7.0.0.0.0 | sed -nr 's/.*IpAddress: (.*)\s*$/\1/p'`
#PPP_IP=`curl -s ifconfig.co`
PPP_IP=`curl -s http://www.example.org/~hogehoge/remote-addr.php`
DNS_IP=`dig @203.0.113.254 hoge.dip.example.org A | sed -nr 's/^hoge\.dip\.example\.org.*IN\tA\t(.*)\s*$/\1/p'`
if [ -n "$PPP_IP" -a \( -z "$DNS_IP" -o "$PPP_IP" != "$DNS_IP" \) ]; then
    cat /home/hogehoge/bin/nsupdate.txt | sed "s/NEW_IP/$PPP_IP/" | nsupdate -k /home/hogehoge/.ssh/Khoge.dip.example.org.+xxx+yyyyy.key
fi
```

```/home/hogehoge/bin/nsupdate.txt
server 203.0.113.254
update delete hoge.dip.example.org A
update add hoge.dip.example.org 600 A NEW_IP
send
```

```php:remote-addr.php
<?php echo $_SERVER["REMOTE_ADDR"]; ?>
```

# サービス開始

```shell-session
# ファイル読み込み
$ systemctl --user daemon-reload
# ファイル読み込み状況確認
$ systemctl --user list-unit-files
# 実行
$ systemctl --user start dip.service
$ systemctl --user start dip.timer
# 実行状況確認
$ systemctl --user status dip.service
$ systemctl --user status dip.timer
# 有効化
$ systemctl --user enable dip.service
$ systemctl --user enable dip.timer
```

# 感想
結局ごちゃごちゃしてしまった…
[GnuDIP](http://gnudip2.sourceforge.net)とか入れたほうがよかったかしら…
でも色々勉強になった。

# 宿題他
- 旧IPアドレス取得は、毎回DNS検索という手抜き
- PPPoEが接続できない間は、何が起こるんでしょうね…(未確認)
- shの条件式があやしい
- sed -rってMacOSでは使えないのね…

[^1]: 各サービス向けに作られたクライアントの中には、汎用的に使えるものもあり、それらを使うという方向もあるが、今回は勉強がてらクライアントも自前の方向で。
[^2]: 蛇足だが、ipRouteNextHopで検索すると、`snmptranslate`的なことをwebでできるところがいろいろ見つかった。
[^3]: 自宅はOSPFのマルチホーム環境だが、別の出口のアドレスが取得された。