Asteriskとスマホで、単一の050を複数端末同時待機できる音声認識留守電つき可搬イエデンを作る

# はじめに

イエデン。
家の電話、個人宅の固定電話...もう死語ですね。
その昔、まだ携帯電話などというものは普及しておらず、異性に連絡するときはその人の家の電話（イエデン）に電話をかけ、相手のお母さまから「あらあらうふふ(c)アリシアさん」な対応を受けたものです（老害発言）。

それはさておき、そんなイエデンをAsteriskでつくります。
ネタとしては以下と同じです。

[AWS上にAsteriskサーバ構築して内線電話環境つくってみよう \- Qiita](https://qiita.com/ganezasan/items/05b16a2254f066f6bbdc)
[asteriskでIP内線電話を構築する。電話機はスマホ＋アプリ \- Qiita](https://qiita.com/harasakih/items/706a8cc4c2568936927b)
[Asteriskでブラステルの留守番電話 | デビアンな生活](https://www.kokobit.com/asterisk%E3%81%A8%E3%83%96%E3%83%A9%E3%82%B9%E3%83%86%E3%83%AB/)
[内線電話 + 050plus 作業メモ - Qiita](https://qiita.com/mbamba/items/abf90358d9b27b5b7993 "内線電話 + 050plus 作業メモ - Qiita")

# 目的

* 050で始まる電話番号を確保する
* 電話がかかってくると、複数のスマホ、複数のパソコンが一斉に呼び出される
* どこか一つでも応答すれば、そこで通話がはじまる
* 一台も応答しなかったときは、留守電になる
* 留守電で録音した後、音声認識をしてテキストをメールしてくる
* いずれの端末からでも、その050を使って発信することもできる
* 電話を鳴らさずに留守電にするかどうかは動的に決定できるようにする
* 電話帳はiCloudが持っているものを使う

複数の端末が同時に待機できますから、一人で複数の端末を持っていても、（後述の法律の問題はありますが）家族の複数人のメンバーが端末を持っていてもOKです。
そのうちの最も早く反応した端末が受信する通信(anycast)の電話、ということになります[^1]。

[^1]: 情報系の大学一年生のお勉強の復習となりますが、携帯電話のような1対1の通信はunicast、ラインのグループみたいな同報はmulticast、テレビのような放送はbroadcastです。

# 電気事業通信法

電気事業通信法という法律があります。

[登録・届出を要しない電気通信事業に該当するか否かの基準 ｜ 電気通信事業届出・登録の安心代行センター｜電気通信事業の申請はおまかせ](https://denkitsuushin.com/fuyou/ "登録・届出を要しない電気通信事業に該当するか否かの基準 ｜ 電気通信事業届出・登録の安心代行センター｜電気通信事業の申請はおまかせ")

なんかSIPの音声通話もこれに該当するそうです。
今回の場合は、

* 自分ひとりが用いるのだけ
* 「他人の通信の用」ではなく自分ひとりのためだけ
* 誰かから金銭を受け取るわけではない

ので多分大丈夫。
ただし同居家族とかいる場合の「イエデン」は知りません。
家庭内感染を防ぐためにも自立しましょう。

# 費用

まず最初に必要なお金は050に払うお金だけです。
500円ほどチャージしておけば、電話を受けるだけならタダです。
自分から発信する時に、少しずつ500円のチャージが減っていきます。
月額課金もありません（電気代やIP接続の費用、サーバ代金は除く)。

今回は留守電の音声認識も含めているので、無課金で以下のサービスと同等のことができます。

[読める、聞ける、新しい留守番電話サービス「スマート留守電（スマ留守）」｜ソースネクスト](https://www.sourcenext.com/product/smart-rusuden/)

# 必要なもの

* グローバルIPv4で以下のポートをだせるdockerコンテナ
    * 5060/tcp
    * 10000-10200くらい/udp
* SIPを話せるクライアント
    * iOSの場合はacrobits（有料, もちろんこれ以外でもいいですが常用するにはこれがおすすめです。理由は後述)
    * 普通のPCの場合は、普通のSIPクライアント
* brastelとの契約
    * 初回チャージ金額

# 手順

## Brastel 050 を契約

以下の契約をします。

[ブラステル｜My 050（マイ050） \- Brastel](https://www.brastel.com/personal/my050/jpn/)

050の会社はたくさんありますが、

* AsteriskのSIPでつながる
* 前払い

という点がおいしいのでこれにしました。

[ブラステルは格安IP電話です！まずはコンビニでカードをゲットしましょう！: 桃好きの桃源郷](http://fanblogs.jp/pechemhlkt1007/archive/50/0)

契約したら、8桁のユーザ名と8桁のパスワードを控えておきます。

## Asterisk

Asteriskをdockerで立ち上げます。

### Dockerfile

だいたいこんな感じ。

``` Dockerfile
FROM debian

RUN cat /etc/apt/sources.list | sed 's/^deb/deb-src/' > /tmp/a && cat /tmp/a >> /etc/apt/sources.list
RUN apt-get update
RUN apt-get build-dep -y asterisk
RUN apt-get remove -y libpjsip2 libsrtp2-1 libssl-dev
RUN apt-get autoremove -y
RUN apt-get install -y libssl1.0-dev

ENV ASTERISK_VERSION 16.3.0
RUN cd /tmp && wget -O-  http://downloads.asterisk.org/pub/telephony/asterisk/releases/asterisk-${ASTERISK_VERSION}.tar.gz | tar xvfz - && mv /tmp/asterisk-${ASTERISK_VERSION} /tmp/asterisk/
RUN cd /tmp/asterisk && ./configure --with-jansson-bundled --with-opus --with-ilbc && make && make install && make samples && make config
RUN rm -fr /tmp/asterisk

RUN mkdir -p /tmp/opus && cd /tmp/opus && wget -O- http://downloads.digium.com/pub/telephony/codec_opus/asterisk-16.0/x86-64/codec_opus-16.0_current-x86_64.tar.gz | tar xvfz -
RUN install /tmp/opus/*/*.so /usr/lib/asterisk/modules/
RUN install /tmp/opus/*/*.xml /var/lib/asterisk/documentation/thirdparty
RUN rm -fr /tmp/opus

RUN cd /var/lib/asterisk/sounds/ && wget -O- https://voip-info.jp/downloads/asterisk/sounds/1_6/asterisk-sound-jp_16_pre.tar.gz | tar xvfz - && ln -s jp ja_JP
```

### compose

ネットワーク周りはこんなかんじ。

``` docker-compose.yaml
   ports:
      - "5060:5060"
      - "10000-10200/udp"
```

### Asterisk 設定ファイル

#### codecs.conf

有効になっていればオプションはそんなに気にする必要ないのかも。

``` ini:codecs.conf
[opus]
type=opus
max_bandwidth=wide
```

#### rtp.conf

つかえるUDPのポートレンジの指定です

``` ini:rtp.conf
[general]
rtpstart=10000
rtpend=10200
```

#### voicemail.conf

留守電のメール送信です。

``` ini:voicemail.conf
[general]
format=wav49
serveremail=asterisk
attach=yes
maxmsg=9999
maxsecs=300
maxsilence=20
silencethreshold=128

[default]
500 => 1234,hoge,${メールアドレス},tz=japan
```

### sip.conf

どうもpjsipは不安定なところがあるようですので諦めてsip.confです。
SIPSやDTLSの暗号化もありません。

sipはとてもたくさん攻撃を喰らいます。
後述のfail2banとあわせて、ちゃんと設定しておきましょう。

[不適切な設定で Asteriskを利用した場合に発生し得る不正利用に関する注意喚起](https://www.jpcert.or.jp/at/2010/at100032.html "不適切な設定で Asteriskを利用した場合に発生し得る不正利用に関する注意喚起")

``` ini:sip.conf
[general]
context=denyall
udpbindaddr=${外向きグローバルIPアドレス}:5060
tcpbindaddr=${外向きグローバルIPアドレス}:5060
dtmfmode=rfc2833
language=ja_JP
nat=yes
allowguest=no

[brastel]
type=friend
secret=${brastelのパスワード}
username=${brastelのユーザ名}
fromuser=${brastelのユーザ名}
fromdomain=softphone.spc.brastel.ne.jp
host=softphone.spc.brastel.ne.jp
context=brastelin
insecure=invite
canreinvite=no
disallow=all
allow=ulaw
allow=gsm

register => ${brastelのユーザ名}@softphone.spc.brastel.ne.jp:${brastelのパスワード}:${brastelのユーザ名}@softphone.spc.brastel.ne.jp/201

[iphoneFoo]
context=iphoneFoo
type=friend
defaultuser=iphoneFoo
secret=${なにか適当なパスワード}
host=dynamic
canreinvite=no
transport=udp
textsupport=yes
qualify=1000
disallow=all
allow=gsm
allow=opus
allow=speex
allow=vp8
allow=ulaw

[iphoneBar]
context=iphoneBar
type=friend
defaultuser=iphoneBar
secret=${別のなにか適当なパスワード}
host=dynamic
canreinvite=no
transport=udp
textsupport=yes
qualify=1000
disallow=all
allow=gsm
allow=opus
allow=speex
allow=vp8
allow=ulaw
```

#### extension.conf

内線電話の配線テーブルです。
以下の設定は知らない電話番号からは留守電というひどい設定です。
とはいえ、今どきそれでいいような気もしますよ...

``` ini:extensions.conf
[globals]
;電話が終わったら起動するスクリプトの名前
MONITOR_EXEC=/savewav.py

[default]
include => denyall

[iphoneFoo]
include => outviabrastel

[iphoneBar]
include => outviabrastel

[brastelin]

;呼び出し音返す
exten => _20Z,1,Ringing

;/sleepがあるときは留守
exten => _20Z,n,Set(RUSU=${SHELL(test -f /sleep && echo -n 1 || echo -n 0)})
exten => _20Z,n,GotoIf($["${RUSU}"="1"]?vm-rec)

;/rusu.txtに番号があるときは留守
exten => _20Z,n,Set(RUSU=${SHELL(grep -q "${CALLERID(num)}" /etc/asterisk/rusu.txt && echo -n 1 || echo -n 0)})
exten => _20Z,n,GotoIf($["${RUSU}"="1"]?vm-rec)

;/deny.txtに番号があるときは、拒否
exten => _20Z,n,Set(RUSU=${SHELL(grep -q "${CALLERID(num)}" /etc/asterisk/deny.txt && echo -n 1 || echo -n 0)})
exten => _20Z,n,GotoIf($["${RUSU}"="1"]?deny)

;/contacts.txtにもhistory.txtにも番号がないときは、留守（未知の番号）
exten => _20Z,n,Set(RUSU=${SHELL(grep -q "${CALLERID(num)}" /contacts.txt /history.txt && echo -n 1 || echo -n 0)})
exten => _20Z,n,GotoIf($["${RUSU}"="0"]?vm-rec)

;SIP内線に転送を試みる
exten => _20Z,n,Dial(SIP/iphoneFoo&SIP/iphoneBar&SIP,30,tTwW)
exten => _20Z,n,GotoIf($["${DIALSTATUS}"="BUSY"]?vm-rec)
exten => _20Z,n,GotoIf($["${DIALSTATUS}"="NOANSWER"]?vm-rec)
exten => _20Z,n,GotoIf($["${DIALSTATUS}"="CHANUNAVAIL"]?vm-rec)
exten => _20Z,n,Hangup

;録音
exten => _20Z,n(vm-rec),Answer()
exten => _20Z,n,Set(CALLFILENAME=${STRFTIME(${EPOCH},Asia/Tokyo,%Y%m%d%H%M%S)}-${CALLERID(num)})
exten => _20Z,n,Monitor(wav,${CALLFILENAME},m)
exten => _20Z,n,Wait(1)
exten => _20Z,n,Voicemail(500)
exten => _20Z,n,Hangup

;完全拒否のラベル
exten => _20Z,n(deny),Hangup
exten => _20Z,n,Hangup

[outviabrastel]
exten => _0.,1,Dial(SIP/${EXTEN}@brastel,120,T)

....
```

デバッグ用にエコーテストや時報なども入れておきましょう。

[Asterisk \+ FUSION IP\-PhoneでIP電話サーバーの構築 \- Qiita](https://qiita.com/takuma_yoshida/items/855c22383f16ed96598d)

今回は単純なテキストファイルのgrepで動かしていますが、dbをつくってもっと複雑に動かすこともできます。

### コンテナ内メール環境

msmtpをいれて、

```
date | mail hoge@example.org
```

が届くようにしておきます。

[デーモンの起動しないSMTPクライアントの msmtp を試す\(sSMTP乗り換え\) – matoken's meme](https://matoken.org/blog/2018/09/18/try-msmtp-of-smtp-client-which-does-not-start-daemon-ssmtp-transfer/)

### fail2ban

SIPをあげるとやたら沢山のアクセスがきます。
びっくりするくらい来ます。
なのでfail2banで叩き落とすように設定します。
asteriskのログをコンテナの外から見られる場所に出しておいて、fail2banはコンテナの外で動かします。

[Asterisk \- Fail2ban](https://www.fail2ban.org/wiki/index.php/Asterisk)

### 留守電

留守電の保存スクリプト`/savewav.py`を作ります。
録音が終わったあとに引数に送信と受信の音声それぞれがファイルとして渡されます。
`voicemail.conf`ですでにメールは送信してくれるので、このスクリプトでメールをわざわざ送る必要もないのですが、音声認識した結果をメールしたりLINEに送ったり等ができます。

#### 音声認識

うちは問答無用でwavを60秒以下に切り刻んでGoogleに投げてますが、無音検出したほうがいいでしょう。
ただ、電話だとしてもしなくてもだいたいなんとかなります。

Juliusではちょっときつかったです。

[Google Cloud Speech APIのリアルタイム音声認識は使い物になる精度なのか？ \- Qiita](https://qiita.com/hamham/items/9b553d0759a2319ea211)
[Juliusの独自辞書を使って音声を認識させる \- Qiita](https://qiita.com/mininobu/items/322a49856e6665bc8a30)

### iCloudから連絡先をとる

iCloudのカレンダーはCalDAVというので取れるのですが、同じ方法でCardDAVというのを経由することでiOSの連絡先にアクセスすることができます。
定期的にiOSの連絡先をとってきて、`extensions.conf`で参照している`/contacts.txt`に保存しておきます。

CardDAVのライブラリは各種言語にありますし、設定内容も以下にあるような方法でいけます。

[webdav \- Getting vCards from iCloud using PHP \- Stack Overflow](https://stackoverflow.com/questions/41757153/getting-vcards-from-icloud-using-php)

iCloudの設定では多要素認証のアプリケーションパスワードを発行しておきましょう。
日本語の人名と外国の人名や法人名が混合していると姓と名の順番がちょっと面倒ですので、そこは何度か試します。

### コールバック用ログ生成

自分が発信した番号からのコールバックは留守電にそのまま流れてもらっては困るので、ログから`/history.txt`を生成するように仕掛けておきます。

## SIP端末

最後に端末側です。
今回はiOSのAcrobitsというのを使いました。
通常はSIPのコネクションをずっと維持していないと電話を受けることができず、それをするとスマホの電池などあっという間に消えてしまうのですが、Acrobitsはクラウドにあるサーバ側がSIPに代理で登録をした上で監視をしてくれ、着信があったらNotification API経由でスマホを起こしてくれます。
おかげでソフト自体は有料ではありますが...

[‎「Acrobits Softphone」をApp Storeで](https://apps.apple.com/jp/app/acrobits-softphone/id314192799)

SIPアカウントを新規作成で、タイトル、ユーザ名に上記の例なら「iphoneFoo」をいれ、パスワードやこのasteriskのホスト名をいれます。

詳細設定にはいり、

* NAT通過
    * STUN
    * メディアを返送　有効
    * Ignore Symmentric NAT 有効
    * グローバルIPを検索 外部
    * キープアライブ送信 有効
* コーデック
    * opus 有効
* 表示名
    * 「iphoneFoo」
* トランスポートプロトコル SIP
* プッシュオプション
    * シミュレートNAT 有効
* 発信者ID
    * 「iphoneFoo」

このあとログを有効にして試してみてエコーテストやacrobitsのサーバを利用したpush通知が有効なことを確認します。

ちなみに今回は暗号化ナシなのですが、pjsipで頑張ればSIPSとDTLSを有効にすることもできるはずです...が、どうもうまくいきませんでした。

# おわりに

これでもう固定電話はおろか、スマホの音声契約も断捨離できます。
ただし、音声契約を捨てて050のみにすると...

* 非常時に110, 119につながらない
* SMSが通じない時に音声で認証番号を伝えてくるタイプのサービスの登録が心もとない
* まれに飲食店の予約で「050の電話番号での予約はノーショーが多いのでお断りしています」といわれる
* たまにブラステルからはつながらない電話番号がある（0120は繋がります）
* まれに050の電話番号で登録できないサービスがある

という問題は発生します。
110, 119に関しては最寄りの警察署の一般加入電話番号を教えてくれるアプリがあるのでそれでなんとかなるのかもしれません。
なお、一部の「音声契約なしSMS専用SIM」は実際には110, 119に繋がってしまうという噂もあります。

[050のIP電話は緊急電話をかけられない？対処法はこれだ！](https://narupod.com/050-110119/)

といっても楽天モバイルは「着信通話を受ける、RSCでの通話なら0円」ですから無理に捨てることもないのかもしれませんね。
最近のiPhoneに楽天モバイルの物理SIMとMVNO eSIMを入れれば大変安くなります。