NTT光ネクストのひかり電話へAsteriskを直接接続する

# 概要
この投稿では、フレッツ光ネクスト(NGN)で提供されているひかり電話へ、Asteriskを直接接続(直収)する方法を紹介する。
Asteriskをひかり電話へ直接接続するための情報は、従来から存在はしたものの非常に少なく断片的で、また古くて参考にしにくい状況がある。
更には最近のAsteriskの方向として、sipモジュールからpjsipモジュールを使うことになっており、この点でも従来からの情報は参考に出来ない。
以上のことから、ONU直下のAsteriskをpjsipを使って接続する方法を通しで紹介する。

# ポイント/狙い
- 2021年時点で使える具体的な手順や設定内容を**通しで**紹介する。
- それに至る試行錯誤も合わせて紹介して、今後の仕様変更に対応できる手がかりを残したい。
- お世話になった5ch.netや公式コミュニティに感謝する。

# 作戦
- まずフレッツ網から、ひかり電話に必要な各種情報をDHCPで取得する。
- 次にAsteriskをソースからインストールする。
- 最後にDHCPで取得した情報を元に、pjsip.confを設定してレジスト(＆発着信)する。

# 前提条件
### 構成

```
            NTT/NGN
               |
             +---+
             |ONU|
             +---+
               |
             +---+
       +-----|HUB|--------+
       |     +---+        |
       |                  |
  (Public IP)        (Public IP)
   +------+        +-------------+
   |      |        |    (eth0)   |
   |Router|        | Raspberry Pi|
   |      |        |    (eth1)   |
   +------+        +-------------+
 (192.168.0.x)      (192.168.0.x)
       |                  |
       |     +---+        |
       +-----|HUB|--------+
             +---+
              | |
       +------+ +---------+
       |                  |
     +---+           +---------+
     |PC |           |SIP Phone|
     +---+           +---------+

```

- ルータとRaspberry Piそれぞれで、IPv4 [PPPoE接続](https://qiita.com/kmorimoto/items/77ea0370ee46f7b3e006)している。特にRaspberry Piは、オンボードのNIC(eth0)がONUへ直接接続されている。<br>なお投稿の趣旨と直接関係ないが、Raspberry Piをルータ直下に接続しなかったのは、面倒なNAT越えを避けるためである。
- Raspberry PiのLAN側接続は、USBのNIC(eth1)を追加して実現している。<br>なおRaspberry Piのeth0～eth1間は、ルーティング/フォワーディングしていない。
- Raspberry Piでは、Raspbian 10(buster)が動いている。

### おことわり
- 上記の構成や以後の設定がベスト/正しいとは思っていません。<br>材料を提供することで、某wikiや5chで議論が出来ればと思っています。
- 法律や約款は考慮に入れていません。(この場では勘弁してください....)

# 手順
### フレッツ網から各種情報をDHCPで取得する
まずひかり電話に関する諸情報、例えばSIPサーバのIPアドレスや電話番号などは、フレッツ網からDHCPオプション([RFC3361](https://tools.ietf.org/html/rfc3361))で取得する。昔はPPPoE接続＆POSTして情報を得ていたようだが、今は不要/不可?である。おそらくBフレッツとか光プレミアム時代には必要だったのかもしれない。

DHCPオプションについては、[このような試行と判断](https://qiita.com/kmorimoto/items/e76047be70dd64c08a1e#%E8%83%8C%E6%99%AF)により、dhclient([ISC DHCP](https://www.isc.org/dhcp/))を使った。
そして設定は[voip-info.jp](http://www.voip-info.jp/index.php/%E5%88%A9%E7%94%A8%E8%80%85:Pin_ptr#DHCP)にある設定を使わせてもらった。

```
cat <<'EOF' >> /etc/dhcp/dhclient.conf
 
option ip-sip-servers code 120 = { boolean, array of ip-address };
option vendor-class.ntt code 210 = string;
option space ntt code width 1 length width 1 hash size 7;
option ntt.mac code 201 = string;
option ntt.number code 202 = text;
option ntt.domain code 204 = domain-list;
option ntt.firmware code 210 = domain-list;
option vendor.ntt code 210 = encapsulate ntt;
 
interface "eth0" {
  send dhcp-client-identifier = hardware;
  request subnet-mask, routers, ip-sip-servers, rfc3442-classless-static-routes, vivso;
  send vendor-class.ntt = concat(06, suffix(hardware, 6));
}
EOF
```

他にもやろうと思えばDHCPv6で諸情報が取れたりするが、今回のひかり電話直収において必須ではないので取らなかった＝voip-info.jpで紹介されている`dhclient.conf`でほぼ必要十分である。

このdhclientの設定で取れる西日本での情報は以下の通り。おそらく東日本でもIPアドレスとドメイン名が違う以外、同じ項目が取得できると思われる。

```/var/lib/dhcp/dhclient.leases
lease {
  interface "eth0";
  fixed-address 124.xxx.xxx.xxx;
  option subnet-mask 255.255.255.252;
  option dhcp-lease-time 14400;
  option routers 124.xxx.xxx.xxx;
  option dhcp-message-type 5;
  option dhcp-server-identifier 124.xxx.xxx.xxx;
  option dhcp-renewal-time 7200;
  option ip-sip-servers true 124.xxx.xxx.1;
  option dhcp-rebinding-time 10800;
  option rfc3442-classless-static-routes xxx,xxx,xxx,....;
  option vivso xx:xx:xx:....;
  option vendor.ntt xx:xx:xx:....;
  option ntt.domain "ntt-west.ne.jp.";
  option ntt.firmware "www.verinfo.hgw.flets-west.jp.";
  option ntt.mac xx:xx:xx:xx:xx:xx;
  option ntt.number "0xxxxxxxxx";
  renew 3 2021/02/xx xx:xx:xx;
  rebind 3 2021/02/xx xx:xx:xx;
  expire 3 2021/02/xx xx:xx:xx;
}
```

先ほど「ほぼ必要十分な情報が取れる」と言ったが、実は上記の通りDHCPv4ではDNSのアドレスが取得できない。DNSはDHCPv6では取れるのだが、ひかり電話に関してDNSが必要な場面は【ntt-west.ne.jp <--> 124.xxx.xxx.1】だけなので、雑ではあるが`/etc/hosts`で対応することにした。(IPv6はRaspberry Piの隣のルータがv4 over v6で使っており、ルータのconfigを変えるのが面倒くさかった)

```
cat <<'EOF' >> /etc/hosts
124.xxx.xxx.xxx ntt-west.ne.jp
EOF
```

その他これは言うまでもないが、経路情報は自動的に設定されるので、手作業で追加する必要はない。

ここまでの作業により、`ifconfig`と`route`の結果は以下の通りとなる。

```
eth0: flags=4163<UP,BROADCAST,RUNNING,MULTICAST>  mtu 1500
        inet 124.xxx.xxx.xxx  netmask 255.255.255.252  broadcast 124.xxx.xxx.xxx
        ether xx:xx:xx:xx:xx:xx  txqueuelen 1000  (イーサネット)
        (略)

eth1: flags=4163<UP,BROADCAST,RUNNING,MULTICAST>  mtu 1500
        inet 192.168.0.xx  netmask 255.255.255.0  broadcast 192.168.0.255
        ether xx:xx:xx:xx:xx:xx  txqueuelen 1000  (イーサネット)
        (略)

lo: flags=73<UP,LOOPBACK,RUNNING>  mtu 65536
        inet 127.0.0.1  netmask 255.0.0.0
        loop  txqueuelen 1000  (ローカルループバック)
        (略)

ppp0: flags=4305<UP,POINTOPOINT,RUNNING,NOARP,MULTICAST>  mtu 1454
        inet 153.xxx.xxx.xxx  netmask 255.255.255.255  destination 153.xxx.xxx.xxx
        ether xx:xx:xx:xx:xx:xx  txqueuelen 1000  (イーサネット)
        (略)
```


```
カーネルIP経路テーブル
受信先サイト    ゲートウェイ    ネットマスク   フラグ Metric Ref 使用数 インタフェース
default         0.0.0.0         0.0.0.0         U     0      0        0 ppp0
124.xxx.0.0     124.xxx.xxx.xxx 255.255.xxx.0   UG    0      0        0 eth0
124.xxx.xxx.xxx 0.0.0.0         255.255.255.252 U     0      0        0 eth0
124.xxx.xxx.0   124.xxx.xxx.xxx 255.255.xxx.0   UG    0      0        0 eth0
153.xxx.xxx.xxx 0.0.0.0         255.255.255.255 UH    0      0        0 ppp0
192.168.0.0     0.0.0.0         255.255.255.0   U     0      0        0 eth1
xxx.xxx.xxx.0   124.xxx.xxx.xxx 255.255.224.0   UG    0      0        0 eth0
```

当たり前だが環境が人それぞれで違うので、この通りになるという意味ではない。
ポイントは、`ifconfig`においてはeth0にフレッツ網からDHCPで取得した情報(この例だと124.xxx.xxx.xxx)がついていること、そして`route`においてはその情報に応じた経路(この例だと124.xxx.xxx.xxxがeth0を向く)設定がされていること、である。

### Asteriskをソースからインストールする
後ほど説明するが、ひかり電話直収に必要な`pjsip.conf`のパラメータ(`disable_rport`)は、[Asterisk 16のChangeLog](http://downloads.asterisk.org/pub/telephony/asterisk/ChangeLog-16-current)や[Asterisk 18のChangeLog](http://downloads.asterisk.org/pub/telephony/asterisk/ChangeLog-18-current)によると、16.12.0以降もしくは18.0.0以降で使えるようだ。
しかし2021年2月現在、Raspberry Pi(Raspbian)のaptで入るAsteriskは16.2.1であり、当該パラメータが使えない。従って現時点では[公式サイト](https://www.asterisk.org/downloads/)からソースを落としてきて、入れなければならない。

まずtar.gzを落としてきて、[公式ドキュメント](https://wiki.asterisk.org/wiki/display/AST/Checking+Asterisk+Requirements)でも言われているとおり、何も考えずに`./configure`すれば必要なパッケージをあぶり出せる。


```
cd /usr/src
wget https://downloads.asterisk.org/pub/telephony/asterisk/asterisk-18.2.0.tar.gz
tar -zxvf asterisk-18.2.0.tar.gz
cd asterisk-18.2.0
./configure
```

`configure`の途中で「xxxxが足りない」などと言われたら、都度それをaptで入れていった。
なぜこうしたかと言うと、現時点でひかり電話直収に必要/必須なパッケージを明らかにしたかったからだ。voip-info.jpや他のqiitaの記事で必要なパッケージがまとめられているが、それはその時入れるAsteriskのバージョンやOS(Distribution)によって異なるので、必ずしも正しいとは限らない。
今回Raspbian 10(Buster)でAsterisk 18.2.0を入れるに際しては、以下のものの不足が指摘されたので入れた。

```
apt install libedit-dev uuid uuid-runtime uuid-dev libjansson-dev libxml2-dev sqlite3 libsqlite3-dev
```

そしてAsteriskの動作としては必須ではないが、[公式コミュニティ](https://community.asterisk.org/t/pjsip-dns-resolution-with-etc-hosts-asterisk-12-4/70637)や[公式ドキュメント](https://www.asterisk.org/res_resolver_unbound/)によると、Asteriskが`/etc/hosts`を読むためには以下のものが必要だそうなので、これも入れた。

```
apt install libunbound-dev
```

なお`configure`の引数について、またpjsip対応についても[公式ドキュメント](https://wiki.asterisk.org/wiki/display/AST/PJSIP-pjproject)で書かれているとおり、Asterisk 15.0.0以降であればデフォルトで組み込まれるようだ。なので何も設定しなかった。
`configure`が問題なく終わると,以下のようになる。

```
./configure
(略)
               .$$$$$$$$$$$$$$$=..     
            .$7$7..          .7$$7:.   
          .$$:.                 ,$7.7  
        .$7.     7$$$$           .$$77 
     ..$$.       $$$$$            .$$$7
    ..7$   .?.   $$$$$   .?.       7$$$.
   $.$.   .$$$7. $$$$7 .7$$$.      .$$$.
 .777.   .$$$$$$77$$$77$$$$$7.      $$$,
 $$$~      .7$$$$$$$$$$$$$7.       .$$$.
.$$7          .7$$$$$$$7:          ?$$$.
$$$          ?7$$$$$$$$$$I        .$$$7
$$$       .7$$$$$$$$$$$$$$$$      :$$$.
$$$       $$$$$$7$$$$$$$$$$$$    .$$$. 
$$$        $$$   7$$$7  .$$$    .$$$.  
$$$$             $$$$7         .$$$.   
7$$$7            7$$$$        7$$$     
 $$$$$                        $$$      
  $$$$7.                       $$  (TM)    
   $$$$$$$.           .7$$$$$$  $$     
     $$$$$$$$$$$$7$$$$$$$$$.$$$$$$     
       $$$$$$$$$$$$$$$$.               
 
configure: Package configured for:
configure: OS type  : linux-gnueabihf
configure: Host CPU : armv7l
configure: build-cpu:vendor:os: armv7l : unknown : linux-gnueabihf :
configure: host-cpu:vendor:os: armv7l : unknown : linux-gnueabihf :
```

続いて[公式ドキュメント](https://wiki.asterisk.org/wiki/display/AST/Building+and+Installing+Asterisk)の通り、`make`、`make install`する。

```
make
(略)
 +--------- Asterisk Build Complete ---------+
 + Asterisk has successfully been built, and +
 + can be installed by running:              +
 +                                           +
 +                make install               +
 +-------------------------------------------+
 
make install
(略)
 +---- Asterisk Installation Complete -------+
 +                                           +
 +    YOU MUST READ THE SECURITY DOCUMENT    +
 +                                           +
 + Asterisk has successfully been installed. +
 + If you would like to install the sample   +
 + configuration files (overwriting any      +
 + existing config files), run:              +
 +                                           +
 + For generic reference documentation:      +
 +    make samples                           +
 +                                           +
 + For a sample basic PBX:                   +
 +    make basic-pbx                         +
 +                                           +
 +                                           +
 +-----------------  or ---------------------+
 +                                           +
 + You can go ahead and install the asterisk +
 + program documentation now or later run:   +
 +                                           +
 +               make progdocs               +
 +                                           +
 + **Note** This requires that you have      +
 + doxygen installed on your local system    +
 +-------------------------------------------+
```

最後の仕上げとして、公式ドキュメント([Installing Initialization Script](https://wiki.asterisk.org/wiki/display/AST/Installing+Initialization+Scripts)や[Installing Sample Files](https://wiki.asterisk.org/wiki/display/AST/Installing+Sample+Files))で紹介されているスクリプトやファイルを入れたりする。

```
make config
make samples
make install-logrotate
systemctl daemon-reload
systemctl enable asterisk
```

これでAsteriskのsystemdユニット定義ファイルが作成されたり、`/etc/asterisk`の中にサンプルのconfファイルが置かれたり、`/etc/logrotate.d/asterisk`が仕込まれたりする。

### Asterisk(pjsip.conf, extensions.conf)を設定する
`sip.conf`を使ったひかり電話直収については各所で時々見かけるが、`pjsip.conf`を使ったひかり電話直収を私は見つけられなかった。なので今回頑張って`pjsip.conf`を作った。
当初は`sip.conf`から`pjsip.conf`へ変換するスクリプト(`contrib/scripts/sip_to_pjsip/sip_to_pjsip.py`)を使えばすぐ出来ると思っていたのだが、[公式ドキュメント](https://wiki.asterisk.org/wiki/display/AST/Migrating+from+chan_sip+to+res_pjsip)でも言われているとおり、常に互換性のあるconfを吐けるものではないようだ。

それを念頭において、スクリプトで一旦`pjsip.conf`を作成し、それを元にレジスト＆発着信できるパラメータを探った。
その苦労話は後で書くとして、結果としては以下の`pjsip.conf`で動いてくれた。(注：ひかり電話直収部分のみ詳解)

```/etc/asterisk/pjsip.conf
[global]
type = global
debug = no

[system]
type = system
disable_rport = yes ;via headerにrportが入らないよう必須

[transport-udp]
type = transport
protocol = udp
bind = 0.0.0.0

[reg_HIKARI-DENWA]
type = registration
contact_user = 0xxxxxxxxx ;電話番号(=DHCPで取得したntt.number)
transport = transport-udp
client_uri = sip:0xxxxxxxxx@ntt-west.ne.jp ;"電話番号@DHCPで取得したntt.domain"である必要がある
server_uri = sip:ntt-west.ne.jp ;DHCPで取得したntt.domain"である必要がある
 
[HIKARI-DENWA]
type = aor
contact = sip:124.xxx.xxx.1 ;DHCPで取得したip-sip-servers
 
[HIKARI-DENWA]
type = identify
endpoint = HIKARI-DENWA
match = 124.xxx.xxx.1 ;DHCPで取得したip-sip-servers
 
[HIKARI-DENWA]
type = endpoint
context = incoming ;extension.conf
disallow = all
allow = ulaw
rtp_symmetric = no ;via headerにrportが入らないよう必須
force_rport = no ;via headerにrportが入らないよう必須
rewrite_contact = no ;via headerにrportが入らないよう必須
direct_media = no ;RTPをAsteriskで中継するため必要
from_domain = ntt-west.ne.jp ;DHCPで取得したntt.domain"である必要がある
aors = HIKARI-DENWA

[100]
type = aor
(略)

[100]
type = auth
(略)

[100]
type = endpoint
(略)
```

##### 苦労した/大事なポイント
- `disable_rport`は必須。<br>pjsipではこれを設定しないと、Viaヘッダにrportという文字列が入る。rportが入るとレジストと着信は出来るが、発信が出来ない。(発信しようとinviteを投げると`400 Bad Request`が返ってくる)
- `client_uri`と`server_uri`は、DHCPで取得した`ntt.domain`(この場合はntt-west.ne.jp)である必要がある。<br>`sip_to_pjsip.py`で変換するとここがIPアドレスになったりするが、それだと何を投げても`400 Bad Request`が返ってくる。
 - 一方で`ntt.domain`の値を設定しただけではそのドメインの名前解決が出来ず、`No Response received`になる。<br>なので`/etc/hosts`で名前解決できるようにしてある。
- `contact`と`match`はDHCPで取得したSIPサーバのIPアドレスで良い。
- `allow`は[公式ドキュメント](https://wiki.asterisk.org/wiki/display/AST/Asterisk+18+Configuration_res_pjsip)でデフォルト値が空欄だったので、NTTの技参資([東](https://flets.com/hikaridenwa/use/data.html),[西](https://www.ntt-west.co.jp/info/gisanshi/))を見てG.711 ulawにした。
- `rtp_symmetric`, `force_rport`, `rewrite_contact`は必須。<br>[公式ドキュメント](https://wiki.asterisk.org/wiki/display/AST/Migrating+from+chan_sip+to+res_pjsip)に書かれているが、`sip.conf`における`nat=never`にあたるもので、これと先の`disable_rport`を指定することで、Viaヘッダにrportという文字列が入らない。
- `direct_media`はエンドエンドで通信するか否かの設定だと理解していて、インターネットからひかり電話(124.xxx.xxx.1)へ直接繋がるわけがないと考えて、noにした。(今改めて考えてみると必須なのだろうか....)

その他、入れなかったパラメータで気になっているものを以下のメモに残す。

- `dtmf_mode`は[公式ドキュメント](https://wiki.asterisk.org/wiki/display/AST/Asterisk+18+Configuration_res_pjsip)でデフォルトがRFC4733であると確認し、NTTの技参資([東](https://flets.com/hikaridenwa/use/data.html),[西](https://www.ntt-west.co.jp/info/gisanshi/))でもRFC4733に対応していると書かれていたので、設定しなかった。<br>なお`sip.conf`ではよく`dtmfmode=rfc2833`としていたが、RFC2833を改廃したものが[RFC4733](https://tools.ietf.org/html/rfc4733)である。
- `timers`, `timers_min_se`, `timers_sess_expires`はセッションタイマに関する設定で、今のところ必要なかったので入れなかった。もし今後、意図せず通話が切れたり切れなかったりしたら、この辺を調整してみる。
- `tos`は設定しなくても動いたので、設定しなかった。QoS担保の観点では設定すべきなのかもしれないが。

次、ひかり電話直収とは直接関係ないが、`extension.conf`も参考に紹介しておく。

```/etc/asterisk/extensions.conf
[general]
writeprotect=no
priorityjumping=no
 
[globals]
USEVOICEMAIL=NO
MYNUMBER=0xxxxxxxxx
PHONEALL=PJSIP/100(略)
 
[default]
exten => 100,1,Dial(PJSIP/100)
exten => 100,n,Hangup
(略)
exten => _X.,1,Dial(PJSIP/${EXTEN}@HIKARI-DENWA)
exten => _X.,n,Hangup

[incoming]
exten => _X.,1,Set(free_dial="0120")
exten => _X.,2,Set(free_call="0800")
exten => _X.,3,GotoIf($["${CALLERID(num)}"="anonymous"]?99:4)
exten => _X.,4,GotoIf($[${CALLERID(num)}:${free_dial}]?99:5)
exten => _X.,5,GotoIf($[${CALLERID(num)}:${free_call}]?99:6)
exten => _X.,6,Dial(${PHONEALL})
exten => _X.,99,Answer()
exten => _X.,n,Hangup
```
これに間違いがないのか自信はないが、私の理解ではこれでいいと思っていて、意図通り動いているので良しとしている。
なお少し頑張ったところとしては、`incoming`でフリーダイヤルを拒否している。これはひかり電話直収と直接関係ないが、以前5ch.netでヒントをもらったので、御礼として紹介しておく。

上記の通り、DHCPで取得した電話番号やIPアドレスは`pjsip.conf`や`extensions.conf`で直接設定されている。この場合もしDHCPで与えられる情報が変更されると、`pjsip.conf`や`extensions.conf`はその情報に追随できない。
この解決策として、[voip-info.jp](http://www.voip-info.jp/index.php/%E5%88%A9%E7%94%A8%E8%80%85:Pin_ptr#ntt.conf)ではdhclientのhookを使って`sip.conf`を修正するようにしている。
ただ`/var/lib/dhcp/dhclient.leases`を観察している限り、これらDHCPの情報は今まで変わったことがなく、これからも変わらないような気がする。
なので面倒くさいというのもあり、今のところ`dhclient.leases`を`pjsip.conf`へ反映するスクリプトは作らず放置している。

# あとがき
疲れました。
誤字脱字、内容が飛んでいる、単語の揺らぎなどあればご指摘ください。