NGINXのStreamモジュールでMQTT <=> MQTTS変換をしてAWS IoT Coreへ接続する

IoTでよく使われるプロトコル「MQTT」ですが、クラウド側のブローカー(MQTTサーバー)がMQTTS(MQTT+TLS)を要求するケースが多くなり、MCU側への実装負担が増えています。

このエントリーでは、Proxy上でNGINXを動かし、[Streamモジュール](http://nginx.org/en/docs/stream/ngx_stream_proxy_module.html)を使用して MQTT <=> MQTTS 変換の方法を解説します。
MQTTSブローカーとして、今回は[AWS IoT Core](https://aws.amazon.com/jp/iot-core/)をターゲットにします。

```plantuml
MCU <-> Proxy: MQTT
Proxy -> Proxy: NGINXでTLS化
Proxy <-> "MQTTSブローカー": MQTTS
```

### 注意

MCUとProxy間のセキュリティは別途確保してください。
例としては[IPSec](https://ja.wikipedia.org/wiki/IPsec)や[WireGuard](https://www.wireguard.com/)といったVPNや、[3G/LTE/5G等セルラー通信](https://www.gsma.com/iot/wp-content/uploads/2018/07/CLP.14-v2.0_JPN.pdf#page=26)による通信路レイヤーによるセキュリティ確保の方法があります。

[ESP32(M5Stack Basic)上でWireGuardを使用したMQTT実装サンプル](https://qiita.com/ma2shita/items/0f0dce8ff0e45cf9fbac) も併せてご覧ください。

### 確認環境

* Proxy
    * Raspberry Pi 4 model B
        * Raspberry Pi OS (Release date: May 7th 2021)
* MCU
    * M5Stack Basic
        * ボード定義: ESP32(1.0.6)
        * ライブラリ: M5Stack(0.3.6)

## Proxyの構成 / Raspberry

### 事前準備

AWS IoT Coreにてクライアント証明書を発行し、 `**-certificate.pem.crt` と `**-private.pem.key` ファイルを入手してから、Proxyに何らかの方法で保存してください。
また、AWS IoT Core のエンドポイント情報を入手しておいてください。

ここでは

- `/home/pi/a/xxxxxxxxxx-certificate.pem.crt`
- `/home/pi/a/xxxxxxxxxx-private.pem.key`
- `foobar-ats.iot.region.amazonaws.com`

とした前提で、以降の設定などを解説します。


### NGINXのインストールから動作確認まで

```console:shell
sudo apt update
sudo apt install -y nginx libnginx-mod-stream
```

```nginx:/etc/nginx/modules-available/mqtt_proxy.conf
stream {
  upstream mqtt {
    server foobar-ats.iot.region.amazonaws.com:8883;
  }
  server {
    listen 1883;
    proxy_pass mqtt;
    proxy_ssl on;
    proxy_ssl_certificate /home/pi/a/xxxxxxxxxx-certificate.pem.crt;
    proxy_ssl_certificate_key /home/pi/a/xxxxxxxxxx-private.pem.key;
  }
}
```

```console:shell
(cd /etc/nginx/modules-enabled/ && sudo ln -s ../modules-available/mqtt_proxy.conf ./55-mqtt_proxy.conf)
sudo systemctl reload nginx
```

最終的にReloadに成功していればOKです。また、netstatで1883でLISTENしていることも確認できます。

```console
$ sudo journalctl -u nginx -n 2
-- Logs begin at Thu 2019-02-14 19:11:59 JST, end at Sat 2021-10-02 01:40:37 JST. --
Oct 02 01:39:33 rpi3 systemd[1]: Reloading A high performance web server and a reverse proxy server.
Oct 02 01:39:33 rpi3 systemd[1]: Reloaded A high performance web server and a reverse proxy server.
```

```console
$ netstat -natu | grep 1883
tcp        0      0 0.0.0.0:1883            0.0.0.0:*               LISTEN
```

#### 動作確認 

mosquittoを使います。
あらかじめAWS IoT CoreのMQTTテストクライアントで `#` (=すべてのMQTTトピック) をサブスクライブしておくと確認しやすいです。

```
sudo apt install mosquitto
mosquitto_pub -d -h localhost -p 1883 -t a/a -m '{"foo": "bar"}'
```

これでAWS IoT Coreの `a/a` トピックに `{"foo": "bar"}` が着信していることが確認できるはずです。

### ちょっと解説

AWS IoT CoreのMQTTSはX.509証明書の他に[Server Name Indication(SNI)拡張を要求](https://docs.aws.amazon.com/ja_jp/iot/latest/developerguide/transport-security.html)します。
NGINXのStreamモジュール(ngx_stream_proxy_module)は、その辺いい感じにやってくれるようです。
※ちゃんと調べてなくてゴメンナサイ

## MCU / M5Stack Basic側

[arduino-mqtt](https://github.com/256dpi/arduino-mqtt)でMQTT接続します。

```cpp:m5stack_mqtt_with_proxy.ino
/*
   Copyright 2021 Kohei "Max" MATSUSHITA.

   Licensed under the Apache License, Version 2.0 (the "License");
   you may not use this file except in compliance with the License.
   You may obtain a copy of the License at

     http://www.apache.org/licenses/LICENSE-2.0

   Unless required by applicable law or agreed to in writing, software
   distributed under the License is distributed on an "AS IS" BASIS,
   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
   See the License for the specific language governing permissions and
   limitations under the License.
*/
#include <M5Stack.h>

#include <WiFi.h>
char WIFI_SSID[] = "{YOUR_WIFI_SSID}";
char WIFI_PASSWORD[] = "{YOUR_WIFI_PASS}";
WiFiClient net;

#include <PubSubClient.h>
PubSubClient mqtt = PubSubClient(net);

#define THING_NAME      "M5StackBasic0"
#define ENDPOINT        "proxy-server.local"
#define PORT             (1883)
#define PUBLISH_TOPIC   "demo0/via_proxy"

void connectToNetwork() {
    Serial.print("Connecting to Wi-Fi: ");
    WiFi.mode(WIFI_STA);
    WiFi.begin(WIFI_SSID, WIFI_PASSWORD);
    while (WiFi.status() != WL_CONNECTED){
        delay(200);
        Serial.print(".");
    }
    Serial.println("done.");
}

void connectToAWSIoTCore() {
    Serial.print("Connecting to AWS IoT Core via Proxy: ");
    mqtt.setServer(ENDPOINT, PORT);
    while (!mqtt.connect(THING_NAME)) {
        Serial.print(".");
        delay(200);
    }
    Serial.println("Connected!");
}

void setup() {
    Serial.begin(115200);
    connectToNetwork();
    connectToAWSIoTCore();
}

void publishMessage() {
    char payload[512];
    sprintf(payload, "{\"time\": %lu}", millis());
    mqtt.publish(PUBLISH_TOPIC, payload);
}

#define LOOP_INTERVAL_MS (10000)
void loop() {
    Serial.println("loop");
    publishMessage();
    unsigned long next = millis();
    while (millis() < next + LOOP_INTERVAL_MS) {
        mqtt.loop();
    }
}
```


### メモリーの使用量

M5Stackで直接MQTTS(MQTTL+TLS)を実装した場合と比較して、メモリーの使用量はフラッシュメモリで約163KBも節約できています。これは大きいかと思います。

|  | フラッシュメモリ | RAM |
|--|-----------------:|-----:|
| MQTTのみ | 748,302 | 39,912 |
| MQTT+TLS | 911,762 | 40,456 |
| 差(節約量) | -163,460 | -544 |

※単位はbyte

<details><summary>参考コード) MQTT+TLS 版</summary><div>

```cpp:m5stack_mqtts.ino
/*
   Copyright 2021 Kohei "Max" MATSUSHITA.

   Licensed under the Apache License, Version 2.0 (the "License");
   you may not use this file except in compliance with the License.
   You may obtain a copy of the License at

     http://www.apache.org/licenses/LICENSE-2.0

   Unless required by applicable law or agreed to in writing, software
   distributed under the License is distributed on an "AS IS" BASIS,
   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
   See the License for the specific language governing permissions and
   limitations under the License.
*/
#include <M5Stack.h>

#include <WiFi.h>
char WIFI_SSID[] = "{YOUR_WIFI_SSID}";
char WIFI_PASSWORD[] = "{YOUR_WIFI_PASS}";
#include <WiFiClientSecure.h>
WiFiClientSecure net = WiFiClientSecure();

#include <PubSubClient.h>
PubSubClient mqtt = PubSubClient(net);

#define THING_NAME      "M5StackBasic0"
#define ENDPOINT        "foobar-ats.iot.us-west-2.amazonaws.com"
#define PORT             (8883)
#define PUBLISH_TOPIC   "demo0/directly"

static const char root_ca[] = {\
"-----BEGIN CERTIFICATE-----\n\
...
-----END CERTIFICATE-----\n"};

static const char thing_certificate[] = {\
"-----BEGIN CERTIFICATE-----\n\
...
-----END CERTIFICATE-----\n"};

const char thing_private[] = {\
"-----BEGIN RSA PRIVATE KEY-----\n\
...
-----END RSA PRIVATE KEY-----\n"};

void connectToNetwork() {
    Serial.print("Connecting to Wi-Fi: ");
    WiFi.mode(WIFI_STA);
    WiFi.begin(WIFI_SSID, WIFI_PASSWORD);
    while (WiFi.status() != WL_CONNECTED) {
        delay(200);
        Serial.print(".");
    }
    Serial.println("done.");
}

void connectToAWSIoTCore() {
    Serial.print("Connecting to AWS IoT Core: ");

    // MQTTS with X.509 cert.
    net.setCACert(root_ca);
    net.setCertificate(thing_certificate);
    net.setPrivateKey(thing_private);

    mqtt.setServer(ENDPOINT, PORT);
    while (!mqtt.connect(THING_NAME)) {
        Serial.print(".");
        delay(200);
    }
    Serial.println("Connected!");
}

void setup() {
    Serial.begin(115200);
    connectToNetwork();
    connectToAWSIoTCore();
}

void publishMessage() {
    char payload[512];
    sprintf(payload, "{\"time\": %lu}", millis());
    mqtt.publish(PUBLISH_TOPIC, payload);
}

#define LOOP_INTERVAL_MS (10000)
void loop() {
    Serial.println("loop");
    publishMessage();
    unsigned long next = millis();
    while (millis() < next + LOOP_INTERVAL_MS) {
        mqtt.loop();
    }
}
```

</div></details>

## 終わりに ～ なぜこんなことをしているのか

私の所属している[ソラコム](https://soracom.jp/)では、IoT向けの通信をはじめとしたIoT開発で使えるプラットフォームを提供しています。提供している通信の中に[LTE(閉域もあり)](https://soracom.jp/services/air/)や[WireGuardベースのVPN(SORACOM Arc)](https://soracom.jp/services/arc/)があり、通信路レベルでのセキュリティ確保が容易になるため、アプリケーションに集中できるようになるわけです。

というか、今回紹介した仕組みと同じもの(中身は違うけど)を[SORACOM Beam というデータ転送サービス](https://soracom.jp/services/beam/)で提供してるので、構築や運用が面倒な方はSORACOM Beamもご検討ください！

### 参考資料

- [Module ngx_stream_proxy_module](https://nginx.org/en/docs/stream/ngx_stream_proxy_module.html)
- [たごもりすメモ / TCP serverをSSL/TLS化するのに nginx の stream_ssl_module/stream_proxy_module が便利](https://tagomoris.hatenablog.com/entry/2016/08/24/134619)


EoT