Microsoft TeamsとopenSIPSを接続する

# 前提条件

- Microsoft Teamsが有効なMicrosoft 365アカウント
    - Microsoft 電話システムのライセンスが必要
    - example.onmicrosoft.comのようなドメインではなく、独自ドメインを利用している必要あり
    - 例：example.com
- openSIPS 3.3
    - 標準モジュールに追加でdomain/permissions/TLS(proto_tls, tls_openssl,tls_mgm)/droutingが有効になっていること
- openSIPSが動作しているホストのIPアドレスが引けるホスト名があること
    - 例：sbc1.example.com(このドメインはMicrosoft 365で利用しているドメインと一致している必要あり)
- SSL証明書(例：sbc1.example.com)
    - [ダイレクト ルーティングを計画する - SBC 用の公開された信頼できる証明書 - Microsoft](https://docs.microsoft.com/ja-jp/microsoftteams/direct-routing-plan#public-trusted-certificate-for-the-sbc)に記載されているルート証明機関である必要あり
    - 2022/8/20 現在は、Let's Encryptionの証明書のルート証明機関も含まれている(ISRG Root X1	)

当然ながら、Microsoftの認定を受けたSBCでは無いため、試すときは自己責任でお願いします。
別記事で、AsteriskとTeamsを繋げてますが、Asteriskはソースコードの書き換えが必要で、毎回手間なので
openSIPSで試してみました。始めてopenSIPSを使っているので、本当にとりあえず動いているレベルです。

# 設定(Microsoft 365側)

別記事にまとめていますので、そちらを参照してください
- [Microsoft Teamsでダイレクトルーティングを構成する](https://qiita.com/tatsuya_info/items/23825e98d2a3cb6443ba)

# 事前準備(openSIPS側)

openSIPSを導入しておきます。

openSIPSは設定ファイルと別に構成情報を保存するデータベースが必須のようです。
(とりあえず、MySQLで動くことは確認済みで、時間が有ったらSQLiteで試したい)

# 設定(openSIPS側)

既存システムともTLSで接続をしているので、そのあたりは適当に読み替えてください。

```opensips.cfg
## UDPソケットは使わないのでコメントアウトします。(既存システムと繋ぐときに使う残す)
#socket=udp:127.0.0.1:5060   # CUSTOMIZE ME
## 同様に proto_udp のモジュールもコメントアウトします。
#loadmodule "proto_udp.so"

## TLSソケットを開ける 下記はプライベートIPアドレスだけど、NAT越えは面倒なのでグローバル前提
socket=tls:192.168.0.1:5061

### 各種モジュール
## データベースにMySQLを使う
loadmodule "db_mysql.so"
## domainモジュール(リクエストが自分のSIPドメインかチェック)
loadmodule "domain.so"
modparam("domain", "db_url", "mysql://dbuser:dbpass@localhost/opensips")

## permissionモジュール(ACL関係)
loadmodule "permissions.so"
modparam("permissions", "db_url", "mysql://dbuser:dbpass@localhost/opensips")

## drouting(dynamic routing)
loadmodule "drouting.so"
modparam("drouting", "db_url","mysql://dbuser:dbpass@localhost/opensips")

## TLS関係
loadmodule "proto_tls.so"
# この辺は細かく調整してない
modparam("proto_tls", "tls_handshake_timeout", 1000)
modparam("proto_tls", "tls_send_timeout", 1000)
modparam("proto_tls", "tls_max_msg_chunks", 8)

loadmodule "tls_openssl.so"

loadmodule "tls_mgm.so"
# server_domain/client_domainのあたりもよく理解してない
# verify_certを1にすると上手く動かない
modparam("tls_mgm","server_domain", "sbc1.example.com")
modparam("tls_mgm","match_ip_address", "[sbc1.example.com]*")
modparam("tls_mgm","verify_cert", "[sbc1.example.com]0")
modparam("tls_mgm","require_cert", "[sbc1.example.com]0")
modparam("tls_mgm","tls_method", "[sbc1.example.com]TLSv1_2")
modparam("tls_mgm","certificate", "[sbc1.example.com]/etc/letsencrypt/live/sbc1.example.com/cert.pem")
modparam("tls_mgm","private_key", "[sbc1.example.com]/etc/letsencrypt/live/sbc1.example.com/privkey.pem")
modparam("tls_mgm","ca_list", "[sbc1.example.com]/etc/letsencrypt/live/sbc1.example.com/fullchain.pem")
modparam("tls_mgm", "ca_dir", "[sbc1.example.com]/etc/ssl/certs/")

modparam("tls_mgm", "client_domain", "defaultdom")
modparam("tls_mgm", "match_ip_address", "[defaultdom]*")
modparam("tls_mgm", "match_sip_domain", "[defaultdom]*")
modparam("tls_mgm", "verify_cert", "[defaultdom]0")
modparam("tls_mgm", "require_cert", "[defaultdom]0")
modparam("tls_mgm", "tls_method", "[defaultdom]tlsv1_2")
modparam("tls_mgm", "certificate", "[defaultdom]/etc/letsencrypt/live/sbc1.example.com/cert.pem")
modparam("tls_mgm", "private_key", "[defaultdom]/etc/letsencrypt/live/sbc1.example.com/privkey.pem")
modparam("tls_mgm","ca_list", "[defaultdom]/etc/letsencrypt/live/sbc1.example.com/fullchain.pem")
modparam("tls_mgm", "ca_dir", "[defaultdom]/etc/ssl/certs/")

## 「route {」の上に追加した
## Teamsから送信されるOPTIONメッセージの返答に必要なContactヘッダーを追加
##「($(ru{s.index, $var(dst)}) != NULL)」の部分の調査必要
local_route {
  $var(dst) = "pstnhub.microsoft.com";
  if (is_method("OPTIONS") && ($(ru{s.index, $var(dst)}) != NULL))
    append_hf("Contact: <sip:sbc1.example.com:5061;transport=tls>\r\n");
}

## 「route {」の中に追記・「if (has_totag()) {」の前
## Teamsから送信されるOPTIONメッセージへ200 OKで返す
## check_source_addressの中の2は後で出てくる、addressテーブルのgrpと紐付いてる
	# Checks from MS Teams
	if(is_method("OPTIONS") && is_domain_local("$rd") && check_source_address(2)) {
	  xlog("L_INFO", "[MS TEAMS] OPTIONS In\r\n");
	  send_reply(200, "OK");
	  exit;
	}

## 「	# record routing」の下
	if (!is_method("REGISTER|MESSAGE")){
# 初期値 コメントアウト
#		record_route();

		# 多分この書き方だと、Teams以外に影響ありそう（専用ならいいんだけろうけど)
		record_route_preset("sbc1.example.com:5061;transport=tls");
	}

##「	# do lookup with method filtering」の前
# これもいまいちだけど、とりあえずTeamsからはE.164形式で飛んでくるので、それと一致したらTeams関係とする
	if ($rU=~"^\+[0-9]+") {
	  xlog("L_INFO", "[LOGGER] INVITE\r\n");

		if (!is_from_local()) {
			# ローカル宛でないのは処理しない 403で返す
			send_reply(403,"Forbidden access to media service");
			exit;
		}else if(check_source_address(2)){
			# ソースアドレスチェックし、グループ2(Teams側)だったら既存にRelay
			xlog("L_INFO", "[LOGGER] FROM TEAMS\r\n");
			t_relay(2,"tls:relay-to-sip-proxy.example.com:5061");
			exit;
		}else if(check_source_address(1)){
			# ソースアドレスチェックし、グループ1(既存側)だったらContactヘッダー追加してTeamsにRelay
			xlog("L_INFO", "[LOGGER] FROM LOCAL\r\n");
			append_hf("Contact: <sip:sbc1.example.com:5061;transport=tls>\r\n");
			 t_relay(2,"tls:sip.pstnhub.microsoft.com:5061");
			 exit;
		}
	}
```

## データベースに登録が必要なレコード

とりあえず、MySQLのクエリのまま

```
## drouting関係(Teams側でOPTIONメッセージによるSBC監視を行うため)
## 最後のパラメタはListenに書いた内容と合わせる
insert into dr_gateways (gwid,address,probe_mode,state,socket) VALUES ('ms1','sip.pstnhub.microsoft.com',2,0,'tls:192.168.0.1:5061');
insert into dr_gateways (gwid,address,probe_mode,state,socket) VALUES ('ms2','sip2.pstnhub.microsoft.com',2,0,'tls:192.168.0.1:5061');
insert into dr_gateways (gwid,address,probe_mode,state,socket) VALUES ('ms3','sip3.pstnhub.microsoft.com',2,0,'tls:192.168.0.1:5061');

## ACL 既存システム側
insert into address set grp=1, ip='192.168.2.1'
## ACL Teams側(実際は下記URLを参照し登録するサブネット確認してください)
## https://docs.microsoft.com/ja-jp/microsoftteams/direct-routing-plan#sip-signaling-fqdns
insert into address set grp=2, ip='52.112.0.0', mask=14;
insert into address set grp=2, ip='52.120.0.0', mask=14;

## 自分が処理すべきSIPドメイン
## Teams側
insert into domain set domain='sip.pstnhub.microsoft.com';
## 既存側
insert into domain set domain='sbc1.example.com';
```

# パケットフィルタ

ここも、Asteriskで接続している記事を参照してください。
- [ Microsoft TeamsとAsteriskを接続する](https://qiita.com/tatsuya_info/items/7221438f2953e17718fe#パケットフィルタ)

ただ、openSIPSはRTP中継しないのでSIPだけ開ければ大丈夫です。

# 参考にしたサイト

- [OpenSIPS as MS Teams SBC](https://blog.opensips.org/2019/09/16/opensips-as-ms-teams-sbc/)
- [Kamailio as SBC for MS Teams](https://skalatan.de/en/blog/kamailio-sbc-teams)
    - Kamailioでも同じ事が出来るようだったけど、とりあえずopenSIPSで試した