A pure-Go implementation of the MMS (Manufacturing Message Specification) protocol, ISO 9506-1/2, designed as the base transport layer for industrial automation systems including IEC 61850.
- What is go-mms
- Features
- Quick Start
- Operations
- Architecture
- Out of Scope
- Status
- Documentation
- License
- Pure Go, no CGO — builds and cross-compiles with the standard toolchain.
- Client and server — both sides of the protocol in one library.
- Comprehensive MMS coverage for IEC 61850-oriented applications — variable access, named variable lists, file services, journal services, information reports, and more. Semaphore, Event, and Program Invocation services are outside scope.
- Base layer for go-iec61850 — go-mms handles MMS; IEC 61850 abstractions live in a separate package.
| Category | Services |
|---|---|
| Association | Initiate, Conclude, Abort |
| Identity & Status | Identify, Status |
| Variable Access | Read (single, multi-variable, alternate access), Write (single, multi-variable, alternate access) |
| Named Variable Lists | DefineNamedVariableList, GetNamedVariableListAttributes, DeleteNamedVariableList |
| Information Reports | Send (server), receive (client), broadcast |
| File Services | Open, Read, Close, Directory, Delete, Rename, ObtainFile, DownloadFile |
| Journal Services | ReadJournal (time-range and start-after cursors) |
| Type Introspection | GetVariableAccessAttributes |
| Object Enumeration | GetNameList with automatic pagination |
All services are supported on both client and server unless noted in COMPLIANCE.md.
go get github.com/otfabric/go-mmsRequires Go 1.26 or later.
package main
import (
"context"
"fmt"
"log"
mms "github.com/otfabric/go-mms"
"github.com/otfabric/go-mms/transport/iso"
)
func main() {
ctx := context.Background()
client, err := iso.Dial(ctx, "10.0.0.1:102",
iso.WithClientDialOptions(mms.DialOptions{
MMS: mms.MMSOptions{MaxPDUSize: 65000},
}),
)
if err != nil {
log.Fatal(err)
}
defer client.Close(ctx)
id, err := client.Identify(ctx)
if err != nil {
log.Fatal(err)
}
fmt.Printf("Vendor: %s Model: %s Rev: %s\n", id.Vendor, id.Model, id.Revision)
result, err := client.Read(ctx, mms.ReadRequest{
DomainID: "MyDomain",
ItemID: "MyVariable",
})
if err != nil {
log.Fatal(err)
}
fmt.Println("Value:", result.Value)
}package main
import (
"context"
"errors"
"log"
mms "github.com/otfabric/go-mms"
"github.com/otfabric/go-mms/transport/iso"
)
func main() {
srv := mms.NewServer(mms.ServerOptions{
MMS: mms.ServerMMSOptions{MaxPDUSize: 65000},
})
srv.HandleIdentify(func(_ context.Context, _ mms.IdentifyRequest) (*mms.ServerIdentity, error) {
return &mms.ServerIdentity{Vendor: "Acme", Model: "Controller", Revision: "1.0"}, nil
})
srv.RegisterDomain("process")
srv.RegisterVariable(mms.Variable{
Name: mms.ObjectName{Scope: mms.ObjectScopeDomain, Domain: "process", ItemID: "temperature"},
TypeSpec: mms.TypeSpec{Type: mms.ValueTypeFloat, FormatWidth: 32, ExponentWidth: 8},
Read: func(_ context.Context) (*mms.Value, error) {
return mms.NewFloat(21.5), nil
},
Write: func(_ context.Context, v *mms.Value) error {
f, ok := v.Float64()
if !ok {
return errors.New("expected float")
}
_ = f // store the value
return nil
},
})
ln, err := iso.Listen(":102")
if err != nil {
log.Fatal(err)
}
log.Fatal(srv.ListenAndServe(context.Background(), ln))
}See examples/ for complete runnable examples.
- Errors — every error is classified with sentinels and typed errors
(
ErrClosed,ErrInvokeTimeout,ErrInvalidArgument,*ServiceError,*RejectError, ...). Useerrors.Is/errors.As, never the message. See ERRORS.md. - Timeouts —
DialOptions.RequestTimeoutbounds each confirmed request;ServerOptions.AssociationTimeoutandiso.WithHandshakeTimeoutbound connection setup. See TIMEOUT_AND_CLOSE.md and LIMITS.md. - Reconnect and retry —
Client.Done()/Client.Err()report a lost association;iso.WithRetry(mms.DefaultRetryPolicy())retries dialing, andmms.Retrywraps any operation in backoff with jitter. - Graceful shutdown —
Server.Shutdown(ctx)stops accepting, lets in-flight requests finish and aborts the associations;ListenAndServethen returnsmms.ErrServerClosed. - Metrics and logging — implement
mms.Metrics(DialOptions.Metrics,ServerOptions.Metrics) to export request and connection events to Prometheus or OpenTelemetry;log/slogloggers viaDialOptions.Logger,ServerOptions.Loggerandiso.WithLogger. See OBSERVABILITY.md.
client, err := iso.Dial(ctx, "10.0.0.1:102",
iso.WithRetry(mms.DefaultRetryPolicy()),
iso.WithClientDialOptions(mms.DialOptions{
RequestTimeout: 5 * time.Second,
Metrics: myMetrics, // implements mms.Metrics
}),
)
if err != nil {
log.Fatal(err)
}
go func() {
<-client.Done()
log.Println("association lost:", client.Err())
}()
// Server: stop gracefully on SIGTERM.
go func() {
<-sigterm
shutdownCtx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
_ = srv.Shutdown(shutdownCtx)
}()
if err := srv.ListenAndServe(ctx, ln); !errors.Is(err, mms.ErrServerClosed) {
log.Fatal(err)
}go-mms implements the full ISO/OSI upper-layer stack required by MMS:
TPKT (RFC 1006)
└─ COTP (ISO 8073)
└─ Session (ISO 8327-1)
└─ Presentation (ISO 8823-1)
└─ ACSE (ISO 8650-1)
└─ MMS (ISO 9506-1/2)
The lower transport layers (TPKT and COTP) are provided by companion
libraries go-tpkt and
go-cotp. The transport/iso
subpackage wires them together and exposes Dial (client) and Listen
(server) entry points.
For custom or test transports, implement the Transport interface and
pass it directly to NewClient or Server.Serve.
The following are intentionally not part of go-mms:
- IEC 61850 abstractions — data model, SCL, logical nodes, datasets. These belong in go-iec61850.
- ASN.1 code generation — BER encoding is hand-written for performance and control.
- Semaphore, Event, and Program Invocation services — rarely used MMS services not required for IEC 61850.
- GOOSE, Sampled Values, GSE — non-MMS protocols defined in IEC 61850 parts 8-1 and 9-2.
go-mms follows semantic versioning. The v1 API is stable; backward-incompatible public API changes are reserved for a future major release. Patch releases may correct protocol encoding or decoding defects without being considered breaking changes — a standards-correct wire fix can change observable behaviour while remaining API-compatible.
- Well-tested: 1,000+ unit tests, 60+ end-to-end tests, 200+ negative tests, race/concurrency and chaos tests, 60 fuzz targets, 21 benchmarks.
- Quality gates (CI
qualityjob; coverage and the short fuzz pass are also part ofmake check): ≥ 90% statement coverage per package and in total (make coverage), a fuzz pass over every target (make fuzz-short;make fuzzfor longer runs), and chaos tests under-racethrough a fault-injecting connection (make chaos). See PROGRESS.md. - Hardened: strict decoder validation, bounds checking on all PDU fields, resource limits on negotiated parameters.
- Interop-verified: client and server behaviour tested bidirectionally against a pinned libiec61850 adapter; see INTEROP.md. Testing against physical IEDs has not yet been formally established.
examples/— runnable client and server examplesAPI.md— complete public API reference (types, methods, constants)ARCHITECTURE.md— layers, goroutines and ownershipERRORS.md— error taxonomy and handling guideTESTING.md— test tiers, coverage policy, fuzz regressionOBSERVABILITY.md— metrics interface (Prometheus example), slog loggingTIMEOUT_AND_CLOSE.md— timeouts, cancellation, reconnect and shutdownKNOWN_LIMITATIONS.md·LIMITS.md— concurrency / outstanding-request behaviour and boundsINTEROP.md— interoperability tests and MMS compatibility matrixCOMPLIANCE.md— service support matrix with test coveragePROGRESS.md— current status and quality gatesRELEASE.md— changelog- pkg.go.dev — generated API reference
This project is licensed under the MIT License. See LICENSE.