Documentation
¶
Overview ¶
Package natsmqtt5 is an MQTT version 5.0 broker that uses an existing NATS server as its messaging core.
The broker is a NATS client, not a fork of nats-server. Point it at a NATS URL you already run and it speaks MQTT v5 on a TCP port, translating topics to NATS subjects, fan-out to NATS subscriptions, shared subscriptions to NATS queue groups and retained messages to a JetStream stream. Two brokers against the same NATS server see each other's traffic, so an MQTT client on one reaches a subscriber on the other.
The subject mapping is byte-for-byte the one nats-server uses for its own MQTT 3.1.1 support, so topics land where a NATS-native subscriber expects.
Minimal use:
b, err := natsmqtt5.New(natsmqtt5.Options{NATSURL: "nats://localhost:4222"})
if err != nil {
return err
}
defer b.Close()
return b.Serve(ctx)
Section numbers in the documentation refer to the OASIS MQTT v5.0 Standard of 07 March 2019.
Example ¶
Run a broker against an existing NATS server.
package main
import (
"context"
"log"
"github.com/taumatix/natsmqtt5"
)
func main() {
broker, err := natsmqtt5.New(natsmqtt5.Options{
NATSURL: "nats://localhost:4222",
Listen: ":1883",
})
if err != nil {
log.Fatal(err)
}
defer broker.Close()
if err := broker.Serve(context.Background()); err != nil {
log.Fatal(err)
}
}
Output:
Example (Authentication) ¶
Reject unknown credentials with the CONNACK Reason Code of your choosing, and confine each user to its own topic subtree.
package main
import (
"context"
"errors"
"log"
"strings"
"github.com/nats-io/nuid"
"github.com/taumatix/natsmqtt5"
"github.com/taumatix/natsmqtt5/packet"
)
func main() {
passwords := map[string]string{"alice": "s3cret"}
opts := natsmqtt5.Options{
NATSURL: "nats://localhost:4222",
Authenticator: natsmqtt5.AuthenticatorFunc(
func(_ context.Context, req *natsmqtt5.AuthRequest) (*natsmqtt5.AuthResult, error) {
if want, ok := passwords[req.Username]; !ok || want != string(req.Password) {
return nil, &natsmqtt5.ConnectError{Code: packet.BadUserNameOrPassword}
}
// A Session is keyed by its Client Identifier alone, so an
// authenticated user must not be free to choose any: without
// this, alice connecting as "bob-sensor" inherits bob's
// session and everything it is subscribed to.
//
// A zero-length identifier is the client asking the server to
// assign one [MQTT-3.1.3-6], so it is answered rather than
// refused.
if req.ClientID == "" {
return &natsmqtt5.AuthResult{
Identity: req.Username,
AssignClientID: req.Username + "/" + nuid.Next(),
}, nil
}
// Equality on the first segment, not a prefix test: a user
// named "a" passes HasPrefix("a/b/sensor", "a/") and would
// inherit the session of the user named "a/b".
if owner, _, found := strings.Cut(req.ClientID, "/"); !found || owner != req.Username {
return nil, &natsmqtt5.ConnectError{Code: packet.ClientIdentifierNotValid}
}
return &natsmqtt5.AuthResult{Identity: req.Username}, nil
}),
Authorizer: natsmqtt5.AuthorizerFunc(
func(_ context.Context, req *natsmqtt5.AuthzRequest) error {
// A denied publish comes back as 0x87 in the PUBACK; a denied
// subscription as 0x87 in the SUBACK. The connection stays up.
if !strings.HasPrefix(req.Topic, "users/"+req.Identity+"/") {
return errors.New("outside the user's subtree")
}
return nil
}),
}
broker, err := natsmqtt5.New(opts)
if err != nil {
log.Fatal(err)
}
defer broker.Close()
log.Fatal(broker.Serve(context.Background()))
}
Output:
Example (Embedded) ¶
Share one NATS connection between the broker and the rest of an application, and keep MQTT traffic in its own subject namespace.
package main
import (
"context"
"log"
"github.com/nats-io/nats.go"
"github.com/taumatix/natsmqtt5"
)
func main() {
// An existing connection the application already uses. The broker does not
// close a connection it did not open.
nc, err := nats.Connect("nats://localhost:4222")
if err != nil {
log.Fatal(err)
}
defer nc.Close()
broker, err := natsmqtt5.New(natsmqtt5.Options{
Conn: nc,
Listen: "127.0.0.1:1883",
SubjectPrefix: "iot",
StreamPrefix: "IOT",
})
if err != nil {
log.Fatal(err)
}
defer broker.Close()
// An MQTT client publishing to "sensors/7/temp" now reaches any NATS
// subscriber on "iot.sensors.7.temp", and the other way round.
log.Fatal(broker.Serve(context.Background()))
}
Output:
Index ¶
Examples ¶
Constants ¶
const ( DefaultListen = ":1883" DefaultSubjectPrefix = "mqtt" DefaultStreamPrefix = "MQTT5" // DefaultReceiveMaximum bounds how many QoS 1 and QoS 2 publications a // client may have in flight towards the broker (MQTT-5.0 §3.2.2.3.3). DefaultReceiveMaximum = 1024 // DefaultTopicAliasMaximum is the highest Topic Alias the broker accepts // from a client (MQTT-5.0 §3.1.2.11.5). DefaultTopicAliasMaximum = 64 // DefaultMaximumPacketSize caps an inbound packet at 64 MiB // (MQTT-5.0 §3.2.2.3.6). DefaultMaximumPacketSize = 64 << 20 // DefaultConnectTimeout bounds how long a new connection may stay silent // before sending its CONNECT (MQTT-5.0 §3.1.4, step 1). DefaultConnectTimeout = 10 * time.Second // DefaultMaxSessionExpiry caps the Session Expiry Interval a client may // request. The broker returns the capped value in CONNACK, as // MQTT-5.0 §3.2.2.3.2 provides for. DefaultMaxSessionExpiry = time.Hour )
Defaults applied to the zero value of the corresponding Options field.
const MaxPersistentClientIDLen = 128
MaxPersistentClientIDLen caps the Client Identifier a broker with Options.PersistentSessions accepts. The identifier is encoded into a JetStream key-value key, and a key is part of a NATS subject, so it cannot be the 65535 bytes MQTT permits. A longer one is refused with 0x85 (Client Identifier not valid), which MQTT-5.0 §3.1.3.1 provides for: a server states the identifiers it accepts, and 128 bytes is more than five times the 23 that every MQTT server must accept.
ReauthorizeForwardWait is how long Broker.Reauthorize waits for another broker to say it holds a session this one does not. Every broker answers at once, so the wait is spent only when no broker holds it.
Variables ¶
var ErrInvalidOptions = errors.New("natsmqtt5: invalid options")
ErrInvalidOptions reports an Options value the broker cannot run with.
Functions ¶
Types ¶
type AuthRequest ¶
type AuthRequest struct {
// ClientID is the Client Identifier from CONNECT, before the broker
// assigns one for an empty value.
ClientID string
// Username is empty when the User Name Flag was not set.
Username string
// Password is nil when the Password Flag was not set. MQTT v5 permits a
// password with no username (MQTT-5.0 §3.1.2.9).
Password []byte
// RemoteAddr is the client's network address.
RemoteAddr net.Addr
// TLS is the connection's TLS state, or nil for a plaintext connection.
// Use it for client-certificate authentication.
TLS *tls.ConnectionState
// Properties are the CONNECT properties, including any User Properties.
Properties *packet.Properties
}
AuthRequest describes a connection attempt for an Authenticator.
type AuthResult ¶
type AuthResult struct {
// Identity, when non-empty, names the authenticated principal. It is
// passed to the Authorizer and included in log records; it does not change
// the Client Identifier.
Identity string
// AssignClientID, when non-empty, overrides the Client Identifier for the
// session and is returned to the client as the Assigned Client Identifier
// [MQTT-3.1.3-7].
AssignClientID string
}
AuthResult is an Authenticator's verdict.
type Authenticator ¶
type Authenticator interface {
Authenticate(ctx context.Context, req *AuthRequest) (*AuthResult, error)
}
Authenticator decides whether a CONNECT is accepted.
Returning a nil error accepts the connection. Returning an error rejects it; wrap or return a *ConnectError to choose the CONNACK Reason Code, otherwise the broker answers 0x87 (Not authorized) and does not disclose the reason.
The Client Identifier is the session key ¶
A Session is identified by its Client Identifier alone (MQTT-5.0 §4.1), so every connection an implementation accepts under a given identifier inherits that session: its subscriptions, its unacknowledged messages and its QoS 2 receive state. An implementation that authenticates a principal but lets it choose any ClientID therefore lets it read another principal's session.
So an implementation must either refuse a CONNECT whose AuthRequest.ClientID is not one the authenticated principal owns, or ignore it and return an AuthResult.AssignClientID derived from the principal. Nothing downstream can make up for this: the Authorizer decides topics, not sessions.
The broker re-runs the Authorizer over a resumed session's Topic Filters, so a permission narrowed between two connections takes effect on the second one rather than when the session expires. That bounds the damage; it does not replace binding the identifier, because the filters a principal is allowed are usually not what distinguishes it from the session's rightful owner.
type AuthenticatorFunc ¶
type AuthenticatorFunc func(ctx context.Context, req *AuthRequest) (*AuthResult, error)
AuthenticatorFunc adapts a function to the Authenticator interface.
func (AuthenticatorFunc) Authenticate ¶
func (f AuthenticatorFunc) Authenticate(ctx context.Context, req *AuthRequest) (*AuthResult, error)
Authenticate calls f.
type Authorizer ¶
type Authorizer interface {
Authorize(ctx context.Context, req *AuthzRequest) error
}
Authorizer decides whether a session may publish to or subscribe to a topic. A nil error permits the operation.
A denied publish is answered with 0x87 (Not authorized) in the PUBACK or PUBREC, or dropped silently at QoS 0, as MQTT-5.0 §3.3.4 requires. A denied SUBSCRIBE is answered with 0x87 in the SUBACK for that filter. A denied Will Message, asked about with AuthzRequest.Will set, refuses the CONNECT with 0x87 in the CONNACK.
It is also called while a session is being resumed ¶
A session is keyed by its Client Identifier alone, so the broker puts every filter of a resumed session back past the Authorizer before answering the CONNECT — once per live filter, with AuthzRequest.Resume set. That is what makes a permission narrowed between two connections take effect on the second one rather than when the session expires.
Two consequences an implementation has to plan for:
A slow Authorizer delays the CONNACK in proportion to how many filters the session holds. The ctx carries no deadline of the broker's making.
A denial there is silent and it is final. There is no per-filter Reason Code in a CONNACK, so the filter is torn down, logged at warn level, and the client is told SessionPresent: true with no indication that anything is missing. With Options.PersistentSessions the reduced set is written straight back to the durable record. An error returned because the policy store could not be reached is therefore indistinguishable from a decision to deny, and costs the client its subscription permanently — an implementation that cannot decide should say so by failing the connection from the Authenticator, not by returning an error from here.
type AuthorizerFunc ¶
type AuthorizerFunc func(ctx context.Context, req *AuthzRequest) error
AuthorizerFunc adapts a function to the Authorizer interface.
func (AuthorizerFunc) Authorize ¶
func (f AuthorizerFunc) Authorize(ctx context.Context, req *AuthzRequest) error
Authorize calls f.
type AuthzRequest ¶
type AuthzRequest struct {
Action Action
// Resume distinguishes a filter being re-checked because a session is
// being resumed from one the client has just asked for in a SUBSCRIBE. It
// is only ever set with ActionSubscribe.
//
// The two are otherwise identical, and an implementation that audit-logs,
// meters or rate-limits subscriptions needs to tell them apart: the client
// performed no action, and the same filters come back on every reconnect.
// It is also the cheap way out of the latency this check adds to a CONNACK
// — an implementation may answer a resume from a cache it would not trust
// for a fresh SUBSCRIBE.
Resume bool
// Will marks the Will Message of a CONNECT, asked about as an
// ActionPublish before the connection is accepted. It is only ever set with
// ActionPublish.
//
// The broker publishes a Will on the client's behalf after the client has
// gone, so this is the only chance to refuse it: a denial refuses the
// CONNECT with 0x87 (Not authorized), and the Will is not re-checked when it
// fires. An implementation that audit-logs publishes can use the flag to
// tell a Will — which may never be sent — from a PUBLISH the client made.
Will bool
// ClientID is the session's Client Identifier, after any assignment.
ClientID string
// Identity is the AuthResult.Identity from authentication, if any.
Identity string
// Username is the User Name from CONNECT, if any.
Username string
// Topic is the Topic Name for ActionPublish or the Topic Filter, shared
// prefix included, for ActionSubscribe.
Topic string
// QoS is the requested QoS.
QoS packet.QoS
// Retain is set for a publish with the RETAIN flag.
Retain bool
}
AuthzRequest describes an operation for an Authorizer.
type Broker ¶
type Broker struct {
// contains filtered or unexported fields
}
Broker is an MQTT v5 broker backed by a NATS server. Create one with New, run it with Serve, and release its resources with Close.
A Broker is safe for concurrent use.
func New ¶
New connects to NATS, provisions the JetStream assets the broker needs and binds the MQTT listener. It does not accept connections until Serve is called, so a caller can read ListenAddr first.
The returned Broker must be closed with Close.
func NewWithContext ¶
NewWithContext is New with a context bounding the NATS connection and JetStream setup.
func (*Broker) Close ¶
Close stops the broker: it closes the listener, disconnects every client with 0x8B (Server shutting down) and releases the NATS resources it owns. A NATS connection passed in through Options.Conn is left open.
func (*Broker) ListenAddr ¶
ListenAddr reports the address the broker accepts MQTT connections on. It is valid as soon as New returns, which lets a test bind :0 and discover the port.
func (*Broker) NATS ¶
NATS returns the NATS connection the broker uses, so an embedding application can share it rather than opening a second one.
func (*Broker) Reauthorize ¶ added in v0.5.0
Reauthorize re-runs the Authorizer over everything the session for clientID holds: each subscription, as at a resume, and its Will Message, as at CONNECT. Call it when a principal's permissions change; the broker does not poll a policy store it knows nothing about, and does not ask the Authorizer on every delivery.
A denied subscription is removed. It stops delivering at once, including messages already queued for the connection, and the messages it had in flight are withdrawn, so a later resume does not resend them; their acknowledgements are still accepted. A denied Will is discarded, including one already waiting out its Will Delay Interval. The connection itself is left alone: it is still allowed, and a DISCONNECT would publish the very Will being revoked. The client is not told which filters it lost; MQTT has no packet for that.
The Authorizer sees a subscription with AuthzRequest.Resume set and a Will with Will set, as it does for those checks elsewhere. As there, any error it returns is a denial, so an Authorizer that cannot reach its policy should not be asked to re-check a fleet during an outage.
Any broker will do. One that does not hold the session asks the others sharing its NATS connection and SubjectPrefix, and the one holding it runs the sweep and answers. When none holds it after ReauthorizeForwardWait (or when ctx ends first), it returns nil: a session held nowhere, only in the session store, is re-checked when it is next resumed. It returns ctx's error, having changed nothing further, if ctx ends during this broker's own sweep, and the holder's error if its sweep failed.
type ConnectError ¶
type ConnectError struct {
Code packet.ReasonCode
Reason string
}
ConnectError carries the CONNACK Reason Code an Authenticator wants the broker to send. Any code of 0x80 or greater from MQTT-5.0 Table 3-1 is valid; common choices are BadUserNameOrPassword (0x86), NotAuthorized (0x87), Banned (0x8A) and ClientIdentifierNotValid (0x85).
func (*ConnectError) Error ¶
func (e *ConnectError) Error() string
type Options ¶
type Options struct {
// NATSURL is the NATS server to use. Defaults to nats.DefaultURL. Ignored
// when Conn is set.
NATSURL string
// NATSOptions are passed to nats.Connect for credentials, TLS and so on.
// Ignored when Conn is set.
NATSOptions []nats.Option
// Conn is an already-established NATS connection to use instead of
// dialling. The broker does not close a connection it did not open.
Conn *nats.Conn
// Listen is the TCP address to accept MQTT connections on. Defaults to
// DefaultListen. Ignored when Listener is set.
Listen string
// Listener is an already-open listener to accept on instead of binding
// Listen. The broker closes it on shutdown.
Listener net.Listener
// TLSConfig, when non-nil, wraps the listener in TLS.
TLSConfig *tls.Config
// SubjectPrefix namespaces every NATS subject the broker publishes MQTT
// traffic on, keeping it clear of NATS system subjects. Defaults to
// DefaultSubjectPrefix. Set it to the same value on every broker that
// should share traffic.
SubjectPrefix string
// StreamPrefix names the JetStream assets the broker creates. Defaults to
// DefaultStreamPrefix. Brokers sharing a SubjectPrefix must share this.
StreamPrefix string
// DisableRetained turns off retained-message support, so no JetStream
// assets are created and CONNACK advertises Retain Available = 0
// (MQTT-5.0 §3.2.2.3.5). Use it against a NATS server without JetStream.
DisableRetained bool
// RetainedStorage selects file or memory storage for the retained-message
// stream. Defaults to file storage.
RetainedStorage jetstream.StorageType
// RetainedReplicas is the JetStream replica count for the retained-message
// stream. Defaults to 1.
RetainedReplicas int
// PersistentSessions keeps session state in a JetStream key-value bucket
// instead of in the broker process, so a client reconnecting with Clean
// Start 0 resumes its subscriptions after a broker restart and on a broker
// that has never seen it. Brokers sharing a StreamPrefix share the bucket
// and arbitrate ownership between themselves, so exactly one of them serves
// a given Client Identifier [MQTT-3.1.4-3].
//
// It needs JetStream, and it caps the Client Identifier at
// MaxPersistentClientIDLen bytes.
//
// A resumed session carries its subscription set and nothing else.
// In-flight QoS 1 and QoS 2 messages are not stored, because resending
// them needs an offline queue the broker does not have; the Will Message is
// not stored because it belongs to the network connection rather than to
// the session (MQTT-5.0 §3.1.2.5). ROADMAP.md has both.
PersistentSessions bool
// SessionStorage selects file or memory storage for the session bucket.
// Defaults to file storage. Memory storage makes sessions survive a broker
// restart but not a NATS restart.
SessionStorage jetstream.StorageType
// SessionReplicas is the JetStream replica count for the session bucket.
// Defaults to 1.
SessionReplicas int
// Authenticator, when non-nil, decides whether a CONNECT is accepted. When
// nil every connection is accepted, which is only appropriate on a trusted
// network.
Authenticator Authenticator
// Authorizer, when non-nil, decides whether a session may publish to or
// subscribe to a topic. When nil everything is permitted.
Authorizer Authorizer
// ReceiveMaximum is advertised in CONNACK and bounds the client's in-flight
// QoS 1 and QoS 2 publications. Defaults to DefaultReceiveMaximum.
ReceiveMaximum uint16
// MaximumQoS is the highest QoS the broker accepts in a PUBLISH and grants
// on a SUBSCRIBE, advertised in CONNACK [MQTT-3.2.2-9]. Values above 2 are
// clamped. Leave it nil for the default of 2; it is a pointer so that
// Ptr(uint8(0)) can express a broker that only does QoS 0, which the zero
// value would otherwise make unreachable.
MaximumQoS *uint8
// TopicAliasMaximum is advertised in CONNACK. Zero disables inbound topic
// aliases; leave it unset for DefaultTopicAliasMaximum.
TopicAliasMaximum *uint16
// MaximumPacketSize caps an inbound packet. Defaults to
// DefaultMaximumPacketSize.
MaximumPacketSize uint32
// ServerKeepAlive, when non-zero, overrides the client's requested Keep
// Alive and is returned in CONNACK [MQTT-3.1.2-21].
ServerKeepAlive uint16
// ConnectTimeout bounds the wait for a CONNECT on a new connection.
// Defaults to DefaultConnectTimeout.
ConnectTimeout time.Duration
// MaxSessionExpiry caps a requested Session Expiry Interval. Defaults to
// DefaultMaxSessionExpiry.
MaxSessionExpiry time.Duration
// Logger receives broker events. Defaults to slog.Default().
Logger *slog.Logger
}
Options configures a Broker. The zero value is usable: it connects to nats.DefaultURL and listens on :1883.
Fields are only ever added to this struct, never removed or repurposed, so a composite literal with field names keeps compiling across versions.
Source Files
¶
Directories
¶
| Path | Synopsis |
|---|---|
|
cmd
|
|
|
natsmqtt5
command
Command natsmqtt5 runs an MQTT v5 broker against an existing NATS server.
|
Command natsmqtt5 runs an MQTT v5 broker against an existing NATS server. |
|
Package packet implements the MQTT version 5.0 wire format as specified by the OASIS Standard of 07 March 2019.
|
Package packet implements the MQTT version 5.0 wire format as specified by the OASIS Standard of 07 March 2019. |
|
Package topic implements MQTT Topic Name and Topic Filter handling, and the mapping between MQTT topics and NATS subjects.
|
Package topic implements MQTT Topic Name and Topic Filter handling, and the mapping between MQTT topics and NATS subjects. |