Skip to content

Fix the handling of the protocol requirements for MQTT V5 topic aliases maximum#1507

Open
alvin1221 wants to merge 1 commit intomainfrom
alvin/fix_topic_alias
Open

Fix the handling of the protocol requirements for MQTT V5 topic aliases maximum#1507
alvin1221 wants to merge 1 commit intomainfrom
alvin/fix_topic_alias

Conversation

@alvin1221
Copy link
Copy Markdown
Contributor

@alvin1221 alvin1221 commented May 1, 2026

Fix the following issue:

Description
Summary
NanoMQ accepts client-supplied MQTT v5 Topic Alias values and reuses them in later empty-topic PUBLISH packets even when the server's CONNACK did not negotiate Topic Alias Maximum.
In other words, the broker creates and consumes connection-local alias state without explicitly advertising support for that capability to the client.
Details
The core issue is that NanoMQ's inbound PUBLISH handling lacks a state check for whether Topic Alias support was actually negotiated for the connection.
In nanomq/pub_handler.c, the MQTT v5 PUBLISH path does the following:
if (proto == MQTT_PROTOCOL_VERSION_v5) {
property_data *pdata = property_get_value(
work->pub_packet->var_header.publish.properties,
TOPIC_ALIAS);
if (len > 0 && topic != NULL) {
if (pdata) {
dbhash_insert_atpair(
work->pid.id, pdata->p_value.u16, topic);
}
} else {
if (pdata) {
const char *tp = dbhash_find_atpair(
work->pid.id, pdata->p_value.u16);
if (tp) {
topic = work->pub_packet->var_header
.publish.topic_name.body =
nng_strdup(tp);
len = work->pub_packet->var_header
.publish.topic_name.len =
strlen(tp);
} else {
return TOPIC_FILTER_INVALID;
}
}
}
}
Relevant locations:
nanomq/pub_handler.c:1651
nanomq/pub_handler.c:1657
nanomq/pub_handler.c:1663
This means:
if a PUBLISH contains both a topic and TOPIC_ALIAS, NanoMQ immediately stores the alias mapping;
if a later PUBLISH contains the same alias and an empty topic, NanoMQ restores the topic from that mapping; and
there is no visible check here that Topic Alias support was previously negotiated on this connection.
On the handshake side, the broker does not proactively add TOPIC_ALIAS_MAXIMUM to CONNACK by default. In nng/src/sp/transport/mqtt/broker_tcp.c, the negotiation path only injects MAXIMUM_PACKET_SIZE when needed and does not automatically set TOPIC_ALIAS_MAXIMUM:
nng/src/sp/transport/mqtt/broker_tcp.c:426
nng/src/sp/transport/mqtt/broker_tcp.c:432
Although the CONNACK encoder is capable of encoding TOPIC_ALIAS_MAXIMUM:
nng/src/supplemental/mqtt/mqtt_codec.c:4280
nng/src/supplemental/mqtt/mqtt_codec.c:4300
in the actual broker path tested here, the observed CONNACK was:
20 09 00 00 06 2a 01 29 01 28 01
This packet does not contain Topic Alias Maximum, yet subsequent Topic Alias usage was still accepted and persisted as connection-local state.
PoC
The following Python PoC reproduces the issue over the normal network path.
Reproduction steps:
A normal subscriber subscribes to alias/test
An MQTT v5 publisher connects and receives a CONNACK that does not advertise Topic Alias Maximum
It sends PUBLISH(topic="alias/test", topic_alias=1, payload="ONE")
It then sends PUBLISH(topic="", topic_alias=1, payload="TWO")
If NanoMQ incorrectly accepts unnegotiated alias state, the subscriber will receive both messages
import socket
import struct
import time

HOST = "127.0.0.1"
PORT = 18838

def enc_varint(x):
out = []
while True:
b = x % 128
x //= 128
if x > 0:
b |= 0x80
out.append(b)
if x == 0:
break
return bytes(out)

def recv_some(sock, n=4096, timeout=0.5):
sock.settimeout(timeout)
try:
return sock.recv(n)
except Exception:
return b""

def conn_v5(client_id: bytes):
vh = b"\x00\x04MQTT" + b"\x05" + b"\x02" + b"\x00\x3c" + b"\x00"
payload = struct.pack("!H", len(client_id)) + client_id
pkt = b"\x10" + enc_varint(len(vh) + len(payload)) + vh + payload
s = socket.create_connection((HOST, PORT), timeout=1)
s.sendall(pkt)
return s, recv_some(s)

def conn_v4(client_id: bytes):
vh = b"\x00\x04MQTT" + b"\x04" + b"\x02" + b"\x00\x3c"
payload = struct.pack("!H", len(client_id)) + client_id
pkt = b"\x10" + enc_varint(len(vh) + len(payload)) + vh + payload
s = socket.create_connection((HOST, PORT), timeout=1)
s.sendall(pkt)
return s, recv_some(s)

def sub_v4(pid: int, topic: bytes):
body = struct.pack("!H", pid) + struct.pack("!H", len(topic)) + topic + b"\x00"
return b"\x82" + enc_varint(len(body)) + body

def pub_v5_alias(topic: bytes, alias: int, payload: bytes):
props = b"\x23" + struct.pack("!H", alias)
body = struct.pack("!H", len(topic)) + topic + enc_varint(len(props)) + props + payload
return b"\x30" + enc_varint(len(body)) + body

sub, sub_connack = conn_v4(b"subv4")
print("SUB_CONNACK:", sub_connack.hex())
topic = b"alias/test"
sub.sendall(sub_v4(1, topic))
print("SUBACK:", recv_some(sub).hex())

pub, connack = conn_v5(b"aliaspub")
print("PUB_CONNACK:", connack.hex())

pub.sendall(pub_v5_alias(topic, 1, b"ONE"))
time.sleep(0.2)
print("FORWARDED1:", recv_some(sub).hex())

pub.sendall(pub_v5_alias(b"", 1, b"TWO"))
time.sleep(0.2)
print("FORWARDED2:", recv_some(sub).hex())

pub.close()
sub.close()
Observed output during testing:
PUB_CONNACK: 20090000062a0129012801
FORWARDED1: 300f000a616c6961732f746573744f4e45
FORWARDED2: 300f000a616c6961732f7465737454574f
Interpretation:
PUB_CONNACK does not contain Topic Alias Maximum
FORWARDED1 shows that the first alias-bearing publish created the alias mapping
FORWARDED2 shows that the second empty-topic publish successfully resolved the alias and was actually routed
Impact
This is a connection-state semantic flaw with the following impact:
the broker accepts client Topic Alias usage without negotiating Topic Alias Maximum;
the broker creates and maintains alias mappings for that connection anyway;
later empty-topic PUBLISH packets can successfully depend on that unnegotiated state; and
this breaks the consistency between MQTT v5 capability negotiation and later stateful feature use.

Summary by CodeRabbit

  • New Features
    • Added MQTT5 topic alias maximum outbound parameter support
    • Broker now advertises topic alias maximum capability in connection negotiation responses

…5 topic aliases maximum

Co-authored-by: Copilot <copilot@github.com>
Signed-off-by: Alvin <alvin@emqx.io>
@alvin1221 alvin1221 self-assigned this May 1, 2026
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented May 1, 2026

📝 Walkthrough

Walkthrough

This PR adds MQTT5 support for topic alias maximum outbound parameter. It introduces a new topic_alias_max_out field to the connection parameter structure, provides a public getter function, initializes the field in MQTT parsing, and modifies CONNECT negotiation logic to advertise the broker's topic alias maximum in CONNACK responses.

Changes

Cohort / File(s) Summary
Public API Interface
include/nng/nng.h
Adds exported function declaration conn_param_get_topic_alias_max_out() to retrieve the topic_alias_max_out value as uint16_t.
Data Structure
src/core/message.h
Adds uint16_t topic_alias_max_out field to struct conn_param for storing broker's advertised topic alias limit.
Getter Implementation
src/nng.c
Implements the public getter function returning the topic_alias_max_out field value from connection parameters.
MQTT Initialization
src/sp/protocol/mqtt/mqtt_parser.c, src/supplemental/mqtt/mqtt_msg.c
Initializes topic_alias_max_out to 0 as default value during connection parameter setup.
CONNECT Negotiation
src/sp/transport/mqtt/broker_tcp.c
Modifies tcptran_pipe_nego_cb to advertise broker's TOPIC_ALIAS_MAXIMUM in CONNACK by removing client-sourced property, setting topic_alias_max_out to 65535, and appending the property for MQTT v5.
Property Encoding
src/supplemental/mqtt/mqtt_codec.c
Removes special-case handling that overrode TOPIC_ALIAS_MAXIMUM values; now uniformly encodes all U16 properties with their stored values.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~22 minutes

Possibly related PRs

  • #1413: Both PRs extend the conn_param API by adding new getter functions and related exported declarations to the public interface.

Suggested reviewers

  • wanghaEMQ
  • xinyi-xs
  • JaylinYu

Poem

🐰 A fluffy addition hops in today,
Topic aliases now have their say,
Max outbound limits, set with care,
Brokers advertise what they declare,
MQTT v5 flows more fair!

🚥 Pre-merge checks | ✅ 3 | ❌ 2

❌ Failed checks (1 warning, 1 inconclusive)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 50.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
Description check ❓ Inconclusive The description provides detailed context about the bug, including specific file locations, code analysis, a Python PoC, and the impact. However, it lacks the required format: no issue number reference (fixes #) and no explicit git commit message format mentioned. Add a 'fixes #' reference at the beginning and clarify that this format should be used in git commit messages as specified in the template.
✅ Passed checks (3 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately summarizes the main fix: addressing MQTT V5 topic aliases maximum handling protocol requirements, which aligns with the core bug fix of properly negotiating and checking Topic Alias support.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch alvin/fix_topic_alias

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share
Review rate limit: 0/1 reviews remaining, refill in 60 minutes.

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1


ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: bec68d9b-eec5-4802-8ce1-fcdba5722ac0

📥 Commits

Reviewing files that changed from the base of the PR and between f814f47 and b9a182e.

📒 Files selected for processing (7)
  • include/nng/nng.h
  • src/core/message.h
  • src/nng.c
  • src/sp/protocol/mqtt/mqtt_parser.c
  • src/sp/transport/mqtt/broker_tcp.c
  • src/supplemental/mqtt/mqtt_codec.c
  • src/supplemental/mqtt/mqtt_msg.c

Comment on lines +427 to 463
conn_param *cparam = p->tcp_cparam;
property *props = cparam->properties;
if (props != NULL) {
// CONNECT's Topic Alias Maximum is client
// capability and must not be reused as
// broker's CONNACK capability.
property_remove(props, TOPIC_ALIAS_MAXIMUM);
}
if (cparam->max_packet_size == 0) {
// set default max packet size for client
p->tcp_cparam->max_packet_size =
p->conf == NULL
cparam->max_packet_size = p->conf == NULL
? NANO_MAX_PACKET_SIZE
: p->conf->client_max_packet_size;
if (p->tcp_cparam->properties != NULL) {
property_remove(p->tcp_cparam->properties,
MAXIMUM_PACKET_SIZE);
property_append(p->tcp_cparam->properties,
property_set_value_u32(MAXIMUM_PACKET_SIZE,
p->tcp_cparam->max_packet_size));
if (props != NULL) {
property_remove(
props, MAXIMUM_PACKET_SIZE);
property_append(props,
property_set_value_u32(
MAXIMUM_PACKET_SIZE,
cparam->max_packet_size));
}
}
// Advertise broker's Topic Alias Maximum in CONNACK
// for MQTT v5. Default to 65535 to allow clients to
// use Topic Aliases.
if (p->pro_ver == MQTT_PROTOCOL_VERSION_v5) {
if (props == NULL) {
props = property_alloc();
cparam->properties = props;
}
if (props != NULL) {
cparam->topic_alias_max_out = 65535;
property_append(props,
property_set_value_u16(
TOPIC_ALIAS_MAXIMUM,
cparam->topic_alias_max_out));
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | 🏗️ Heavy lift

Build CONNACK properties from a dedicated list instead of mutating CONNECT properties

At Line 428, props is sourced from cparam->properties (CONNECT-side data) and then edited for CONNACK. Removing only TOPIC_ALIAS_MAXIMUM is partial; other CONNECT-only properties can still leak into CONNACK, and original CONNECT metadata is mutated in place.

Suggested direction
- conn_param *cparam = p->tcp_cparam;
- property   *props  = cparam->properties;
+ conn_param *cparam = p->tcp_cparam;
+ property   *conn_props = cparam->properties;   // keep CONNECT properties
+ property   *props      = property_alloc();      // build CONNACK properties only

+ if (props == NULL) {
+     rv   = NNG_ENOMEM;
+     code = SERVER_UNAVAILABLE;
+     goto error;
+ }

+ // Use `props` only for CONNACK-legal properties (e.g. MAXIMUM_PACKET_SIZE,
+ // TOPIC_ALIAS_MAXIMUM), and keep CONNECT properties separate.
+ // (If your encoder currently reads cparam->properties for CONNACK,
+ // introduce/route a dedicated CONNACK property container.)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant