Skip to content

Commit 81a495e

Browse files
authored
Merge pull request #537 from dgarske/broker_fixes
Fix MQTTv5 QoS 2 CONNACK interop and add WOLFMQTT_MAX_QOS build cap
2 parents e1f1376 + ed375ea commit 81a495e

9 files changed

Lines changed: 177 additions & 19 deletions

File tree

.github/workflows/broker-check.yml

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,22 @@ jobs:
4747
wolfmqtt_opts: "--enable-broker --enable-tls --enable-websocket"
4848
extra_deps: "libwebsockets-dev"
4949
wolfssl_opts: "--enable-opensslcoexist --enable-enckeys"
50+
# Maximum-QoS matrix. WOLFMQTT_MAX_QOS=2 is the default and runs
51+
# the full broker.test. =1 and =0 caps compile out the QoS 2
52+
# state machine; broker.test exercises QoS 2 pub/sub (tests 3
53+
# and 11) which is intentionally rejected on capped builds, so
54+
# those entries are build-only.
55+
- name: "Broker MAX_QOS=2 (default, full QoS)"
56+
cflags: ""
57+
wolfmqtt_opts: "--enable-v5 --enable-broker --enable-max-qos=2"
58+
- name: "Broker MAX_QOS=1 (build only)"
59+
cflags: ""
60+
wolfmqtt_opts: "--enable-v5 --enable-broker --enable-max-qos=1"
61+
skip_broker_test: "yes"
62+
- name: "Broker MAX_QOS=0 (build only)"
63+
cflags: ""
64+
wolfmqtt_opts: "--enable-v5 --enable-broker --enable-max-qos=0"
65+
skip_broker_test: "yes"
5066

5167
steps:
5268
- name: Install dependencies
@@ -82,6 +98,7 @@ jobs:
8298
run: make
8399

84100
- name: "Run broker tests (${{ matrix.name }})"
101+
if: matrix.skip_broker_test != 'yes'
85102
run: ./scripts/broker.test
86103

87104
- name: Show logs on failure

.github/workflows/cmake-build.yml

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,22 @@ jobs:
1111

1212
runs-on: ubuntu-22.04
1313

14+
strategy:
15+
fail-fast: false
16+
matrix:
17+
# Smoke-build the CMake project under each WOLFMQTT_MAX_QOS value
18+
# to keep the build-option plumbing exercised. "" means leave the
19+
# cache value at its 2 default.
20+
include:
21+
- name: "CMake default (MAX_QOS unset)"
22+
cmake_opts: ""
23+
- name: "CMake MAX_QOS=2"
24+
cmake_opts: "-DWOLFMQTT_V5=yes -DWOLFMQTT_BROKER=yes -DWOLFMQTT_MAX_QOS=2"
25+
- name: "CMake MAX_QOS=1"
26+
cmake_opts: "-DWOLFMQTT_V5=yes -DWOLFMQTT_BROKER=yes -DWOLFMQTT_MAX_QOS=1"
27+
- name: "CMake MAX_QOS=0"
28+
cmake_opts: "-DWOLFMQTT_V5=yes -DWOLFMQTT_BROKER=yes -DWOLFMQTT_MAX_QOS=0"
29+
1430
steps:
1531
# Install cmake
1632
- name: Install cmake
@@ -36,9 +52,9 @@ jobs:
3652
- uses: actions/checkout@master
3753

3854
#build wolfMQTT
39-
- name: Build wolfMQTT
55+
- name: "Build wolfMQTT (${{ matrix.name }})"
4056
run: |
4157
mkdir build
4258
cd build
43-
cmake ..
59+
cmake ${{ matrix.cmake_opts }} ..
4460
cmake --build .

CMakeLists.txt

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -136,6 +136,19 @@ endif()
136136
add_option(WOLFMQTT_DISCB
137137
"Enable disconnect callback"
138138
"yes" "yes;no")
139+
140+
# Maximum QoS supported (compile-time cap for both client and broker).
141+
# Default 2 = full QoS support. 1 or 0 compiles out the broker's QoS 2
142+
# state machine and the client's initial max_qos clamp, and causes the
143+
# broker to advertise v5 MQTT_PROP_MAX_QOS in CONNACK.
144+
set(WOLFMQTT_MAX_QOS "2" CACHE STRING
145+
"Maximum QoS supported by client and broker (0, 1, or 2)")
146+
set_property(CACHE WOLFMQTT_MAX_QOS PROPERTY STRINGS "0;1;2")
147+
if (NOT WOLFMQTT_MAX_QOS MATCHES "^[012]$")
148+
message(SEND_ERROR
149+
"WOLFMQTT_MAX_QOS must be 0, 1, or 2 (got: ${WOLFMQTT_MAX_QOS})")
150+
endif()
151+
list(APPEND WOLFMQTT_DEFINITIONS "-DWOLFMQTT_MAX_QOS=${WOLFMQTT_MAX_QOS}")
139152
if (WOLFMQTT_DISCB)
140153
list(APPEND WOLFMQTT_DEFINITIONS "-DWOLFMQTT_DISCONNECT_CB")
141154
endif()

configure.ac

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -323,6 +323,26 @@ then
323323
AM_CFLAGS="$AM_CFLAGS -DWOLFMQTT_DISCONNECT_CB"
324324
fi
325325

326+
# Maximum QoS supported (compile-time cap for both client and broker).
327+
# Values: 0, 1, 2. Default 2 (full QoS support). Capping at 1 (or 0)
328+
# compiles out the broker's QoS 2 state machine and the client's initial
329+
# max_qos clamp, saves ~2.5 KB of broker .text, and makes the broker
330+
# advertise the v5 MQTT_PROP_MAX_QOS property in CONNACK.
331+
AC_ARG_ENABLE([max-qos],
332+
[AS_HELP_STRING([--enable-max-qos@<:@=0|1|2@:>@],
333+
[Maximum QoS supported by client and broker (default: 2)])],
334+
[ ENABLED_MAX_QOS=$enableval ],
335+
[ ENABLED_MAX_QOS=2 ]
336+
)
337+
case "x$ENABLED_MAX_QOS" in
338+
"x0"|"x1"|"x2")
339+
AM_CFLAGS="$AM_CFLAGS -DWOLFMQTT_MAX_QOS=$ENABLED_MAX_QOS"
340+
;;
341+
*)
342+
AC_MSG_ERROR([--enable-max-qos must be 0, 1, or 2 (got: $ENABLED_MAX_QOS)])
343+
;;
344+
esac
345+
326346
# Multithread support
327347
AC_ARG_ENABLE([mt],
328348
[AS_HELP_STRING([--enable-mt],[Enable multiple thread support (default: disabled)])],
@@ -611,6 +631,7 @@ echo " * Linker Flags: $LDFLAGS"
611631
echo " * LIB Flags: $LIB"
612632

613633
echo " * Disconnect Callback: $ENABLED_DISCB"
634+
echo " * Maximum QoS: $ENABLED_MAX_QOS"
614635
echo " * Error Strings: $ENABLED_ERROR_STRINGS"
615636
echo " * Enable MQTT-SN: $ENABLED_SN"
616637
echo " * Enable MQTT v5.0: $ENABLED_MQTTV50"

scripts/broker.test

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -603,10 +603,13 @@ if [ "$has_v5" = "yes" ]; then
603603
>"${TMP_DIR}/t12.log" 2>&1
604604
T12_RC=$?
605605

606-
# 12b: Verify CONNACK server properties were received
606+
# 12b: Verify CONNACK server properties were received.
607+
# Type 37 = Retain Available, Type 40 = Wildcard Subscription Available.
608+
# Maximum QoS (Type 36) is intentionally omitted per [MQTT-3.2.2.3.4]
609+
# (absence signals QoS 2 support; emitting Max QoS=2 is a Protocol Error).
607610
T12_PROPS=yes
608611
grep -q "Property CB: Type 37" "${TMP_DIR}/t12.log" 2>/dev/null || T12_PROPS=no
609-
grep -q "Property CB: Type 36" "${TMP_DIR}/t12.log" 2>/dev/null || T12_PROPS=no
612+
grep -q "Property CB: Type 40" "${TMP_DIR}/t12.log" 2>/dev/null || T12_PROPS=no
610613

611614
# 12c: v5 pub/sub with separate clients (property forwarding)
612615
start_broker

src/mqtt_broker.c

Lines changed: 63 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -24,8 +24,8 @@
2424
#include <config.h>
2525
#endif
2626

27-
#include "wolfmqtt/mqtt_types.h"
2827
#include "wolfmqtt/mqtt_broker.h"
28+
#include "wolfmqtt/mqtt_types.h"
2929
#include "wolfmqtt/mqtt_client.h"
3030
#include "wolfmqtt/mqtt_packet.h"
3131
#include "wolfmqtt/mqtt_socket.h"
@@ -1278,8 +1278,13 @@ static int BrokerNetDisconnect(void* context)
12781278
/* must NOT be re-delivered to subscribers. The state is per-client and is */
12791279
/* cleared on disconnect; surviving across reconnect would require the */
12801280
/* broader session-state work (see #485/#489/#494). */
1281+
/* */
1282+
/* The entire QoS 2 inbound state and PUBREL/PUBREC/PUBCOMP handling is */
1283+
/* compiled out when WOLFMQTT_MAX_QOS < 2. Subscribe-grant capping and */
1284+
/* inbound-publish QoS rejection cover the corresponding wire paths. */
12811285
/* -------------------------------------------------------------------------- */
12821286

1287+
#if WOLFMQTT_MAX_QOS >= 2
12831288
/* Returns 1 if packet_id is currently awaiting PUBREL, 0 otherwise. */
12841289
static int BrokerInboundQos2_Contains(BrokerClient* bc, word16 packet_id)
12851290
{
@@ -1425,13 +1430,16 @@ static void BrokerInboundQos2_Clear(BrokerClient* bc)
14251430
bc->qos2_pending_count = 0;
14261431
#endif
14271432
}
1433+
#endif /* WOLFMQTT_MAX_QOS >= 2 */
14281434

14291435
static void BrokerClient_Free(BrokerClient* bc)
14301436
{
14311437
if (bc == NULL) {
14321438
return;
14331439
}
1440+
#if WOLFMQTT_MAX_QOS >= 2
14341441
BrokerInboundQos2_Clear(bc);
1442+
#endif
14351443

14361444
#ifdef ENABLE_MQTT_WEBSOCKET
14371445
if (bc->ws_ctx != NULL) {
@@ -3264,7 +3272,12 @@ static int BrokerHandle_Connect(BrokerClient* bc, int rx_len,
32643272
#endif
32653273
bc->will_payload_len = wp_len;
32663274
}
3267-
bc->will_qos = mc.lwt_msg->qos;
3275+
/* Clamp will QoS to this build's Maximum QoS. A v5 client that
3276+
* sent Will QoS > advertised Max QoS would already be in
3277+
* Protocol Error territory, but for v3.1.1 (no advertisement)
3278+
* we silently downgrade rather than rejecting CONNECT. */
3279+
bc->will_qos = (mc.lwt_msg->qos > WOLFMQTT_MAX_QOS) ?
3280+
(MqttQoS)WOLFMQTT_MAX_QOS : mc.lwt_msg->qos;
32683281
bc->will_retain = mc.lwt_msg->retain;
32693282
bc->will_delay_sec = 0;
32703283
#ifdef WOLFMQTT_V5
@@ -3445,11 +3458,18 @@ static int BrokerHandle_Connect(BrokerClient* bc, int rx_len,
34453458
prop->data_byte = 0;
34463459
#endif
34473460
}
3461+
/* [MQTT-3.2.2.3.4] Maximum QoS property MUST be 0 or 1. Absence
3462+
* of the property signals server supports Maximum QoS 2. Emitting
3463+
* Maximum QoS = 2 is a Protocol Error and strict v5 clients will
3464+
* disconnect on receipt. Emit the property only when this build
3465+
* caps below QoS 2 via WOLFMQTT_MAX_QOS. */
3466+
#if WOLFMQTT_MAX_QOS < 2
34483467
prop = MqttProps_Add(&ack.props);
34493468
if (prop != NULL) {
34503469
prop->type = MQTT_PROP_MAX_QOS;
3451-
prop->data_byte = MQTT_QOS_2;
3470+
prop->data_byte = (byte)WOLFMQTT_MAX_QOS;
34523471
}
3472+
#endif
34533473
}
34543474
#endif
34553475

@@ -3517,9 +3537,10 @@ static int BrokerHandle_Subscribe(BrokerClient* bc, int rx_len,
35173537
MqttQoS topic_qos = sub.topics[i].qos;
35183538
MqttQoS granted_qos;
35193539

3520-
/* Cap at QoS 2 */
3521-
if (topic_qos > MQTT_QOS_2) {
3522-
topic_qos = MQTT_QOS_2;
3540+
/* [MQTT-3.8.4-7] / [MQTT-3.9.3]: subscribe grant capped at the
3541+
* build's Maximum QoS. Default is QoS 2. */
3542+
if (topic_qos > WOLFMQTT_MAX_QOS) {
3543+
topic_qos = (MqttQoS)WOLFMQTT_MAX_QOS;
35233544
}
35243545
granted_qos = topic_qos;
35253546

@@ -3662,7 +3683,9 @@ static int BrokerHandle_Publish(BrokerClient* bc, int rx_len,
36623683
MqttPublishResp resp;
36633684
byte* payload = NULL;
36643685
char* topic = NULL;
3686+
#if WOLFMQTT_MAX_QOS >= 2
36653687
int qos2_duplicate = 0;
3688+
#endif
36663689
#ifdef WOLFMQTT_STATIC_MEMORY
36673690
char topic_buf[BROKER_MAX_TOPIC_LEN];
36683691
#endif
@@ -3684,6 +3707,26 @@ static int BrokerHandle_Publish(BrokerClient* bc, int rx_len,
36843707
* MALFORMED_DATA before reaching this handler. The broker no longer
36853708
* needs a per-handler scan. */
36863709

3710+
#if WOLFMQTT_MAX_QOS < 2
3711+
/* [MQTT-3.2.2.3.4] / [MQTT-3.3.4]: this build advertised Maximum QoS
3712+
* below 2. A client publishing at QoS > our cap is a Protocol Error;
3713+
* v5 spec wants reason 0x9B QoS Not Supported. v3 has no reason code
3714+
* field, so we just abnormally close. */
3715+
if (pub.qos > WOLFMQTT_MAX_QOS) {
3716+
WBLOG_ERR(broker,
3717+
"broker: PUBLISH QoS %d exceeds WOLFMQTT_MAX_QOS=%d sock=%d",
3718+
pub.qos, WOLFMQTT_MAX_QOS, (int)bc->sock);
3719+
#ifdef WOLFMQTT_V5
3720+
if (bc->protocol_level >= MQTT_CONNECT_PROTOCOL_LEVEL_5) {
3721+
(void)BrokerSend_Disconnect(bc, MQTT_REASON_QOS_NOT_SUPPORTED);
3722+
}
3723+
#endif
3724+
rc = MQTT_CODE_ERROR_MALFORMED_DATA;
3725+
goto publish_cleanup;
3726+
}
3727+
#endif /* WOLFMQTT_MAX_QOS < 2 */
3728+
3729+
#if WOLFMQTT_MAX_QOS >= 2
36873730
/* [MQTT-4.3.3] QoS 2 duplicate detection. If we already PUBREC'd this
36883731
* packet_id and are still waiting for PUBREL, treat the inbound PUBLISH
36893732
* as a retransmission: send another PUBREC but DO NOT re-deliver the
@@ -3722,6 +3765,7 @@ static int BrokerHandle_Publish(BrokerClient* bc, int rx_len,
37223765
}
37233766
}
37243767
}
3768+
#endif /* WOLFMQTT_MAX_QOS >= 2 */
37253769

37263770
/* Create null-terminated topic copy for matching/logging */
37273771
if (pub.topic_name && pub.topic_name_len > 0) {
@@ -3749,7 +3793,11 @@ static int BrokerHandle_Publish(BrokerClient* bc, int rx_len,
37493793
#ifdef WOLFMQTT_BROKER_RETAINED
37503794
/* Handle retained messages - skipped for QoS 2 duplicates: the original
37513795
* PUBLISH already updated the retained store. */
3752-
if (!qos2_duplicate && topic != NULL && pub.retain) {
3796+
if (
3797+
#if WOLFMQTT_MAX_QOS >= 2
3798+
!qos2_duplicate &&
3799+
#endif
3800+
topic != NULL && pub.retain) {
37533801
if (pub.total_len == 0) {
37543802
BrokerRetained_Delete(broker, topic);
37553803
}
@@ -3778,7 +3826,10 @@ static int BrokerHandle_Publish(BrokerClient* bc, int rx_len,
37783826

37793827
/* Fan-out is skipped for QoS 2 duplicates: subscribers already received
37803828
* the application message from the original PUBLISH ([MQTT-4.3.3]). */
3781-
if (!qos2_duplicate &&
3829+
if (
3830+
#if WOLFMQTT_MAX_QOS >= 2
3831+
!qos2_duplicate &&
3832+
#endif
37823833
topic != NULL && (payload != NULL || pub.total_len == 0)) {
37833834
#ifdef WOLFMQTT_STATIC_MEMORY
37843835
int i;
@@ -3883,6 +3934,7 @@ static int BrokerHandle_Publish(BrokerClient* bc, int rx_len,
38833934
return rc;
38843935
}
38853936

3937+
#if WOLFMQTT_MAX_QOS >= 2
38863938
static int BrokerHandle_PublishRel(BrokerClient* bc, int rx_len)
38873939
{
38883940
int rc;
@@ -3957,6 +4009,7 @@ static int BrokerHandle_PublishRec(BrokerClient* bc, int rx_len)
39574009
}
39584010
return rc;
39594011
}
4012+
#endif /* WOLFMQTT_MAX_QOS >= 2 */
39604013

39614014
/* [MQTT-2.2.2-2] / [MQTT-3.8.1-1] etc.: a malformed packet MUST cause the
39624015
* server to close the network connection. Mirrors the read-failure close
@@ -4129,6 +4182,7 @@ static int BrokerClient_Process(MqttBroker* broker, BrokerClient* bc)
41294182
case MQTT_PACKET_TYPE_PUBLISH_ACK:
41304183
/* QoS 1 ack from subscriber - delivery complete */
41314184
break;
4185+
#if WOLFMQTT_MAX_QOS >= 2
41324186
case MQTT_PACKET_TYPE_PUBLISH_REC:
41334187
{
41344188
/* QoS 2 step 2: subscriber sends PUBREC, broker
@@ -4155,6 +4209,7 @@ static int BrokerClient_Process(MqttBroker* broker, BrokerClient* bc)
41554209
/* QoS 2 step 4: subscriber sends PUBCOMP - delivery
41564210
* complete */
41574211
break;
4212+
#endif /* WOLFMQTT_MAX_QOS >= 2 */
41584213
case MQTT_PACKET_TYPE_SUBSCRIBE:
41594214
{
41604215
int s_rc = BrokerHandle_Subscribe(bc, rc, broker);

src/mqtt_client.c

Lines changed: 14 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -575,10 +575,15 @@ static void Handle_ConnectAck_Props(MqttClient* client, MqttProp* props)
575575
for (prop = props; prop != NULL; prop = prop->next) {
576576
if (prop->type == MQTT_PROP_MAX_QOS) {
577577
/* MQTT v5 [3.1.2.11.6]: only 0 or 1 are legal. Clamp a
578-
* non-conforming broker value so client-side publish guards
578+
* non-conforming broker value, then narrow against this
579+
* build's WOLFMQTT_MAX_QOS so client-side publish guards
579580
* remain meaningful. */
580-
client->max_qos = (prop->data_byte <= MQTT_QOS_1) ?
581+
byte adv = (prop->data_byte <= MQTT_QOS_1) ?
581582
prop->data_byte : MQTT_QOS_1;
583+
if (adv > WOLFMQTT_MAX_QOS) {
584+
adv = (byte)WOLFMQTT_MAX_QOS;
585+
}
586+
client->max_qos = adv;
582587
}
583588
else if (prop->type == MQTT_PROP_RETAIN_AVAIL) {
584589
/* MQTT v5 [3.1.2.11.5]: only 0 or 1 are legal. */
@@ -1647,7 +1652,9 @@ int MqttClient_Init(MqttClient *client, MqttNet* net,
16471652
client->rx_buf_len = rx_buf_len;
16481653
client->cmd_timeout_ms = cmd_timeout_ms;
16491654
#ifdef WOLFMQTT_V5
1650-
client->max_qos = MQTT_QOS_2;
1655+
/* Initialize to this build's Maximum QoS. Handle_Props will narrow
1656+
* this if the server advertises a lower MQTT_PROP_MAX_QOS. */
1657+
client->max_qos = (MqttQoS)WOLFMQTT_MAX_QOS;
16511658
client->retain_avail = 1;
16521659
client->protocol_level = MQTT_CONNECT_PROTOCOL_LEVEL;
16531660
rc = MqttProps_Init();
@@ -1757,8 +1764,10 @@ int MqttClient_Connect(MqttClient *client, MqttConnect *mc_connect)
17571764

17581765
/* Reset server-supplied session limits so stale values from a
17591766
* prior broker do not leak across reconnects. An accepted CONNACK
1760-
* will repopulate these in Handle_ConnectAck_Props. */
1761-
client->max_qos = MQTT_QOS_2;
1767+
* will repopulate these in Handle_ConnectAck_Props. Initialize to
1768+
* this build's Maximum QoS so the runtime guard in MqttPublishMsg
1769+
* caps publishes even before CONNACK is processed. */
1770+
client->max_qos = (MqttQoS)WOLFMQTT_MAX_QOS;
17621771
client->retain_avail = 1;
17631772
client->packet_sz_max = 0;
17641773
#endif

0 commit comments

Comments
 (0)