Skip to content

Commit

Permalink
feat(mqtt): add support for MQTT broker functionality
Browse files Browse the repository at this point in the history
  • Loading branch information
activeshadow committed Jun 23, 2024
1 parent 5787355 commit 4cbda55
Show file tree
Hide file tree
Showing 9 changed files with 768 additions and 420 deletions.
14 changes: 9 additions & 5 deletions src/go/go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ module github.com/patsec/ot-sim

go 1.21

toolchain go1.21.1

require (
actshad.dev/mbserver v0.3.1
actshad.dev/modbus v0.2.1
Expand All @@ -13,25 +15,27 @@ require (
github.com/gofrs/uuid v4.4.0+incompatible
github.com/gorilla/mux v1.8.0
github.com/gorilla/websocket v1.5.0
github.com/mochi-mqtt/server/v2 v2.4.2
github.com/pebbe/zmq4 v1.2.7
github.com/prometheus/client_golang v1.12.2
github.com/reiver/go-telnet v0.0.0-20180421082511-9ff0b2ab096e
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9
golang.org/x/crypto v0.14.0
gopkg.in/natefinch/lumberjack.v2 v2.2.1
)

require (
github.com/beorn7/perks v1.0.1 // indirect
github.com/cespare/xxhash/v2 v2.1.2 // indirect
github.com/golang/protobuf v1.5.2 // indirect
github.com/google/go-cmp v0.5.8 // indirect
github.com/matttproud/golang_protobuf_extensions v1.0.1 // indirect
github.com/prometheus/client_model v0.2.0 // indirect
github.com/prometheus/common v0.32.1 // indirect
github.com/prometheus/procfs v0.7.3 // indirect
github.com/reiver/go-oi v1.0.0 // indirect
golang.org/x/net v0.0.0-20210525063256-abc453219eb5 // indirect
github.com/rs/xid v1.4.0 // indirect
golang.org/x/net v0.17.0 // indirect
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c // indirect
golang.org/x/sys v0.12.0 // indirect
google.golang.org/protobuf v1.26.0 // indirect
golang.org/x/sys v0.13.0 // indirect
google.golang.org/protobuf v1.28.1 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)
22 changes: 17 additions & 5 deletions src/go/go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -148,6 +148,8 @@ github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/ad
github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
github.com/jinzhu/copier v0.3.5 h1:GlvfUwHk62RokgqVNvYsku0TATCF7bAHVwEXoBh3iJg=
github.com/jinzhu/copier v0.3.5/go.mod h1:DfbEm0FYsaqBcKcFuvmOZb218JkPGtvSHsKg8S8hyyg=
github.com/jpillora/backoff v1.0.0/go.mod h1:J/6gKK9jxlEcS3zixgDgUAsiuZ7yrSoa/FX5e0EB2j4=
github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU=
github.com/json-iterator/go v1.1.10/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
Expand All @@ -161,11 +163,15 @@ github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+o
github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
github.com/konsorten/go-windows-terminal-sequences v1.0.3/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc=
github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/matttproud/golang_protobuf_extensions v1.0.1 h1:4hp9jkHxhMHkqkrB3Ix0jegS5sx/RkqARlsWZ6pIwiU=
github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=
github.com/mochi-mqtt/server/v2 v2.4.2 h1:x7xC41Qn/ek1hOWNcZraRm+Cmqc2yrfhD5VA1NFnXhc=
github.com/mochi-mqtt/server/v2 v2.4.2/go.mod h1:M1lZnLbyowXUyQBIlHYlX1wasxXqv/qFWwQxAzfphwA=
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
Expand Down Expand Up @@ -207,6 +213,8 @@ github.com/reiver/go-oi v1.0.0/go.mod h1:RrDBct90BAhoDTxB1fenZwfykqeGvhI6LsNfStJ
github.com/reiver/go-telnet v0.0.0-20180421082511-9ff0b2ab096e h1:quuzZLi72kkJjl+f5AQ93FMcadG19WkS7MO6TXFOSas=
github.com/reiver/go-telnet v0.0.0-20180421082511-9ff0b2ab096e/go.mod h1:+5vNVvEWwEIx86DB9Ke/+a5wBI464eDRo3eF0LcfpWg=
github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
github.com/rs/xid v1.4.0 h1:qd7wPTDkN6KQx2VmMBLrpHkiyQwgFXRnkOLacUiaSNY=
github.com/rs/xid v1.4.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg=
github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo=
github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE=
github.com/sirupsen/logrus v1.6.0/go.mod h1:7uNnSEd1DgxDLC74fIahvMZmmYsHGZGEOFrfsX/uA88=
Expand All @@ -232,8 +240,9 @@ golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACk
golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9 h1:psW17arqaxU48Z5kZ0CQnkZWQJsqcURM6tKiBApRjXI=
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.14.0 h1:wBqGXzWJW6m1XrIKlAH0Hs1JJ7+9KBwnIO8v66Q9cHc=
golang.org/x/crypto v0.14.0/go.mod h1:MVFd36DqK4CsrnJYDkBA3VC4m2GkXAM0PvzMCn4JQf4=
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8=
Expand Down Expand Up @@ -293,8 +302,9 @@ golang.org/x/net v0.0.0-20200520182314-0ba52f642ac2/go.mod h1:qpuaurCH72eLCgpAm/
golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
golang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
golang.org/x/net v0.0.0-20210525063256-abc453219eb5 h1:wjuX4b5yYQnEQHzd+CBcrcC6OVR2J1CN6mUy0oSxIPo=
golang.org/x/net v0.0.0-20210525063256-abc453219eb5/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.17.0 h1:pVaXccu2ozPjCXewfr1S7xza/zcXTity9cCdXQYSjIM=
golang.org/x/net v0.17.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE=
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
Expand Down Expand Up @@ -348,8 +358,8 @@ golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7w
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210603081109-ebe580a85c40/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220114195835-da31bd327af9/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.12.0 h1:CM0HF96J0hcLAwsHPJZjfdNzs0gftsLfgKt57wWHJ0o=
golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.13.0 h1:Af8nKPmuFypiUBjVoU9V20FiaFXOcuZI21p0ycVYYGE=
golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
Expand Down Expand Up @@ -478,11 +488,13 @@ google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpAD
google.golang.org/protobuf v1.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGjtUeSXeh4=
google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c=
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
google.golang.org/protobuf v1.26.0 h1:bxAC2xTBsZGibn2RTntX0oH50xLsqy1OxA9tTL3p/lk=
google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
google.golang.org/protobuf v1.28.1 h1:d0NfwRgPtno5B1Wa6L2DAG+KivqkdutMf1UhdNx175w=
google.golang.org/protobuf v1.28.1/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo=
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
gopkg.in/natefinch/lumberjack.v2 v2.2.1 h1:bBRl1b0OH9s/DuPhuXpNl+VtCaJXFZ5/uEFST95x9zc=
Expand Down
213 changes: 213 additions & 0 deletions src/go/mqtt/broker/broker.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,213 @@
package broker

import (
"bytes"
"context"
"crypto/tls"
"encoding/binary"
"errors"
"fmt"
"strings"

"github.com/patsec/ot-sim/mqtt/types"
"github.com/patsec/ot-sim/msgbus"

"github.com/beevik/etree"
mochi "github.com/mochi-mqtt/server/v2"
"github.com/mochi-mqtt/server/v2/hooks/auth"
"github.com/mochi-mqtt/server/v2/listeners"
)

/*
<mqtt mode="broker">
<!-- broker can listen on multiple interfaces (just TCP for now) -->
<endpoint>
<address>127.0.0.1:1883</address>
</endpoint>
<endpoint>
<address>10.11.12.13:8883</address>
<tls>
<ca>/etc/ot-sim/root.pem</ca>
<key>/etc/ot-sim/broker.key</key>
<certificate>/etc/ot-sim/broker.crt</certificate>
</tls>
</endpoint>
<endpoint>127.0.0.1:1883</endpoint> <!-- alternative way to specify -->
<topic tag="foo.bar">foo/bar</topic>
</mqtt>
*/

type MQTTBroker struct {
pubEndpoint string
pullEndpoint string

endpoints []types.Endpoint

name string
topicToTag map[string]string
tagToTopic map[string]string

server *mochi.Server
}

func New(name string) *MQTTBroker {
return &MQTTBroker{
name: name,
topicToTag: make(map[string]string),
tagToTopic: make(map[string]string),
}
}

func (this MQTTBroker) Name() string {
return this.name
}

func (this *MQTTBroker) Configure(e *etree.Element) error {
for _, child := range e.ChildElements() {
switch child.Tag {
case "pull-endpoint":
this.pullEndpoint = child.Text()
case "endpoint":
var endpoint types.Endpoint

if len(child.ChildElements()) == 0 {
endpoint.Address = child.Text()
} else {
for _, child := range child.ChildElements() {
switch child.Tag {
case "address":
endpoint.Address = child.Text()
case "tls":
for _, child := range child.ChildElements() {
switch child.Tag {
case "ca":
endpoint.CAPath = child.Text()
case "key":
endpoint.KeyPath = child.Text()
case "certificate":
endpoint.CertPath = child.Text()
}
}
}
}
}

this.endpoints = append(this.endpoints, endpoint)
case "topic":
var (
topic = child.Text()
tag = child.SelectAttrValue("tag", strings.ReplaceAll(topic, "/", "."))
)

this.topicToTag[topic] = tag
this.tagToTopic[tag] = topic
}
}

for idx, endpoint := range this.endpoints {
if err := endpoint.Validate(); err != nil {
return fmt.Errorf("validating endpoint: %w", err)
}

this.endpoints[idx] = endpoint
}

return nil
}

func (this *MQTTBroker) Run(ctx context.Context, pubEndpoint, pullEndpoint string) error {
// Use ZeroMQ PUB endpoint specified in `mqtt` config block if provided.
if this.pubEndpoint != "" {
pubEndpoint = this.pubEndpoint
}

// Use ZeroMQ PULL endpoint specified in `mqtt` config block if provided.
if this.pullEndpoint != "" {
pullEndpoint = this.pullEndpoint
}

if len(this.endpoints) == 0 {
return fmt.Errorf("no MQTT broker listener endpoints provided")
}

msgBusHook := &PublishToMsgBus{
name: this.name,
pusher: msgbus.MustNewPusher(pullEndpoint),
topics: this.topicToTag,
log: this.log,
}

subscriber := msgbus.MustNewSubscriber(pubEndpoint)
subscriber.AddUpdateHandler(this.handleMsgBusUpdate)
subscriber.Start("RUNTIME")

this.server = mochi.New(&mochi.Options{InlineClient: true})
this.server.AddHook(new(auth.AllowHook), nil)
this.server.AddHook(msgBusHook, nil)

for i, endpoint := range this.endpoints {
var config *listeners.Config

if !endpoint.Insecure {
config = &listeners.Config{
TLSConfig: &tls.Config{
RootCAs: endpoint.Roots,
Certificates: []tls.Certificate{endpoint.Cert},
},
}
}

l := listeners.NewTCP(fmt.Sprintf("t%d", i), endpoint.Address, config)

if err := this.server.AddListener(l); err != nil {
return fmt.Errorf("adding TCP listener to MQTT broker: %w", err)
}
}

go func() {
if err := this.server.Serve(); err != nil {
this.log("[ERROR] serving MQTT broker: %v", err)
}
}()

go func() {
<-ctx.Done()
this.server.Close()
}()

return nil
}

func (this *MQTTBroker) handleMsgBusUpdate(env msgbus.Envelope) {
if env.Sender() == this.name {
return
}

update, err := env.Update()
if err != nil {
if !errors.Is(err, msgbus.ErrKindNotUpdate) {
this.log("[ERROR] getting Update message from envelope: %v", err)
}

return
}

for _, point := range update.Updates {
this.log("[DEBUG] received update for tag %s (value: %f)", point.Tag, point.Value)

if topic, ok := this.tagToTopic[point.Tag]; ok {
var buf bytes.Buffer
if err := binary.Write(&buf, binary.BigEndian, point.Value); err != nil {
this.log("[ERROR] converting value %f for tag %s to bytes: %v", point.Value, point.Tag, err)
}

this.log("[DEBUG] publishing value %f to topic %s", point.Value, topic)

this.server.Publish(topic, buf.Bytes(), false, 0)
}
}
}

func (this MQTTBroker) log(format string, a ...any) {
fmt.Printf("[%s] %s\n", this.name, fmt.Sprintf(format, a...))
}
61 changes: 61 additions & 0 deletions src/go/mqtt/broker/hooks.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
package broker

import (
"bytes"
"strconv"

mochi "github.com/mochi-mqtt/server/v2"
"github.com/mochi-mqtt/server/v2/packets"
"github.com/patsec/ot-sim/msgbus"
)

type PublishToMsgBus struct {
mochi.HookBase

name string

pusher *msgbus.Pusher
topics map[string]string

log func(string, ...any)
}

func (this *PublishToMsgBus) ID() string {
return "publish-to-ot-sim-msg-bus"
}

func (this *PublishToMsgBus) Provides(b byte) bool {
return bytes.Contains([]byte{
mochi.OnPublished,
}, []byte{b})
}

func (this *PublishToMsgBus) OnPublished(c *mochi.Client, p packets.Packet) {
if c.ID == "inline" {
return
}

this.log("[DEBUG] topic: %s -- payload: %s", p.TopicName, string(p.Payload))

if tag, ok := this.topics[p.TopicName]; ok {
var points []msgbus.Point

value, err := strconv.ParseFloat(string(p.Payload), 64)
if err != nil {
this.log("[ERROR] parsing payload for topic %s to float64: %v", p.TopicName, err)
return
}

points = append(points, msgbus.Point{Tag: tag, Value: value})

env, err := msgbus.NewEnvelope(this.name, msgbus.Status{Measurements: points})
if err != nil {
this.log("[ERROR] creating status message: %v", err)
return
}

if err := this.pusher.Push("RUNTIME", env); err != nil {
this.log("[ERROR] sending status message: %v", err)
}
}
}
Loading

0 comments on commit 4cbda55

Please sign in to comment.