IoTでよく使われるプロトコル「MQTT」ですが、クラウド側のブローカー(MQTTサーバー)がMQTTS(MQTT+TLS)を要求するケースが多くなり、MCU側への実装負担が増えています。
このエントリーでは、Proxy上でNGINXを動かし、Streamモジュールを使用して MQTT <=> MQTTS 変換の方法を解説します。
MQTTSブローカーとして、今回はAWS IoT Coreをターゲットにします。
注意
MCUとProxy間のセキュリティは別途確保してください。
例としてはIPSecやWireGuardといったVPNや、3G/LTE/5G等セルラー通信による通信路レイヤーによるセキュリティ確保の方法があります。
ESP32(M5Stack Basic)上でWireGuardを使用したMQTT実装サンプル も併せてご覧ください。
確認環境
- Proxy
- Raspberry Pi 4 model B
- Raspberry Pi OS (Release date: May 7th 2021)
- Raspberry Pi 4 model B
- MCU
- M5Stack Basic
- ボード定義: ESP32(1.0.6)
- ライブラリ: M5Stack(0.3.6)
- M5Stack Basic
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のインストールから動作確認まで
sudo apt update
sudo apt install -y nginx libnginx-mod-stream
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;
}
}
(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していることも確認できます。
$ 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.
$ 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)拡張を要求します。
NGINXのStreamモジュール(ngx_stream_proxy_module)は、その辺いい感じにやってくれるようです。
※ちゃんと調べてなくてゴメンナサイ
MCU / M5Stack Basic側
arduino-mqttでMQTT接続します。
/*
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
参考コード) MQTT+TLS 版
/*
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();
}
}
終わりに ~ なぜこんなことをしているのか
私の所属しているソラコムでは、IoT向けの通信をはじめとしたIoT開発で使えるプラットフォームを提供しています。提供している通信の中にLTE(閉域もあり)やWireGuardベースのVPN(SORACOM Arc)があり、通信路レベルでのセキュリティ確保が容易になるため、アプリケーションに集中できるようになるわけです。
というか、今回紹介した仕組みと同じもの(中身は違うけど)をSORACOM Beam というデータ転送サービスで提供してるので、構築や運用が面倒な方はSORACOM Beamもご検討ください!
参考資料
- Module ngx_stream_proxy_module
- たごもりすメモ / TCP serverをSSL/TLS化するのに nginx の stream_ssl_module/stream_proxy_module が便利
EoT