Bluetoothでシリアルコンソール接続する (CentOS 7)

この記事では、 CentOS 7 のマシンに他の端末から Bluetooth によるシリアルコンソールログインができるようにする方法を紹介します。

他のディストリビューションでもやり方は少し違うと思いますが、同じように実現できると思います。

# まえがき

Linux マシン（特にサーバ用途）を操作する際、別の端末からネットワーク越しに SSH などで接続することが多いと思います。しかし、 SSH 接続中に以下のようなことをやらかして SSH 接続できなくしてしまったことはありませんか？

- ``/etc/init.d/network stop`` (``systemctl stop network``) してしまった
- ルーティングなどネットワークの設定をいじったら SSH クライアントとの疎通がとれなくなってしまった
- sshd を ``kill`` してしまった

こういうときのために（？）、シリアルコンソールが有効になっていると便利です。 Linux マシンと端末側マシンのシリアルポート同士を接続して、シリアルコンソール経由でログインができます。しかし、最近の一般的な PC からはシリアルポートが搭載されなくなってしまいました。[^1]

[^1]: D-Sub9 ピンの RS-232C というやつですね。ひと昔前？くらいから無くなってしまいました。 USB-シリアル変換ケーブルもありますが、ひとつ2千円くらいします。シリアルポートがないマシン同士を接続するには、それぞれに変換ケーブルをつないで、その間をシリアルケーブルで接続することになります。お金もかかるし、ちょっとナンセンスな方法だと思うのは私だけではないはず。

そこで Bluetooth の登場です。最近の PC からシリアルポートはなくなってしまったものの、 Bluetooth は多くの PC に搭載されています（搭載されておらずとも USB ドングルは比較的安価に入手可能）。そして Bluetooth でもシリアル通信が可能です。これを使ってシリアルコンソール接続ができるようにしてみたので、その手順を公開します。

# 環境

OS は CentOS 7 、インストール時には GUI デスクトップ環境を選択しているため、 minimal インストールなどでは導入されていないパッケージもあると思います。必要に応じて ``yum install`` してください。

また、 SELinux のポリシーを作成して組み込むことで、 SELinux が有効な状態で使えるようにしています。 SELinux を無効 (Permissive) にしている場合は、「SELinux ポリシーの作成と適用」の手順は飛ばしても利用できます。[^2]

[^2]: Permissive の場合でも、 SELinux の手順を飛ばすと Audit Log が多く吐かれるので、やっといて損はないと思います。というか、状況にもよると思いますが、否応なしに SELinux を無効化する時代はそろそろ終わりだと思う。

# なにはともあれ手順

まずは Linux マシン側の設定を行い、その後端末側マシンとのペアリングを行います。ペアリング済みの端末からのみシリアルコンソール接続ができます。

なお、 Bluetooth 関連の用語やコマンドの詳細については説明していません。というか自分自身も手探りで実施したため、説明できるほど理解できていません…。

## Linux マシンの設定

まずはシリアルコンソール接続される側の Linux マシンの設定を行います。

### 前準備: Bluetooth が利用可能か確認する

とにもかくにも、 Bluetooth が利用できなければ話が始まらないので、まずは利用できるか確認します。

以下のように dmesg を ``grep`` することで確認できます。

```shell-session
$ grep -i bluetooth /var/log/dmesg
[    9.890172] Bluetooth: Core ver 2.22
[    9.890205] Bluetooth: HCI device and connection manager initialized
[    9.890211] Bluetooth: HCI socket layer initialized
[    9.890215] Bluetooth: L2CAP socket layer initialized
[    9.890223] Bluetooth: SCO socket layer initialized
[    9.930770] Bluetooth: hci0: read Intel version: 370810011003110e00
[    9.936104] Bluetooth: hci0: Intel Bluetooth firmware file: intel/ibt-hw-37.8.10-fw-1.10.3.11.e.bseq
[   10.271789] Bluetooth: hci0: Intel Bluetooth firmware patch completed and activated
```

### bluetoothd の設定ファイル編集

/etc/bluetooth/main.conf を編集して AutoEnable を有効化します。デフォルトでは Linux マシン起動時に Bluetooth が無効化されていますが、これで起動時から有効化されます。[^3]

[^3]: 起動後に任意のタイミングで有効化/無効化したい場合は、 ``bluetoothctl`` にて ``power on`` / ``power off`` で可能です

該当箇所はファイルの最下部にあります。

```diff:/etc/bluetooth/main.conf
-AutoEnable=false
+#AutoEnable=false
+AutoEnable=true
```

### systemd の設定

systemd 関連では、 bluetooth.service の起動時の動作に手を加え、さらにシリアル接続時にログインプロンプトを出すための設定を行います。

#### bluetooth.service 起動時に SPP 追加

/etc/system/systemd/ 配下に bluetooth.service.d ディレクトリを作成し、その中に Drop-in 設定ファイルを作成します。ファイル名は、末尾が ".conf" であれば大方何でも行けると思います。

```ini:/etc/system/systemd/bluetooth.service.d/add-spp.conf
[Service]
ExecStart=
ExecStart=/usr/libexec/bluetooth/bluetoothd --compat
ExecStartPost=/usr/bin/sdptool add SP
```

この設定ファイルによって、 bluetoothd を起動した際に Bluetooth でシリアル通信を行うための SPP (Serial Port Profile) を追加しています。 ``bluetoothd`` に --compat （または -C）オプションをつけて起動しないと ``sdptool`` が使えません。

#### rfcomm-getty ユニットファイル

/etc/system/systemd/ 配下にも新たにファイルを作成します。

※ /usr/lib/systemd/system/serial-getty@.service を参考に作成

```ini:/etc/system/systemd/rfcomm-getty@.service
[Unit]
Description=RFCOMM Getty on %I
Documentation=https://qiita.com/tetsuy/items/d9220ac66bd18fd0a01b
After=bluetooth.service
Requires=bluetooth.service
Before=getty.target
IgnoreOnIsolate=yes

[Service]
ExecStart=/bin/rfcomm watch %I 1 /sbin/agetty 115200,38400,9600 %I
UtmpIdentifier=%I
TTYPath=/dev/%I
TTYReset=yes
TTYVHangup=yes

[Install]
WantedBy=getty.target
```

``rfcomm watch`` はシリアル接続を待ち受け、接続されると RFCOMM デバイス (/dev/rfcommX) を作成して指定のコマンドを実行（子プロセスを起動）します。ここで ``agetty`` を実行するよう指定し、それに /dev/rfcommX を渡してやることで、シリアル接続元にログインプロンプトが表示されます。

#### systemd 設定の反映

なお、ちょっと話が逸れますが systemd のユニットファイルの編集について、 ``/usr/lib/systemd/system/`` 以下のファイルを編集すると説明されている記事もありますが、作法としては**間違い**です。（systemd の Update 時にこのディレクトリ以下のファイルが更新されることもある）

参考: [Red Hat Enterprise Linux 7 システム管理者のガイド / 10.6.4. 既存のユニットファイルの変更](https://access.redhat.com/documentation/ja-jp/red_hat_enterprise_linux/7/html/system_administrators_guide/sect-Managing_Services_with_systemd-Unit_Files#sect-Managing_Services_with_systemd-Unit_File_Modify)

話を戻して、上記2ファイルが作成できたら systemd に反映・有効化します。

```shell-session
# systemd daemon-reload
# systemd enable rfcomm-getty@rfcomm0.service
```

### udev ルールの作成

続いて、/etc/udev/rules.d/ 配下に新たなルールのファイルを作成します。

```config:/etc/udeb/rules.d/90-rfcomm.rules
KERNEL=="rfcomm[0-9]*", ENV{ID_MM_DEVICE_IGNORE}="1"
```

これをしておかないと、端末から Bluetooth 接続した際に作成される RFCOMM デバイスファイル /dev/rfcomm0 に ModemManager が反応して、端末に AT コマンドらしきデータを送信してしまいます（ログインプロンプトに変な文字が割り込んでくる）。

``systemd {stop,disable} ModemManager`` してもよいですが、デフォルトで有効になっているサービスなので、何かの時にハマらないよう除外ルールの追加としました。

### SELinux のポリシー作成と適用

今回、 SELinux 関連でハマって色々と調べた結果、 "SELinux 食わず嫌い" から一歩踏み出すことができました。私と同じように食わず嫌いしている人向けに、もっと詳細に書きたいのは山々なのですが、ボリュームが増えすぎてしまうため割愛します…。

#### 作成するポリシーの方針

SELinux が有効な環境では、 ``rfcomm`` を シェルから手動で起動すれば問題なく動作するのですが、 systemd からデーモンとして立ち上げようとするとうまく動作しませんでした。原因を調査すると、以下のことがわかりました。

- シェルから起動したプロセスには "unconfined_t" というドメインが付与される。このプロセスは SELinux による制限なく動作することができる
- 一方で systemd からデーモンとして起動すると、「ドメイン遷移」によりプロセス独自のドメイン（``rfcomm`` の場合 "bluetooth_t"）が付与される
- "bluetooth_t" ドメインのプロセスから ``agetty`` を立ち上げて利用することが想定されていないためか、デフォルトではそのような動作を許可するポリシーが入っていない<br>
→ **SELinux によって動作が拒否されてしまう**

該当する SELinux のポリシーがないなら自分で作成しよう、ということで Permissive モードで ``ausearch`` コマンドを使って地道に潰していきました。作成したポリシーの中身ですが、ざっくり言うと以下の4点です。

1. ``rfcomm`` が子プロセスを起動できるようにする
2. ``rfcomm`` が ``agetty`` を子プロセスとして起動する際、そのドメインを "getty_t" にドメイン遷移させる
3. ``rfcomm`` と ``agetty`` 関連プロセス間でシグナルのやり取りができるようにする
4. ``agetty`` 関連プロセスから Bluetooth 関連リソース（/dev/rfcomm0 など）へのアクセスができるようにする

この中で特にポイントなのが 2. です。

``agetty`` プロセスからログイン関連のリソースへのアクセスは、当然ながらデフォルトで許可されています。しかし、今回のケースではその動作は拒否されてしまいます。これは、 ``agetty`` プロセスは "getty_t" というドメインで動作する前提で許可のポリシーが設定されているにもかかわらず、今回のケースでは ``agetty`` プロセスが "bluetooth_t" ドメインで動作してしまうためです（子プロセスは親プロセスのドメインを引き継ぐため、 "bluetooth_t" ドメインの ``rfcomm`` によって ``agetty`` が起動されるとそうなってしまう）。

![ドメインを継承すると agetty がログイン関連のリソースを参照できない](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/60164/101e170c-99d6-90b2-0e94-5f804752a796.png)

通常 SELinux ポリシーを作成する場合、 Permissive モードにした上で ``ausearch`` コマンドを使い、 Audit Log に上がっている SELinux に拒否された動作をひとつずつ許可するルールを書いていきます。しかし今回のケースでこれを愚直にやってしまうと、最終的に "bluetooth_t" ドメインのプロセスがログイン関連のリソースへアクセスすることを許可することになってしまいます。恐らく大量のルールを書くことになりますし、セキュリティ的にもよろしくなさそうです。

そこで、``rfcomm`` が ``agetty`` を起動する際に本来の "getty_t" ドメインでプロセスが立ち上がるように、ドメイン遷移のルールを書いています。

![ドメイン遷移によって agetty がログイン関連のリソースを参照できる](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/60164/590ff36b-6415-3694-fbd8-2ff5ebf04e46.png)

こうすることで、デフォルトの許可ポリシーに従って ``agetty`` が動作することができるようになるというわけです。

#### ポリシーの作成・適用方法
 
さて、説明が長くなってしまいましたが、ポリシーの作成と適用方法です。

SELinux のポリシーファイルを生成するために selinux-policy-devel パッケージが必要なのでインストールします。

```shell-session
# yum install selinux-policy-devel
```

さらにポリシーファイルのもとになる te ファイル (Type Enforcement) も作成します。場所はどこでも OK ですが、仮に /root/selinux/ としています。 ``make`` を実行するとディレクトリを含む複数のファイルが生成されるため、新たに専用のディレクトリを作成するほうがいいです。

```/root/selinux/rfcomm-getty.te

module rfcomm-getty 1.0;

require {
        type bluetooth_t;
        type getty_exec_t;
        type getty_t;
        type local_login_t;
        class file { execute open read };
        class process { transition sigchld signal };
        class socket { read write };
}

allow bluetooth_t getty_exec_t:file { execute open read };
allow bluetooth_t getty_t:process { transition sigchld };
allow bluetooth_t local_login_t:process signal;
type_transition bluetooth_t getty_exec_t:process getty_t;
allow getty_t bluetooth_t:socket { read write };
allow getty_t bluetooth_t:process sigchld;
allow local_login_t bluetooth_t:process sigchld;
allow local_login_t bluetooth_t:socket { read write };
```

作成したこの te ファイルから pp ファイル (Policy Package) を生成し、そのポリシーを適用します。

```shell-session
# cd /root/selinux
# make -f /usr/share/selinux/devel/Makefile rfcomm-getty.pp
/usr/share/selinux/devel/include/contrib/container.if:14: Error: duplicate definition of container_runtime_domtrans(). Original definition on 14.
...snip...
/usr/share/selinux/devel/include/contrib/container.if:498: Error: duplicate definition of container_runtime_typebounds(). Original definition on 684.
Compiling targeted rfcomm-getty module
/usr/bin/checkmodule:  loading policy configuration from tmp/rfcomm-getty.tmp
/usr/bin/checkmodule:  policy configuration loaded
/usr/bin/checkmodule:  writing binary representation (version 19) to tmp/rfcomm-getty.mod
Creating targeted rfcomm-getty.pp policy package
rm tmp/rfcomm-getty.mod.fc tmp/rfcomm-getty.mod
```

```shell-session
# ls
rfcomm-getty.fc
rfcomm-getty.if
rfcomm-getty.pp
rfcomm-getty.te
tmp
```

やたらエラーが表示されますが、 rfcomm-getty.pp が生成されていれば問題ありません。

``semodule -i`` で生成されたポリシーをインストールします。

```shell-session
# semodule -i rfcomm-getty.pp
```

### マシン再起動

ここまで来たら、あとはマシンを再起動すれば Bluetooth でシリアルコンソールが待ち受け状態となるはず。

## Windows から Bluetooth シリアルコンソール接続

ここからは接続しに行く端末側マシンを Linux マシンにペアリングしてシリアルコンソールでログインする手順です。

Bluetooth によるシリアルコンソール接続を行うためには、あらかじめ Linux マシンと端末マシンをペアリングしておく必要があります。逆に言えば、あらかじめペアリングしていない端末からはBluetooth によるシリアルコンソール接続はできないということになります。

### ペアリング

Bluetooth で機器同士が通信するには、まずペアリングする必要があります。一方の機器を周囲のデバイスから発見可能状態にした上で、もう一方の機器からその機器にペアリング要求を送信することとなります。

ここでは Windows 10 のマシンからペアリングする例を説明します。[^4]

[^4]: 反対に Linux マシン側からペアリングしにいってもおそらく同様に使えるのではないかと思いますが（未検証）、手順的にはこちらのほうがラクだと思います。

#### Linux マシンを発見可能に

まずは ``bluetoothctl`` から Linux マシン側を発見可能状態にします。

```shell-session
# bluetoothctl
[NEW] Controller XX:XX:XX:XX:XX:XX HOSTNAME [default]
[bluetooth]# show
Controller XX:XX:XX:XX:XX:XX
        Name: HOSTNAME
        Alias: HOSTNAME
        Class: 0x000104
        Powered: yes
        Discoverable: no
        Pairable: yes
        UUID: Generic Attribute Profile (00001801-0000-1000-8000-00805f9b34fb)
        UUID: A/V Remote Control        (0000110e-0000-1000-8000-00805f9b34fb)
        UUID: PnP Information           (00001200-0000-1000-8000-00805f9b34fb)
        UUID: Generic Access Profile    (00001800-0000-1000-8000-00805f9b34fb)
        UUID: A/V Remote Control Target (0000110c-0000-1000-8000-00805f9b34fb)
        Modalias: usb:v1D6Bp0246d052C
        Discovering: no
```

bluetoothctl のプロンプトで ``show`` と打つと、コントローラの情報が見られます。ここで "Powered: yes" , "Pairable: yes" となっていることを確認して ``discoverable on`` とすると、デフォルトでは3分間 Linux マシンが Bluetooth で発見可能状態となります。

```
[bluetooth]# discoverable on
Changing discoverable on succeeded
[CHG] Controller XX:XX:XX:XX:XX:XX Discoverable: yes
[bluetooth]#
```

なお ”Pairable: no” となっていると発見可能であってもペアリングに失敗します。　``pairable on`` でペアリング可能になります。

発見可能となっている間に端末マシン側からペアリングを行います。

#### Windows マシンとのペアリングと COM ポート割り当て

Windows マシンからのペアリング方法について説明します。もちろん手順は異なりますが他の OS からでも接続できます。

1. Windows の「設定」→「デバイス」→ 「Bluetooth とその他のデバイス」で、Bluetooth がオンになっているのを確認して、「Bluetooth またはその他のデバイスを追加する」
2. 「デバイスを追加する」ウィンドウが開いたら「Bluetooth」を選択し、一覧に Linux マシンのホスト名が表示されるのでクリックする

これだけでペアリングは完了すると思います（Linux 側の ``bluetoothctl`` のプロンプトにもペアリングされたことが表示されます）。

さらに、シリアルポートの割当を確認します。 SPP が有効な状態でペアリングすると、自動的に COM ポートが2つ（着信用、発信用）割り当てられると思います。

1. 設定の「Bluetooth とその他のデバイス」で、下の方にスクロールすると「関連する設定」のところに「その他の Bluetooth オプション」があるのでクリック
2. 「Bluetooth 設定」ウィンドウが開くので「COM ポート」タブをクリック

![bluetooth_settings_comports.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/60164/49595b73-4503-b2c6-bf27-a087f7a4f19a.png)

ここの「発信」に割り当てられているポート（この例では COM4）を開くことで、シリアルコンソールに接続することができます。


### シリアルコンソール接続

COM ポートを開くには、最も手っ取り早いのは、いつも使っている方も多いであろう Tera Term を使います。
![teraterm_serialport.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/60164/b7a9935e-328a-d4dc-1a50-e288d0568a8b.png)

SSH で接続する場合はホスト名とポートを指定しますが、「シリアル」を選択して先ほどの COM ポートを選択して「OK」します。これだけ。画面にはログインプロンプトが表示されると思います。

# 未解決な点

端末から Bluetooth で接続した際、以下のようなエラーログが吐かれています。

```
/dev/rfcomm0: cannot get controlling tty: 許可されていない操作です
/dev/rfcomm0: cannot get controlling tty: 許可されていない操作です
/dev/rfcomm0: cannot set process group: デバイスに対する不適切なioctlです
```

TTY まわりのことがまだイマイチ理解できていないところがあるなぁ…

原因がわかる方、教えて下さい！

# 参考

参考にさせていただいたサイトや記事です。ありがとうございました。

- [Raspberry Pi 2にBluetoothシリアル通信でデータ送受信してみた - カタカタブログ](http://totech.hateblo.jp/entry/2017/03/15/093608)
- [Raspberry Pi と Bluetooth 経由でシリアル通信する - mattintosh note](https://mattintosh.hatenablog.com/entry/20170702/1498985041)
- [Bluetooth で通信が可能になるまで - Qiita](https://qiita.com/propella/items/d961c93c9f5c2aa53881)
- [Linux のコマンドラインで Bluetooth 接続 - Qiita](https://qiita.com/propella/items/6daf3c56e26f709b4141)
- [Armadillo-X1：Bluetoothでシリアル通信 | Armadillo](https://armadillo.atmark-techno.com/blog/615/4246)
- [「SELinuxのせいで動かない」撲滅ガイド - Qiita](https://qiita.com/yunano/items/857ab36faa0d695573dd)
- [SELinuxのセキュリティ設定を理解する3つのポイント | 日経クロステック（xTECH）](https://xtech.nikkei.com/it/atcl/column/17/041900153/052600005/)
- [SELINUX CapmNetwork](http://capm-network.com/?tag=SELINUX)
- [SELinuxを独学したよ。 - 簡潔なQ](https://qnighy.hatenablog.com/entry/20110226/1298724379)

# おまけ: Android のターミナルアプリから接続

Android で ConnectBot というターミナルアプリを愛用しているのですが、このアプリは Tera Term のようにシリアル接続には対応していません。

Play ストアで「Bluetooth serial」とかで検索すれば、 Bluetooth のターミナルアプリが山ほど出てくるのですが、やはりいつも使っているアプリに匹敵する使い勝手のものはありませんでした。

そこで！登場するのが、 "[Bluetooth Bridge (+TCP)](https://play.google.com/store/apps/details?id=masar.bb)" というアプリです。

これは、2つの Bluetooth もしくは TCP の通信をブリッジしてくれるアプリです。一方は Bluetooth のシリアル接続、もう一方は TCP Server として任意のポートで Listen して双方向の通信をブリッジしてやれば、あとはいつものターミナルアプリから、 localhost の指定したポートに Telnet 接続すれば OK!

このアプリの回し者でもなんでもないですが、なかなか Good Job なアプリだったので紹介してみました。