first init
This commit is contained in:
21
LICENSE.txt
Normal file
21
LICENSE.txt
Normal file
@@ -0,0 +1,21 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2019 Simon Vetter
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
199
README.md
199
README.md
@@ -0,0 +1,199 @@
|
||||
## Go modbus stack
|
||||
|
||||
### Description
|
||||
This package is a go implementation of the modbus protocol.
|
||||
It aims to provide a simple-to-use, high-level API to interact with modbus
|
||||
devices using native Go types.
|
||||
|
||||
Both client and server components are available.
|
||||
|
||||
The client supports the following modes:
|
||||
- modbus RTU (serial, over both RS-232 and RS-485),
|
||||
- modbus TCP (a.k.a. MBAP),
|
||||
- modbus TCP over TLS (a.k.a. MBAPS or Modbus Security),
|
||||
- modbus TCP over UDP (a.k.a. MBAP over UDP),
|
||||
- modbus RTU over TCP (RTU tunneled in TCP for use with e.g. remote serial
|
||||
ports or cheap TCP to serial bridges),
|
||||
- modbus RTU over UDP (RTU tunneled in UDP).
|
||||
|
||||
Please note that UDP transports are not part of the Modbus specification.
|
||||
Some devices expect MBAP (modbus TCP) framing in UDP packets while others
|
||||
use RTU frames instead. The client support both so if unsure, try with
|
||||
both udp:// and rtuoverudp:// schemes.
|
||||
|
||||
The server supports:
|
||||
- modbus TCP (a.k.a. MBAP),
|
||||
- modbus TCP over TLS (a.k.a. MBAPS or Modbus Security).
|
||||
|
||||
A CLI client is available in cmd/modbus-cli.go and can be built with
|
||||
```bash
|
||||
$ go build -o modbus-cli cmd/modbus-cli.go
|
||||
$ ./modbus-cli --help
|
||||
```
|
||||
|
||||
### Getting started
|
||||
```bash
|
||||
$ go get github.com/simonvetter/modbus
|
||||
```
|
||||
|
||||
### Using the client
|
||||
|
||||
```golang
|
||||
import (
|
||||
"github.com/simonvetter/modbus"
|
||||
)
|
||||
|
||||
func main() {
|
||||
var client *modbus.ModbusClient
|
||||
var err error
|
||||
|
||||
// for a TCP endpoint
|
||||
// (see examples/tls_client.go for TLS usage and options)
|
||||
client, err = modbus.NewClient(&modbus.ClientConfiguration{
|
||||
URL: "tcp://hostname-or-ip-address:502",
|
||||
Timeout: 1 * time.Second,
|
||||
})
|
||||
// note: use udp:// for modbus TCP over UDP
|
||||
|
||||
// for an RTU (serial) device/bus
|
||||
client, err = modbus.NewClient(&modbus.ClientConfiguration{
|
||||
URL: "rtu:///dev/ttyUSB0",
|
||||
Speed: 19200, // default
|
||||
DataBits: 8, // default, optional
|
||||
Parity: modbus.PARITY_NONE, // default, optional
|
||||
StopBits: 2, // default if no parity, optional
|
||||
Timeout: 300 * time.Millisecond,
|
||||
})
|
||||
|
||||
// for an RTU over TCP device/bus (remote serial port or
|
||||
// simple TCP-to-serial bridge)
|
||||
client, err = modbus.NewClient(&modbus.ClientConfiguration{
|
||||
URL: "rtuovertcp://hostname-or-ip-address:502",
|
||||
Speed: 19200, // serial link speed
|
||||
Timeout: 1 * time.Second,
|
||||
})
|
||||
// note: use rtuoverudp:// for modbus RTU over UDP
|
||||
|
||||
if err != nil {
|
||||
// error out if client creation failed
|
||||
}
|
||||
|
||||
// now that the client is created and configured, attempt to connect
|
||||
err = client.Open()
|
||||
if err != nil {
|
||||
// error out if we failed to connect/open the device
|
||||
// note: multiple Open() attempts can be made on the same client until
|
||||
// the connection succeeds (i.e. err == nil), calling the constructor again
|
||||
// is unnecessary.
|
||||
// likewise, a client can be opened and closed as many times as needed.
|
||||
}
|
||||
|
||||
// read a single 16-bit holding register at address 100
|
||||
var reg16 uint16
|
||||
reg16, err = client.ReadRegister(100, modbus.HOLDING_REGISTER)
|
||||
if err != nil {
|
||||
// error out
|
||||
} else {
|
||||
// use value
|
||||
fmt.Printf("value: %v", reg16) // as unsigned integer
|
||||
fmt.Printf("value: %v", int16(reg16)) // as signed integer
|
||||
}
|
||||
|
||||
// read 4 consecutive 16-bit input registers starting at address 100
|
||||
var reg16s []uint16
|
||||
reg16s, err = client.ReadRegisters(100, 4, modbus.INPUT_REGISTER)
|
||||
|
||||
// read the same 4 consecutive 16-bit input registers as 2 32-bit integers
|
||||
var reg32s []uint32
|
||||
reg32s, err = client.ReadUint32s(100, 2, modbus.INPUT_REGISTER)
|
||||
|
||||
// read the same 4 consecutive 16-bit registers as a single 64-bit integer
|
||||
var reg64 uint64
|
||||
reg64, err = client.ReadUint64(100, modbus.INPUT_REGISTER)
|
||||
|
||||
// read the same 4 consecutive 16-bit registers as a slice of bytes
|
||||
var regBs []byte
|
||||
regBs, err = client.ReadBytes(100, 8, modbus.INPUT_REGISTER)
|
||||
|
||||
// by default, 16-bit integers are decoded as big-endian and 32/64-bit values as
|
||||
// big-endian with the high word first.
|
||||
// change the byte/word ordering of subsequent requests to little endian, with
|
||||
// the low word first (note that the second argument only affects 32/64-bit values)
|
||||
client.SetEncoding(modbus.LITTLE_ENDIAN, modbus.LOW_WORD_FIRST)
|
||||
|
||||
// read the same 4 consecutive 16-bit input registers as 2 32-bit floats
|
||||
var fl32s []float32
|
||||
fl32s, err = client.ReadFloat32s(100, 2, modbus.INPUT_REGISTER)
|
||||
|
||||
// write -200 to 16-bit (holding) register 100, as a signed integer
|
||||
var s int16 = -200
|
||||
err = client.WriteRegister(100, uint16(s))
|
||||
|
||||
// Switch to unit ID (a.k.a. slave ID) #4
|
||||
client.SetUnitId(4)
|
||||
|
||||
// write 3 floats to registers 100 to 105
|
||||
err = client.WriteFloat32s(100, []float32{
|
||||
3.14,
|
||||
1.1,
|
||||
-783.22,
|
||||
})
|
||||
|
||||
// write 0x0102030405060708 to 16-bit (holding) registers 10 through 13
|
||||
// (8 bytes i.e. 4 consecutive modbus registers)
|
||||
err = client.WriteBytes(10, []byte{
|
||||
0x01, 0x02, 0x03, 0x04,
|
||||
0x05, 0x06, 0x07, 0x08,
|
||||
})
|
||||
|
||||
// close the TCP connection/serial port
|
||||
client.Close()
|
||||
}
|
||||
```
|
||||
### Using the server component
|
||||
See:
|
||||
* [examples/tcp_server.go](examples/tcp_server.go) for a modbus TCP example
|
||||
* [examples/tls_server.go](examples/tls_server.go) for TLS and Modbus Security features
|
||||
|
||||
### Supported function codes, golang object types and endianness/word ordering
|
||||
Function codes:
|
||||
* Read coils (0x01)
|
||||
* Read discrete inputs (0x02)
|
||||
* Read holding registers (0x03)
|
||||
* Read input registers (0x04)
|
||||
* Write single coil (0x05)
|
||||
* Write single register (0x06)
|
||||
* Write multiple coils (0x0f)
|
||||
* Write multiple registers (0x10)
|
||||
|
||||
Go object types:
|
||||
* Booleans (coils and discrete inputs)
|
||||
* Bytes (input and holding registers)
|
||||
* Signed/Unisgned 16-bit integers (input and holding registers)
|
||||
* Signed/Unsigned 32-bit integers (input and holding registers)
|
||||
* 32-bit floating point numbers (input and holding registers)
|
||||
* Signed/Unsigned 64-bit integers (input and holding registers)
|
||||
* 64-bit floating point numbers (input and holding registers)
|
||||
|
||||
Byte encoding/endianness/word ordering:
|
||||
* Little and Big endian for byte slices and 16-bit integers
|
||||
* Little and Big endian, with and without word swap for 32 and 64-bit
|
||||
integers and floating point numbers.
|
||||
|
||||
### Logging ###
|
||||
Both client and server objects will log to stdout by default.
|
||||
This behavior can be overriden by passing a log.Logger object
|
||||
through the Logger property of ClientConfiguration/ServerConfiguration.
|
||||
|
||||
### TODO (in no particular order)
|
||||
* Add RTU (serial) support to the server
|
||||
* Add more tests
|
||||
* Add diagnostics register support
|
||||
* Add fifo register support
|
||||
* Add file register support
|
||||
|
||||
### Dependencies
|
||||
* [github.com/goburrow/serial](https://github.com/goburrow/serial) for access to the serial port (thanks!)
|
||||
|
||||
### License
|
||||
MIT.
|
||||
|
||||
584
client_tls_test.go
Normal file
584
client_tls_test.go
Normal file
@@ -0,0 +1,584 @@
|
||||
package modbus
|
||||
|
||||
import (
|
||||
"crypto/tls"
|
||||
"crypto/x509"
|
||||
"io"
|
||||
"fmt"
|
||||
"net"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
const (
|
||||
// note: these certs and associated keys are self-signed
|
||||
// and only meant to be used with this test.
|
||||
// PLEASE DO NOT USE THEM FOR ANYTHING ELSE, EVER, as they
|
||||
// do not provide any kind of security.
|
||||
serverCert string = `-----BEGIN CERTIFICATE-----
|
||||
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
|
||||
-----END CERTIFICATE-----`
|
||||
|
||||
serverKey string = `-----BEGIN PRIVATE KEY-----
|
||||
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
|
||||
yAqUh6zMzzcee67KOWWZMTuPQuu1n/m1
|
||||
-----END PRIVATE KEY-----`
|
||||
|
||||
clientCert string = `
|
||||
-----BEGIN CERTIFICATE-----
|
||||
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
|
||||
-----END CERTIFICATE-----
|
||||
`
|
||||
clientKey string = `-----BEGIN PRIVATE KEY-----
|
||||
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
|
||||
iaKCVjivzjP1s/q6adzOOZVlVwm7Xw==
|
||||
-----END PRIVATE KEY-----
|
||||
`
|
||||
)
|
||||
|
||||
// TestTCPOVerTLSClient tests the TLS layer of the modbus client.
|
||||
func TestTCPoverTLSClient(t *testing.T) {
|
||||
var err error
|
||||
var client *ModbusClient
|
||||
var serverKeyPair tls.Certificate
|
||||
var clientKeyPair tls.Certificate
|
||||
var clientCp *x509.CertPool
|
||||
var serverCp *x509.CertPool
|
||||
var serverHostPort string
|
||||
var serverChan chan string
|
||||
var regs []uint16
|
||||
|
||||
serverChan = make(chan string)
|
||||
|
||||
// load server and client keypairs
|
||||
serverKeyPair, err = tls.X509KeyPair([]byte(serverCert), []byte(serverKey))
|
||||
if err != nil {
|
||||
t.Errorf("failed to load test server key pair: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
clientKeyPair, err = tls.X509KeyPair([]byte(clientCert), []byte(clientKey))
|
||||
if err != nil {
|
||||
t.Errorf("failed to load test client key pair: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
// start with an empty client cert pool initially to reject the server
|
||||
// certificate
|
||||
clientCp = x509.NewCertPool()
|
||||
|
||||
// start with an empty server cert pool initially to reject the client
|
||||
// certificate
|
||||
serverCp = x509.NewCertPool()
|
||||
|
||||
// start a mock modbus TLS server
|
||||
go runMockTLSServer(t, serverKeyPair, serverCp, serverChan)
|
||||
|
||||
// wait for the test server goroutine to signal its readiness
|
||||
// and network location
|
||||
serverHostPort = <-serverChan
|
||||
|
||||
// attempt to create a client without specifying any TLS configuration
|
||||
// parameter: should fail
|
||||
client, err = NewClient(&ClientConfiguration{
|
||||
URL: fmt.Sprintf("tcp+tls://%s", serverHostPort),
|
||||
})
|
||||
if err != ErrConfigurationError {
|
||||
t.Errorf("NewClient() should have failed with %v, got: %v",
|
||||
ErrConfigurationError, err)
|
||||
}
|
||||
|
||||
// attempt to create a client without specifying any TLS server
|
||||
// cert/CA: should fail
|
||||
client, err = NewClient(&ClientConfiguration{
|
||||
URL: fmt.Sprintf("tcp+tls://%s", serverHostPort),
|
||||
TLSClientCert: &clientKeyPair,
|
||||
})
|
||||
if err != ErrConfigurationError {
|
||||
t.Errorf("NewClient() should have failed with %v, got: %v",
|
||||
ErrConfigurationError, err)
|
||||
}
|
||||
|
||||
// attempt to create a client with both client cert+key and server
|
||||
// cert/CA: should succeed
|
||||
client, err = NewClient(&ClientConfiguration{
|
||||
URL: fmt.Sprintf("tcp+tls://%s", serverHostPort),
|
||||
TLSClientCert: &clientKeyPair,
|
||||
TLSRootCAs: clientCp,
|
||||
})
|
||||
if err != nil {
|
||||
t.Errorf("NewClient() should have succeeded, got: %v", err)
|
||||
}
|
||||
|
||||
// connect to the server: should fail with a TLS error as the server cert
|
||||
// is not yet trusted by the client
|
||||
err = client.Open()
|
||||
if err == nil {
|
||||
t.Errorf("Open() should have failed")
|
||||
}
|
||||
|
||||
// now load the server certificate into the client's trusted cert pool
|
||||
// to get the client to accept the server's certificate
|
||||
if !clientCp.AppendCertsFromPEM([]byte(serverCert)) {
|
||||
t.Errorf("failed to load test server cert into cert pool")
|
||||
}
|
||||
|
||||
// connect to the server: should succeed
|
||||
// note: client certificates are verified after the handshake procedure
|
||||
// has completed, so Open() won't fail even though the client cert
|
||||
// is rejected by the server.
|
||||
// (see RFC 8446 section 4.6.2 Post Handshake Authentication)
|
||||
err = client.Open()
|
||||
if err != nil {
|
||||
t.Errorf("Open() should have succeeded, got: %v", err)
|
||||
}
|
||||
|
||||
// attempt to read two registers: since the client cert won't pass
|
||||
// the validation step yet (no cert in server cert pool),
|
||||
// expect a tls error
|
||||
regs, err = client.ReadRegisters(0x1000, 2, INPUT_REGISTER)
|
||||
if err == nil {
|
||||
t.Errorf("ReadRegisters() should have failed")
|
||||
}
|
||||
client.Close()
|
||||
|
||||
// now place the client cert in the server's authorized client list
|
||||
// to get the client cert past the validation procedure
|
||||
if !serverCp.AppendCertsFromPEM([]byte(clientCert)) {
|
||||
t.Errorf("failed to load test client cert into cert pool")
|
||||
}
|
||||
|
||||
// connect to the server: should succeed
|
||||
err = client.Open()
|
||||
if err != nil {
|
||||
t.Errorf("Open() should have succeeded, got: %v", err)
|
||||
}
|
||||
|
||||
// attempt to read two registers: should succeed
|
||||
regs, err = client.ReadRegisters(0x1000, 2, INPUT_REGISTER)
|
||||
if err != nil {
|
||||
t.Errorf("ReadRegisters() should have succeeded, got: %v", err)
|
||||
}
|
||||
if regs[0] != 0x1234 {
|
||||
t.Errorf("expected 0x1234 in 1st reg, saw: 0x%04x", regs[0])
|
||||
}
|
||||
if regs[1] != 0x5678 {
|
||||
t.Errorf("expected 0x5678 in 2nd reg, saw: 0x%04x", regs[1])
|
||||
}
|
||||
|
||||
// attempt to read another: should succeed
|
||||
regs, err = client.ReadRegisters(0x1002, 1, HOLDING_REGISTER)
|
||||
if err != nil {
|
||||
t.Errorf("ReadRegisters() should have succeeded, got: %v", err)
|
||||
}
|
||||
if regs[0] != 0xaabb {
|
||||
t.Errorf("expected 0xaabb in 1st reg, saw: 0x%04x", regs[0])
|
||||
}
|
||||
|
||||
// close the connection: should succeed
|
||||
err = client.Close()
|
||||
if err != nil {
|
||||
t.Errorf("Close() should have succeeded, got: %v", err)
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func TestTLSClientOnServerTimeout(t *testing.T) {
|
||||
var err error
|
||||
var client *ModbusClient
|
||||
var server *ModbusServer
|
||||
var serverKeyPair tls.Certificate
|
||||
var clientKeyPair tls.Certificate
|
||||
var clientCp *x509.CertPool
|
||||
var serverCp *x509.CertPool
|
||||
var th *tlsTestHandler
|
||||
var reg uint16
|
||||
|
||||
th = &tlsTestHandler{}
|
||||
// load server and client keypairs
|
||||
serverKeyPair, err = tls.X509KeyPair([]byte(serverCert), []byte(serverKey))
|
||||
if err != nil {
|
||||
t.Errorf("failed to load test server key pair: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
clientKeyPair, err = tls.X509KeyPair([]byte(clientCert), []byte(clientKey))
|
||||
if err != nil {
|
||||
t.Errorf("failed to load test client key pair: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
// add those keypairs to their corresponding cert pool
|
||||
clientCp = x509.NewCertPool()
|
||||
if !clientCp.AppendCertsFromPEM([]byte(serverCert)) {
|
||||
t.Errorf("failed to load test server cert into cert pool")
|
||||
}
|
||||
|
||||
serverCp = x509.NewCertPool()
|
||||
if !serverCp.AppendCertsFromPEM([]byte(clientCert)) {
|
||||
t.Errorf("failed to load client cert into cert pool")
|
||||
}
|
||||
|
||||
|
||||
// load the server cert into the client CA cert pool to get the server cert
|
||||
// accepted by clients
|
||||
clientCp = x509.NewCertPool()
|
||||
if !clientCp.AppendCertsFromPEM([]byte(serverCert)) {
|
||||
t.Errorf("failed to load test server cert into cert pool")
|
||||
}
|
||||
|
||||
server, err = NewServer(&ServerConfiguration{
|
||||
URL: "tcp+tls://[::1]:5802",
|
||||
MaxClients: 10,
|
||||
TLSServerCert: &serverKeyPair,
|
||||
TLSClientCAs: serverCp,
|
||||
// disconnect idle clients after 500ms
|
||||
Timeout: 500 * time.Millisecond,
|
||||
}, th)
|
||||
if err != nil {
|
||||
t.Errorf("failed to create server: %v", err)
|
||||
}
|
||||
|
||||
err = server.Start()
|
||||
if err != nil {
|
||||
t.Errorf("failed to start server: %v", err)
|
||||
}
|
||||
|
||||
// create the modbus client
|
||||
client, err = NewClient(&ClientConfiguration{
|
||||
URL: "tcp+tls://localhost:5802",
|
||||
TLSClientCert: &clientKeyPair,
|
||||
TLSRootCAs: clientCp,
|
||||
})
|
||||
if err != nil {
|
||||
t.Errorf("failed to create client: %v", err)
|
||||
}
|
||||
|
||||
// connect to the server: should succeed
|
||||
err = client.Open()
|
||||
if err != nil {
|
||||
t.Errorf("Open() should have succeeded, got: %v", err)
|
||||
}
|
||||
|
||||
// write a value to register #3: should succeed
|
||||
err = client.WriteRegister(3, 0x0199)
|
||||
if err != nil {
|
||||
t.Errorf("Write() should have succeeded, got: %v", err)
|
||||
}
|
||||
|
||||
// attempt to read the value back: should succeed
|
||||
reg, err = client.ReadRegister(3, HOLDING_REGISTER)
|
||||
if err != nil {
|
||||
t.Errorf("ReadRegisters() should have succeeded, got: %v", err)
|
||||
}
|
||||
if reg != 0x0199 {
|
||||
t.Errorf("expected 0x0199 in reg #3, saw: 0x%04x", reg)
|
||||
}
|
||||
|
||||
// pause for longer than the server's configured timeout to end up with
|
||||
// an open client with a closed underlying TCP socket
|
||||
time.Sleep(1 * time.Second)
|
||||
|
||||
// attempt a read: should fail
|
||||
_, err = client.ReadRegister(3, INPUT_REGISTER)
|
||||
if err == nil {
|
||||
t.Errorf("ReadRegister() should have failed")
|
||||
}
|
||||
|
||||
// cleanup
|
||||
client.Close()
|
||||
server.Stop()
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// runMockTLSServer spins a test TLS server for use with TestTCPoverTLSClient.
|
||||
func runMockTLSServer(t *testing.T, serverKeyPair tls.Certificate,
|
||||
serverCp *x509.CertPool, serverChan chan string) {
|
||||
var err error
|
||||
var listener net.Listener
|
||||
var sock net.Conn
|
||||
var reqCount uint
|
||||
var clientCount uint
|
||||
var buf []byte
|
||||
|
||||
// let the OS pick an available port on the loopback interface
|
||||
listener, err = tls.Listen("tcp", "localhost:0", &tls.Config{
|
||||
// the server will use serverKeyPair (key+cert) to
|
||||
// authenticate to the client
|
||||
Certificates: []tls.Certificate{serverKeyPair},
|
||||
// the server will use the certpool to authenticate the
|
||||
// client-side cert
|
||||
ClientCAs: serverCp,
|
||||
// request client-side authentication and client cert validation
|
||||
ClientAuth: tls.RequireAndVerifyClientCert,
|
||||
})
|
||||
if err != nil {
|
||||
t.Errorf("failed to start test server listener: %v", err)
|
||||
}
|
||||
defer listener.Close()
|
||||
|
||||
// let the main test goroutine know which port the OS picked
|
||||
serverChan <- listener.Addr().String()
|
||||
|
||||
for err == nil {
|
||||
// accept client connections
|
||||
sock, err = listener.Accept()
|
||||
if err != nil {
|
||||
t.Errorf("failed to accept client conn: %v", err)
|
||||
break
|
||||
}
|
||||
|
||||
// only proceed with clients passing the tls handshake
|
||||
// note: this will reject any client whose cert does not pass the
|
||||
// verification step
|
||||
err = sock.(*tls.Conn).Handshake()
|
||||
if err != nil {
|
||||
sock.Close()
|
||||
err = nil
|
||||
continue
|
||||
}
|
||||
|
||||
clientCount++
|
||||
if clientCount > 2 {
|
||||
t.Errorf("expected 2 client conns, saw: %v", clientCount)
|
||||
}
|
||||
|
||||
// expect MBAP (modbus/tcp) messages inside the TLS tunnel
|
||||
for {
|
||||
// expect 12 bytes per request
|
||||
buf = make([]byte, 12)
|
||||
|
||||
_, err = sock.Read(buf)
|
||||
if err != nil {
|
||||
// ignore EOF errors (clients disconnecting)
|
||||
if err != io.EOF {
|
||||
t.Errorf("failed to read client request: %v", err)
|
||||
}
|
||||
sock.Close()
|
||||
break
|
||||
}
|
||||
|
||||
reqCount++
|
||||
switch reqCount {
|
||||
case 1:
|
||||
for i, b := range []byte{
|
||||
0x00, 0x01, // txn id
|
||||
0x00, 0x00, // protocol id
|
||||
0x00, 0x06, // length
|
||||
0x01, 0x04, // unit id + function code
|
||||
0x10, 0x00, // start address
|
||||
0x00, 0x02, // quantity
|
||||
} {
|
||||
if b != buf[i] {
|
||||
t.Errorf("expected 0x%02x at pos %v, saw 0x%02x",
|
||||
b, i, buf[i])
|
||||
}
|
||||
}
|
||||
|
||||
// send a reply
|
||||
_, err = sock.Write([]byte{
|
||||
0x00, 0x01, // txn id
|
||||
0x00, 0x00, // protocol id
|
||||
0x00, 0x07, // length
|
||||
0x01, 0x04, // unit id + function code
|
||||
0x04, // byte count
|
||||
0x12, 0x34, // reg #0
|
||||
0x56, 0x78, // reg #1
|
||||
})
|
||||
if err != nil {
|
||||
t.Errorf("failed to write reply: %v", err)
|
||||
}
|
||||
|
||||
case 2:
|
||||
for i, b := range []byte{
|
||||
0x00, 0x02, // txn id
|
||||
0x00, 0x00, // protocol id
|
||||
0x00, 0x06, // length
|
||||
0x01, 0x03, // unit id + function code
|
||||
0x10, 0x02, // start address
|
||||
0x00, 0x01, // quantity
|
||||
} {
|
||||
if b != buf[i] {
|
||||
t.Errorf("expected 0x%02x at pos %v, saw 0x%02x",
|
||||
b, i, buf[i])
|
||||
}
|
||||
}
|
||||
|
||||
// send a reply
|
||||
_, err = sock.Write([]byte{
|
||||
0x00, 0x02, // txn id
|
||||
0x00, 0x00, // protocol id
|
||||
0x00, 0x05, // length
|
||||
0x01, 0x03, // unit id + function code
|
||||
0x02, // byte count
|
||||
0xaa, 0xbb, // reg #0
|
||||
})
|
||||
if err != nil {
|
||||
t.Errorf("failed to write reply: %v", err)
|
||||
}
|
||||
|
||||
// stop the server after the 2nd request
|
||||
listener.Close()
|
||||
|
||||
default:
|
||||
t.Errorf("unexpected request id %v", reqCount)
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
1288
cmd/modbus-cli.go
Normal file
1288
cmd/modbus-cli.go
Normal file
File diff suppressed because it is too large
Load Diff
73
crc.go
Normal file
73
crc.go
Normal file
@@ -0,0 +1,73 @@
|
||||
package modbus
|
||||
|
||||
var crcTable [256]uint16 = [256]uint16{
|
||||
0x0000, 0xc0c1, 0xc181, 0x0140, 0xc301, 0x03c0, 0x0280, 0xc241,
|
||||
0xc601, 0x06c0, 0x0780, 0xc741, 0x0500, 0xc5c1, 0xc481, 0x0440,
|
||||
0xcc01, 0x0cc0, 0x0d80, 0xcd41, 0x0f00, 0xcfc1, 0xce81, 0x0e40,
|
||||
0x0a00, 0xcac1, 0xcb81, 0x0b40, 0xc901, 0x09c0, 0x0880, 0xc841,
|
||||
0xd801, 0x18c0, 0x1980, 0xd941, 0x1b00, 0xdbc1, 0xda81, 0x1a40,
|
||||
0x1e00, 0xdec1, 0xdf81, 0x1f40, 0xdd01, 0x1dc0, 0x1c80, 0xdc41,
|
||||
0x1400, 0xd4c1, 0xd581, 0x1540, 0xd701, 0x17c0, 0x1680, 0xd641,
|
||||
0xd201, 0x12c0, 0x1380, 0xd341, 0x1100, 0xd1c1, 0xd081, 0x1040,
|
||||
0xf001, 0x30c0, 0x3180, 0xf141, 0x3300, 0xf3c1, 0xf281, 0x3240,
|
||||
0x3600, 0xf6c1, 0xf781, 0x3740, 0xf501, 0x35c0, 0x3480, 0xf441,
|
||||
0x3c00, 0xfcc1, 0xfd81, 0x3d40, 0xff01, 0x3fc0, 0x3e80, 0xfe41,
|
||||
0xfa01, 0x3ac0, 0x3b80, 0xfb41, 0x3900, 0xf9c1, 0xf881, 0x3840,
|
||||
0x2800, 0xe8c1, 0xe981, 0x2940, 0xeb01, 0x2bc0, 0x2a80, 0xea41,
|
||||
0xee01, 0x2ec0, 0x2f80, 0xef41, 0x2d00, 0xedc1, 0xec81, 0x2c40,
|
||||
0xe401, 0x24c0, 0x2580, 0xe541, 0x2700, 0xe7c1, 0xe681, 0x2640,
|
||||
0x2200, 0xe2c1, 0xe381, 0x2340, 0xe101, 0x21c0, 0x2080, 0xe041,
|
||||
0xa001, 0x60c0, 0x6180, 0xa141, 0x6300, 0xa3c1, 0xa281, 0x6240,
|
||||
0x6600, 0xa6c1, 0xa781, 0x6740, 0xa501, 0x65c0, 0x6480, 0xa441,
|
||||
0x6c00, 0xacc1, 0xad81, 0x6d40, 0xaf01, 0x6fc0, 0x6e80, 0xae41,
|
||||
0xaa01, 0x6ac0, 0x6b80, 0xab41, 0x6900, 0xa9c1, 0xa881, 0x6840,
|
||||
0x7800, 0xb8c1, 0xb981, 0x7940, 0xbb01, 0x7bc0, 0x7a80, 0xba41,
|
||||
0xbe01, 0x7ec0, 0x7f80, 0xbf41, 0x7d00, 0xbdc1, 0xbc81, 0x7c40,
|
||||
0xb401, 0x74c0, 0x7580, 0xb541, 0x7700, 0xb7c1, 0xb681, 0x7640,
|
||||
0x7200, 0xb2c1, 0xb381, 0x7340, 0xb101, 0x71c0, 0x7080, 0xb041,
|
||||
0x5000, 0x90c1, 0x9181, 0x5140, 0x9301, 0x53c0, 0x5280, 0x9241,
|
||||
0x9601, 0x56c0, 0x5780, 0x9741, 0x5500, 0x95c1, 0x9481, 0x5440,
|
||||
0x9c01, 0x5cc0, 0x5d80, 0x9d41, 0x5f00, 0x9fc1, 0x9e81, 0x5e40,
|
||||
0x5a00, 0x9ac1, 0x9b81, 0x5b40, 0x9901, 0x59c0, 0x5880, 0x9841,
|
||||
0x8801, 0x48c0, 0x4980, 0x8941, 0x4b00, 0x8bc1, 0x8a81, 0x4a40,
|
||||
0x4e00, 0x8ec1, 0x8f81, 0x4f40, 0x8d01, 0x4dc0, 0x4c80, 0x8c41,
|
||||
0x4400, 0x84c1, 0x8581, 0x4540, 0x8701, 0x47c0, 0x4680, 0x8641,
|
||||
0x8201, 0x42c0, 0x4380, 0x8341, 0x4100, 0x81c1, 0x8081, 0x4040,
|
||||
}
|
||||
|
||||
type crc struct {
|
||||
crc uint16
|
||||
}
|
||||
|
||||
// Prepares the CRC generator for use.
|
||||
func (c *crc) init() {
|
||||
c.crc = 0xffff
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// Adds the given bytes to the CRC.
|
||||
func (c *crc) add(in []byte) {
|
||||
var index byte
|
||||
|
||||
for _, b := range in {
|
||||
index = b ^ byte(c.crc & 0xff)
|
||||
c.crc >>= 8
|
||||
c.crc ^= crcTable[index]
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// Returns the CRC as two bytes, swapped.
|
||||
func (c *crc) value() (value []byte) {
|
||||
value = uint16ToBytes(LITTLE_ENDIAN, c.crc)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func (c *crc) isEqual(low byte, high byte) (yes bool) {
|
||||
yes = (bytesToUint16(LITTLE_ENDIAN, []byte{low, high}) == c.crc)
|
||||
|
||||
return
|
||||
}
|
||||
106
crc_test.go
Normal file
106
crc_test.go
Normal file
@@ -0,0 +1,106 @@
|
||||
package modbus
|
||||
|
||||
import (
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestCRC(t *testing.T) {
|
||||
var c crc
|
||||
var out []byte
|
||||
|
||||
// initialize the CRC object and make sure we get 0xffff as init value
|
||||
c.init()
|
||||
if c.crc != 0xffff {
|
||||
t.Errorf("expected 0xffff, saw 0x%04x", c.crc)
|
||||
}
|
||||
|
||||
out = c.value()
|
||||
if len(out) != 2 {
|
||||
t.Errorf("value() should have returned 2 bytes, got %v", len(out))
|
||||
}
|
||||
if out[0] != 0xff || out[1] != 0xff {
|
||||
t.Errorf("expected {0xff, 0xff} got {0x%02x, 0x%02x}", out[0], out[1])
|
||||
}
|
||||
|
||||
// add a few bytes, check the output
|
||||
c.add([]byte{0x01, 0x02, 0x03, 0x04, 0x05})
|
||||
if c.crc != 0xbb2a {
|
||||
t.Errorf("expected 0xbb2a, saw 0x%04x", c.crc)
|
||||
}
|
||||
|
||||
out = c.value()
|
||||
if len(out) != 2 {
|
||||
t.Errorf("value() should have returned 2 bytes, got %v", len(out))
|
||||
}
|
||||
if out[0] != 0x2a || out[1] != 0xbb {
|
||||
t.Errorf("expected {0x2a, 0xbb} got {0x%02x, 0x%02x}", out[0], out[1])
|
||||
}
|
||||
|
||||
// add one extra byte, test the output again
|
||||
c.add([]byte{0x06})
|
||||
if c.crc != 0xddba {
|
||||
t.Errorf("expected 0xddba, saw 0x%04x", c.crc)
|
||||
}
|
||||
|
||||
out = c.value()
|
||||
if len(out) != 2 {
|
||||
t.Errorf("value() should have returned 2 bytes, got %v", len(out))
|
||||
}
|
||||
if out[0] != 0xba || out[1] != 0xdd {
|
||||
t.Errorf("expected {0xba, 0xdd} got {0x%02x, 0x%02x}", out[0], out[1])
|
||||
}
|
||||
|
||||
// init the CRC once again: the output bytes should be back to 0xffff
|
||||
c.init()
|
||||
if c.crc != 0xffff {
|
||||
t.Errorf("expected 0xffff, saw 0x%04x", c.crc)
|
||||
}
|
||||
|
||||
out = c.value()
|
||||
if len(out) != 2 {
|
||||
t.Errorf("value() should have returned 2 bytes, got %v", len(out))
|
||||
}
|
||||
if out[0] != 0xff || out[1] != 0xff {
|
||||
t.Errorf("expected {0xff, 0xff} got {0x%02x, 0x%02x}", out[0], out[1])
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func TestCRCIsEqual(t *testing.T) {
|
||||
var c crc
|
||||
var out []byte
|
||||
|
||||
// initialize the CRC object and feed it a few bytes
|
||||
c.init()
|
||||
c.add([]byte{0x01, 0x02, 0x03, 0x04, 0x05, 0x06})
|
||||
|
||||
// make sure the register value is what it should be
|
||||
if c.crc != 0xddba {
|
||||
t.Errorf("expected 0xddba, saw 0x%04x", c.crc)
|
||||
}
|
||||
|
||||
// positive test
|
||||
if !c.isEqual(0xba, 0xdd) {
|
||||
t.Error("isEqual() should have returned true")
|
||||
}
|
||||
|
||||
// negative test
|
||||
if c.isEqual(0xdd, 0xba) {
|
||||
t.Error("isEqual() should have returned false")
|
||||
}
|
||||
|
||||
// loopback test
|
||||
out = c.value()
|
||||
if !c.isEqual(out[0], out[1]) {
|
||||
t.Error("isEqual() should have returned true")
|
||||
}
|
||||
|
||||
// an empty payload should have a CRC of 0xffff
|
||||
c.init()
|
||||
if !c.isEqual(0xff, 0xff) {
|
||||
t.Error("isEqual() should have returned true")
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
209
encoding.go
Normal file
209
encoding.go
Normal file
@@ -0,0 +1,209 @@
|
||||
package modbus
|
||||
|
||||
import (
|
||||
"encoding/binary"
|
||||
"math"
|
||||
)
|
||||
|
||||
func uint16ToBytes(endianness Endianness, in uint16) (out []byte) {
|
||||
out = make([]byte, 2)
|
||||
switch endianness {
|
||||
case BIG_ENDIAN: binary.BigEndian.PutUint16(out, in)
|
||||
case LITTLE_ENDIAN: binary.LittleEndian.PutUint16(out, in)
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func uint16sToBytes(endianness Endianness, in []uint16) (out []byte) {
|
||||
for i := range in {
|
||||
out = append(out, uint16ToBytes(endianness, in[i])...)
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func bytesToUint16(endianness Endianness, in []byte) (out uint16) {
|
||||
switch endianness {
|
||||
case BIG_ENDIAN: out = binary.BigEndian.Uint16(in)
|
||||
case LITTLE_ENDIAN: out = binary.LittleEndian.Uint16(in)
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func bytesToUint16s(endianness Endianness, in []byte) (out []uint16) {
|
||||
for i := 0; i < len(in); i += 2 {
|
||||
out = append(out, bytesToUint16(endianness, in[i:i+2]))
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func bytesToUint32s(endianness Endianness, wordOrder WordOrder, in []byte) (out []uint32) {
|
||||
var u32 uint32
|
||||
|
||||
for i := 0; i < len(in); i += 4 {
|
||||
switch endianness {
|
||||
case BIG_ENDIAN:
|
||||
if wordOrder == HIGH_WORD_FIRST {
|
||||
u32 = binary.BigEndian.Uint32(in[i:i+4])
|
||||
} else {
|
||||
u32 = binary.BigEndian.Uint32(
|
||||
[]byte{in[i+2], in[i+3], in[i+0], in[i+1]})
|
||||
}
|
||||
case LITTLE_ENDIAN:
|
||||
if wordOrder == LOW_WORD_FIRST {
|
||||
u32 = binary.LittleEndian.Uint32(in[i:i+4])
|
||||
} else {
|
||||
u32 = binary.LittleEndian.Uint32(
|
||||
[]byte{in[i+2], in[i+3], in[i+0], in[i+1]})
|
||||
}
|
||||
}
|
||||
|
||||
out = append(out, u32)
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func uint32ToBytes(endianness Endianness, wordOrder WordOrder, in uint32) (out []byte) {
|
||||
out = make([]byte, 4)
|
||||
|
||||
switch endianness {
|
||||
case BIG_ENDIAN:
|
||||
binary.BigEndian.PutUint32(out, in)
|
||||
|
||||
// swap words if needed
|
||||
if wordOrder == LOW_WORD_FIRST {
|
||||
out[0], out[1], out[2], out[3] = out[2], out[3], out[0], out[1]
|
||||
}
|
||||
case LITTLE_ENDIAN:
|
||||
binary.LittleEndian.PutUint32(out, in)
|
||||
|
||||
// swap words if needed
|
||||
if wordOrder == HIGH_WORD_FIRST {
|
||||
out[0], out[1], out[2], out[3] = out[2], out[3], out[0], out[1]
|
||||
}
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func bytesToFloat32s(endianness Endianness, wordOrder WordOrder, in []byte) (out []float32) {
|
||||
var u32s []uint32
|
||||
|
||||
u32s = bytesToUint32s(endianness, wordOrder, in)
|
||||
|
||||
for _, u32 := range u32s {
|
||||
out = append(out, math.Float32frombits(u32))
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func float32ToBytes(endianness Endianness, wordOrder WordOrder, in float32) (out []byte) {
|
||||
out = uint32ToBytes(endianness, wordOrder, math.Float32bits(in))
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func bytesToUint64s(endianness Endianness, wordOrder WordOrder, in []byte) (out []uint64) {
|
||||
var u64 uint64
|
||||
|
||||
for i := 0; i < len(in); i += 8 {
|
||||
switch endianness {
|
||||
case BIG_ENDIAN:
|
||||
if wordOrder == HIGH_WORD_FIRST {
|
||||
u64 = binary.BigEndian.Uint64(in[i:i+8])
|
||||
} else {
|
||||
u64 = binary.BigEndian.Uint64(
|
||||
[]byte{in[i+6], in[i+7], in[i+4], in[i+5],
|
||||
in[i+2], in[i+3], in[i+0], in[i+1]})
|
||||
}
|
||||
case LITTLE_ENDIAN:
|
||||
if wordOrder == LOW_WORD_FIRST {
|
||||
u64 = binary.LittleEndian.Uint64(in[i:i+8])
|
||||
} else {
|
||||
u64 = binary.LittleEndian.Uint64(
|
||||
[]byte{in[i+6], in[i+7], in[i+4], in[i+5],
|
||||
in[i+2], in[i+3], in[i+0], in[i+1]})
|
||||
}
|
||||
}
|
||||
|
||||
out = append(out, u64)
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func uint64ToBytes(endianness Endianness, wordOrder WordOrder, in uint64) (out []byte) {
|
||||
out = make([]byte, 8)
|
||||
|
||||
switch endianness {
|
||||
case BIG_ENDIAN:
|
||||
binary.BigEndian.PutUint64(out, in)
|
||||
|
||||
// swap words if needed
|
||||
if wordOrder == LOW_WORD_FIRST {
|
||||
out[0], out[1], out[2], out[3],out[4], out[5], out[6], out[7] =
|
||||
out[6], out[7], out[4], out[5], out[2], out[3], out[0], out[1]
|
||||
}
|
||||
case LITTLE_ENDIAN:
|
||||
binary.LittleEndian.PutUint64(out, in)
|
||||
|
||||
// swap words if needed
|
||||
if wordOrder == HIGH_WORD_FIRST {
|
||||
out[0], out[1], out[2], out[3],out[4], out[5], out[6], out[7] =
|
||||
out[6], out[7], out[4], out[5], out[2], out[3], out[0], out[1]
|
||||
}
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func bytesToFloat64s(endianness Endianness, wordOrder WordOrder, in []byte) (out []float64) {
|
||||
var u64s []uint64
|
||||
|
||||
u64s = bytesToUint64s(endianness, wordOrder, in)
|
||||
|
||||
for _, u64 := range u64s {
|
||||
out = append(out, math.Float64frombits(u64))
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func float64ToBytes(endianness Endianness, wordOrder WordOrder, in float64) (out []byte) {
|
||||
out = uint64ToBytes(endianness, wordOrder, math.Float64bits(in))
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func encodeBools(in []bool) (out []byte) {
|
||||
var byteCount uint
|
||||
var i uint
|
||||
|
||||
byteCount = uint(len(in)) / 8
|
||||
if len(in) % 8 != 0 {
|
||||
byteCount++
|
||||
}
|
||||
|
||||
out = make([]byte, byteCount)
|
||||
for i = 0; i < uint(len(in)); i++ {
|
||||
if in[i] {
|
||||
out[i/8] |= (0x01 << (i % 8))
|
||||
}
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func decodeBools(quantity uint16, in []byte) (out []bool) {
|
||||
var i uint
|
||||
for i = 0; i < uint(quantity); i++ {
|
||||
out = append(out, (((in[i/8] >> (i % 8)) & 0x01) == 0x01))
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
610
encoding_test.go
Normal file
610
encoding_test.go
Normal file
@@ -0,0 +1,610 @@
|
||||
package modbus
|
||||
|
||||
import (
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestUint16ToBytes(t *testing.T) {
|
||||
var out []byte
|
||||
|
||||
out = uint16ToBytes(BIG_ENDIAN, 0x4321)
|
||||
if len(out) != 2 {
|
||||
t.Errorf("expected 2 bytes, got %v", len(out))
|
||||
}
|
||||
if out[0] != 0x43 || out[1] != 0x21 {
|
||||
t.Errorf("expected {0x43, 0x21}, got {0x%02x, 0x%02x}", out[0], out[1])
|
||||
}
|
||||
|
||||
out = uint16ToBytes(LITTLE_ENDIAN, 0x4321)
|
||||
if len(out) != 2 {
|
||||
t.Errorf("expected 2 bytes, got %v", len(out))
|
||||
}
|
||||
if out[0] != 0x21 || out[1] != 0x43 {
|
||||
t.Errorf("expected {0x21, 0x43}, got {0x%02x, 0x%02x}", out[0], out[1])
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func TestUint16sToBytes(t *testing.T) {
|
||||
var out []byte
|
||||
|
||||
out = uint16sToBytes(BIG_ENDIAN, []uint16{0x4321, 0x8765, 0xcba9})
|
||||
if len(out) != 6 {
|
||||
t.Errorf("expected 6 bytes, got %v", len(out))
|
||||
}
|
||||
if out[0] != 0x43 || out[1] != 0x21 {
|
||||
t.Errorf("expected {0x43, 0x21}, got {0x%02x, 0x%02x}", out[0], out[1])
|
||||
}
|
||||
if out[2] != 0x87 || out[3] != 0x65 {
|
||||
t.Errorf("expected {0x87, 0x65}, got {0x%02x, 0x%02x}", out[0], out[1])
|
||||
}
|
||||
if out[4] != 0xcb || out[5] != 0xa9 {
|
||||
t.Errorf("expected {0xcb, 0xa9}, got {0x%02x, 0x%02x}", out[0], out[1])
|
||||
}
|
||||
|
||||
out = uint16sToBytes(LITTLE_ENDIAN, []uint16{0x4321, 0x8765, 0xcba9})
|
||||
if len(out) != 6 {
|
||||
t.Errorf("expected 6 bytes, got %v", len(out))
|
||||
}
|
||||
if out[0] != 0x21 || out[1] != 0x43 {
|
||||
t.Errorf("expected {0x21, 0x43}, got {0x%02x, 0x%02x}", out[0], out[1])
|
||||
}
|
||||
if out[2] != 0x65 || out[3] != 0x87 {
|
||||
t.Errorf("expected {0x65, 0x87}, got {0x%02x, 0x%02x}", out[0], out[1])
|
||||
}
|
||||
if out[4] != 0xa9 || out[5] != 0xcb {
|
||||
t.Errorf("expected {0xa9, 0xcb}, got {0x%02x, 0x%02x}", out[0], out[1])
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func TestBytesToUint16(t *testing.T) {
|
||||
var result uint16
|
||||
|
||||
result = bytesToUint16(BIG_ENDIAN, []byte{0x43, 0x21})
|
||||
if result != 0x4321 {
|
||||
t.Errorf("expected 0x4321, got 0x%04x", result)
|
||||
}
|
||||
|
||||
result = bytesToUint16(LITTLE_ENDIAN, []byte{0x43, 0x21})
|
||||
if result != 0x2143 {
|
||||
t.Errorf("expected 0x2143, got 0x%04x", result)
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func TestBytesToUint16s(t *testing.T) {
|
||||
var results []uint16
|
||||
|
||||
results = bytesToUint16s(BIG_ENDIAN, []byte{0x11, 0x22, 0x33, 0x44})
|
||||
if len(results) != 2 {
|
||||
t.Errorf("expected 2 values, got %v", len(results))
|
||||
}
|
||||
if results[0] != 0x1122 {
|
||||
t.Errorf("expected 0x1122, got 0x%04x", results[0])
|
||||
}
|
||||
if results[1] != 0x3344 {
|
||||
t.Errorf("expected 0x3344, got 0x%04x", results[1])
|
||||
}
|
||||
|
||||
results = bytesToUint16s(LITTLE_ENDIAN, []byte{0x11, 0x22, 0x33, 0x44})
|
||||
if len(results) != 2 {
|
||||
t.Errorf("expected 2 values, got %v", len(results))
|
||||
}
|
||||
if results[0] != 0x2211 {
|
||||
t.Errorf("expected 0x2211, got 0x%04x", results[0])
|
||||
}
|
||||
if results[1] != 0x4433 {
|
||||
t.Errorf("expected 0x4433, got 0x%04x", results[1])
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func TestUint32ToBytes(t *testing.T) {
|
||||
var out []byte
|
||||
|
||||
out = uint32ToBytes(BIG_ENDIAN, HIGH_WORD_FIRST, 0x87654321)
|
||||
if len(out) != 4 {
|
||||
t.Errorf("expected 4 bytes, got %v", len(out))
|
||||
}
|
||||
if out[0] != 0x87 || out[1] != 0x65 || // first word
|
||||
out[2] != 0x43 || out[3] != 0x21 { // second word
|
||||
t.Errorf("expected {0x87, 0x65, 0x43, 0x21}, got {0x%02x, 0x%02x, 0x%02x, 0x%02x}",
|
||||
out[0], out[1], out[2], out[3])
|
||||
}
|
||||
|
||||
out = uint32ToBytes(BIG_ENDIAN, LOW_WORD_FIRST, 0x87654321)
|
||||
if len(out) != 4 {
|
||||
t.Errorf("expected 4 bytes, got %v", len(out))
|
||||
}
|
||||
if out[0] != 0x43 || out[1] != 0x21 || out[2] != 0x87 || out[3] != 0x65 {
|
||||
t.Errorf("expected {0x43, 0x21, 0x87, 0x65}, got {0x%02x, 0x%02x, 0x%02x, 0x%02x}",
|
||||
out[0], out[1], out[2], out[3])
|
||||
}
|
||||
|
||||
out = uint32ToBytes(LITTLE_ENDIAN, LOW_WORD_FIRST, 0x87654321)
|
||||
if len(out) != 4 {
|
||||
t.Errorf("expected 4 bytes, got %v", len(out))
|
||||
}
|
||||
if out[0] != 0x21 || out[1] != 0x43 || out[2] != 0x65 || out[3] != 0x87 {
|
||||
t.Errorf("expected {0x21, 0x43, 0x65, 0x87}, got {0x%02x, 0x%02x, 0x%02x, 0x%02x}",
|
||||
out[0], out[1], out[2], out[3])
|
||||
}
|
||||
|
||||
out = uint32ToBytes(LITTLE_ENDIAN, HIGH_WORD_FIRST, 0x87654321)
|
||||
if len(out) != 4 {
|
||||
t.Errorf("expected 4 bytes, got %v", len(out))
|
||||
}
|
||||
if out[0] != 0x65 || out[1] != 0x87 || out[2] != 0x21 || out[3] != 0x43 {
|
||||
t.Errorf("expected {0x65, 0x87, 0x21, 0x43}, got {0x%02x, 0x%02x, 0x%02x, 0x%02x}",
|
||||
out[0], out[1], out[2], out[3])
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func TestBytesToUint32s(t *testing.T) {
|
||||
var results []uint32
|
||||
|
||||
results = bytesToUint32s(BIG_ENDIAN, HIGH_WORD_FIRST, []byte{
|
||||
0x87, 0x65, 0x43, 0x21,
|
||||
0x00, 0x11, 0x22, 0x33,
|
||||
})
|
||||
if len(results) != 2 {
|
||||
t.Errorf("expected 2 values, got %v", len(results))
|
||||
}
|
||||
if results[0] != 0x87654321 {
|
||||
t.Errorf("expected 0x87654321, got 0x%08x", results[0])
|
||||
}
|
||||
if results[1] != 0x00112233 {
|
||||
t.Errorf("expected 0x00112233, got 0x%08x", results[1])
|
||||
}
|
||||
|
||||
results = bytesToUint32s(BIG_ENDIAN, LOW_WORD_FIRST, []byte{
|
||||
0x87, 0x65, 0x43, 0x21,
|
||||
0x00, 0x11, 0x22, 0x33,
|
||||
})
|
||||
if len(results) != 2 {
|
||||
t.Errorf("expected 2 values, got %v", len(results))
|
||||
}
|
||||
if results[0] != 0x43218765 {
|
||||
t.Errorf("expected 0x43218765, got 0x%08x", results[0])
|
||||
}
|
||||
if results[1] != 0x22330011 {
|
||||
t.Errorf("expected 0x22330011, got 0x%08x", results[1])
|
||||
}
|
||||
|
||||
results = bytesToUint32s(LITTLE_ENDIAN, LOW_WORD_FIRST, []byte{
|
||||
0x87, 0x65, 0x43, 0x21,
|
||||
0x00, 0x11, 0x22, 0x33,
|
||||
})
|
||||
if len(results) != 2 {
|
||||
t.Errorf("expected 2 values, got %v", len(results))
|
||||
}
|
||||
if results[0] != 0x21436587 {
|
||||
t.Errorf("expected 0x21436587, got 0x%08x", results[0])
|
||||
}
|
||||
if results[1] != 0x33221100 {
|
||||
t.Errorf("expected 0x33221100, got 0x%08x", results[1])
|
||||
}
|
||||
|
||||
results = bytesToUint32s(LITTLE_ENDIAN, HIGH_WORD_FIRST, []byte{
|
||||
0x87, 0x65, 0x43, 0x21,
|
||||
0x00, 0x11, 0x22, 0x33,
|
||||
})
|
||||
if len(results) != 2 {
|
||||
t.Errorf("expected 2 values, got %v", len(results))
|
||||
}
|
||||
if results[0] != 0x65872143 {
|
||||
t.Errorf("expected 0x65872143, got 0x%08x", results[0])
|
||||
}
|
||||
if results[1] != 0x11003322 {
|
||||
t.Errorf("expected 0x11003322, got 0x%08x", results[1])
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func TestFloat32ToBytes(t *testing.T) {
|
||||
var out []byte
|
||||
|
||||
out = float32ToBytes(BIG_ENDIAN, HIGH_WORD_FIRST, 1.234)
|
||||
if len(out) != 4 {
|
||||
t.Errorf("expected 4 bytes, got %v", len(out))
|
||||
}
|
||||
if out[0] != 0x3f || out[1] != 0x9d || out[2] != 0xf3 || out[3] != 0xb6 {
|
||||
t.Errorf("expected {0x3f, 0x9d, 0xf3, 0xb6}, got {0x%02x, 0x%02x, 0x%02x, 0x%02x}",
|
||||
out[0], out[1], out[2], out[3])
|
||||
}
|
||||
|
||||
out = float32ToBytes(BIG_ENDIAN, LOW_WORD_FIRST, 1.234)
|
||||
if len(out) != 4 {
|
||||
t.Errorf("expected 4 bytes, got %v", len(out))
|
||||
}
|
||||
if out[0] != 0xf3 || out[1] != 0xb6 || out[2] != 0x3f || out[3] != 0x9d {
|
||||
t.Errorf("expected {0xf3, 0xb6, 0x3f, 0x9d}, got {0x%02x, 0x%02x, 0x%02x, 0x%02x}",
|
||||
out[0], out[1], out[2], out[3])
|
||||
}
|
||||
|
||||
out = float32ToBytes(LITTLE_ENDIAN, LOW_WORD_FIRST, 1.234)
|
||||
if len(out) != 4 {
|
||||
t.Errorf("expected 4 bytes, got %v", len(out))
|
||||
}
|
||||
if out[0] != 0xb6 || out[1] != 0xf3 || out[2] != 0x9d || out[3] != 0x3f {
|
||||
t.Errorf("expected {0xb6, 0xf3, 0x9d, 0x3f}, got {0x%02x, 0x%02x, 0x%02x, 0x%02x}",
|
||||
out[0], out[1], out[2], out[3])
|
||||
}
|
||||
|
||||
out = float32ToBytes(LITTLE_ENDIAN, HIGH_WORD_FIRST, 1.234)
|
||||
if len(out) != 4 {
|
||||
t.Errorf("expected 4 bytes, got %v", len(out))
|
||||
}
|
||||
if out[0] != 0x9d || out[1] != 0x3f || out[2] != 0xb6 || out[3] != 0xf3 {
|
||||
t.Errorf("expected {0x9d, 0x3f, 0xb6, 0xf3}, got {0x%02x, 0x%02x, 0x%02x, 0x%02x}",
|
||||
out[0], out[1], out[2], out[3])
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func TestBytesToFloat32s(t *testing.T) {
|
||||
var results []float32
|
||||
|
||||
results = bytesToFloat32s(BIG_ENDIAN, HIGH_WORD_FIRST, []byte{
|
||||
0x3f, 0x9d, 0xf3, 0xb6,
|
||||
0x40, 0x49, 0x0f, 0xdb,
|
||||
})
|
||||
if len(results) != 2 {
|
||||
t.Errorf("expected 2 values, got %v", len(results))
|
||||
}
|
||||
if results[0] != 1.234 {
|
||||
t.Errorf("expected 1.234, got %.04f", results[0])
|
||||
}
|
||||
if results[1] != 3.14159274101 {
|
||||
t.Errorf("expected 3.14159274101, got %.09f", results[1])
|
||||
}
|
||||
|
||||
results = bytesToFloat32s(BIG_ENDIAN, LOW_WORD_FIRST, []byte{
|
||||
0xf3, 0xb6, 0x3f, 0x9d,
|
||||
0x0f, 0xdb, 0x40, 0x49,
|
||||
})
|
||||
if len(results) != 2 {
|
||||
t.Errorf("expected 2 values, got %v", len(results))
|
||||
}
|
||||
if results[0] != 1.234 {
|
||||
t.Errorf("expected 1.234, got %.04f", results[0])
|
||||
}
|
||||
if results[1] != 3.14159274101 {
|
||||
t.Errorf("expected 3.14159274101, got %.09f", results[1])
|
||||
}
|
||||
|
||||
results = bytesToFloat32s(LITTLE_ENDIAN, LOW_WORD_FIRST, []byte{
|
||||
0xb6, 0xf3, 0x9d, 0x3f,
|
||||
0xdb, 0x0f, 0x49, 0x40,
|
||||
})
|
||||
if len(results) != 2 {
|
||||
t.Errorf("expected 2 values, got %v", len(results))
|
||||
}
|
||||
if results[0] != 1.234 {
|
||||
t.Errorf("expected 1.234, got %.04f", results[0])
|
||||
}
|
||||
if results[1] != 3.14159274101 {
|
||||
t.Errorf("expected 3.14159274101, got %.09f", results[1])
|
||||
}
|
||||
|
||||
results = bytesToFloat32s(LITTLE_ENDIAN, HIGH_WORD_FIRST, []byte{
|
||||
0x9d, 0x3f, 0xb6, 0xf3,
|
||||
0x49, 0x40, 0xdb, 0x0f,
|
||||
})
|
||||
if len(results) != 2 {
|
||||
t.Errorf("expected 2 values, got %v", len(results))
|
||||
}
|
||||
if results[0] != 1.234 {
|
||||
t.Errorf("expected 1.234, got %.04f", results[0])
|
||||
}
|
||||
if results[1] != 3.14159274101 {
|
||||
t.Errorf("expected 3.14159274101, got %.09f", results[1])
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func TestUint64ToBytes(t *testing.T) {
|
||||
var out []byte
|
||||
|
||||
out = uint64ToBytes(BIG_ENDIAN, HIGH_WORD_FIRST, 0x0fedcba987654321)
|
||||
if len(out) != 8 {
|
||||
t.Errorf("expected 8 bytes, got %v", len(out))
|
||||
}
|
||||
|
||||
if out[0] != 0x0f || out[1] != 0xed || // 1st word
|
||||
out[2] != 0xcb || out[3] != 0xa9 || // 2nd word
|
||||
out[4] != 0x87 || out[5] != 0x65 || // 3rd word
|
||||
out[6] != 0x43 || out[7] != 0x21 { // 4th word
|
||||
t.Errorf("expected {0x0f, 0xed, 0xcb, 0xa9, 0x87, 0x65, 0x43, 0x21}, got {0x%02x, 0x%02x, 0x%02x, 0x%02x, 0x%02x, 0x%02x, 0x%02x, 0x%02x}",
|
||||
out[0], out[1], out[2], out[3], out[4], out[5], out[6], out[7])
|
||||
}
|
||||
|
||||
out = uint64ToBytes(BIG_ENDIAN, LOW_WORD_FIRST, 0x0fedcba987654321)
|
||||
if len(out) != 8 {
|
||||
t.Errorf("expected 8 bytes, got %v", len(out))
|
||||
}
|
||||
if out[0] != 0x43 || out[1] != 0x21 || // 1st word
|
||||
out[2] != 0x87 || out[3] != 0x65 || // 2nd word
|
||||
out[4] != 0xcb || out[5] != 0xa9 || // 3rd word
|
||||
out[6] != 0x0f || out[7] != 0xed { // 4th word
|
||||
t.Errorf("expected {0x43, 0x21, 0x87, 0x65, 0xcb, 0xa9, 0x0f, 0xed}, got {0x%02x, 0x%02x, 0x%02x, 0x%02x, 0x%02x, 0x%02x, 0x%02x, 0x%02x}",
|
||||
out[0], out[1], out[2], out[3], out[4], out[5], out[6], out[7])
|
||||
}
|
||||
|
||||
out = uint64ToBytes(LITTLE_ENDIAN, LOW_WORD_FIRST, 0x0fedcba987654321)
|
||||
if len(out) != 8 {
|
||||
t.Errorf("expected 8 bytes, got %v", len(out))
|
||||
}
|
||||
if out[0] != 0x21 || out[1] != 0x43 || // 1st word
|
||||
out[2] != 0x65 || out[3] != 0x87 || // 2nd word
|
||||
out[4] != 0xa9 || out[5] != 0xcb || // 3rd word
|
||||
out[6] != 0xed || out[7] != 0x0f { // 4th word
|
||||
t.Errorf("expected {0x21, 0x43, 0x65, 0x87, 0xa9, 0xcb, 0xed, 0x0f}, got {0x%02x, 0x%02x, 0x%02x, 0x%02x, 0x%02x, 0x%02x, 0x%02x, 0x%02x}",
|
||||
out[0], out[1], out[2], out[3], out[4], out[5], out[6], out[7])
|
||||
}
|
||||
|
||||
out = uint64ToBytes(LITTLE_ENDIAN, HIGH_WORD_FIRST, 0x0fedcba987654321)
|
||||
if len(out) != 8 {
|
||||
t.Errorf("expected 8 bytes, got %v", len(out))
|
||||
}
|
||||
if out[0] != 0xed || out[1] != 0x0f || // 1st word
|
||||
out[2] != 0xa9 || out[3] != 0xcb || // 2nd word
|
||||
out[4] != 0x65 || out[5] != 0x87 || // 3rd word
|
||||
out[6] != 0x21 || out[7] != 0x43 { // 4th word
|
||||
t.Errorf("expected {0xed, 0x0f, 0xa9, 0xcb, 0x65, 0x87, 0x21, 0x43}, got {0x%02x, 0x%02x, 0x%02x, 0x%02x, 0x%02x, 0x%02x, 0x%02x, 0x%02x}",
|
||||
out[0], out[1], out[2], out[3], out[4], out[5], out[6], out[7])
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func TestBytesToUint64s(t *testing.T) {
|
||||
var results []uint64
|
||||
|
||||
results = bytesToUint64s(BIG_ENDIAN, HIGH_WORD_FIRST, []byte{
|
||||
0x0f, 0xed, 0xcb, 0xa9, 0x87, 0x65, 0x43, 0x21,
|
||||
0x00, 0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77,
|
||||
})
|
||||
if len(results) != 2 {
|
||||
t.Errorf("expected 2 values, got %v", len(results))
|
||||
}
|
||||
if results[0] != 0x0fedcba987654321 {
|
||||
t.Errorf("expected 0x0fedcba987654321, got 0x%016x", results[0])
|
||||
}
|
||||
if results[1] != 0x0011223344556677 {
|
||||
t.Errorf("expected 0x0011223344556677, got 0x%016x", results[1])
|
||||
}
|
||||
|
||||
results = bytesToUint64s(BIG_ENDIAN, LOW_WORD_FIRST, []byte{
|
||||
0x0f, 0xed, 0xcb, 0xa9, 0x87, 0x65, 0x43, 0x21,
|
||||
0x00, 0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77,
|
||||
})
|
||||
if len(results) != 2 {
|
||||
t.Errorf("expected 2 values, got %v", len(results))
|
||||
}
|
||||
|
||||
if results[0] != 0x43218765cba90fed {
|
||||
t.Errorf("expected 0x43218765cba90fed, got 0x%016x", results[0])
|
||||
}
|
||||
|
||||
if results[1] != 0x6677445522330011 {
|
||||
t.Errorf("expected 0x6677445522330011, got 0x%016x", results[1])
|
||||
}
|
||||
|
||||
results = bytesToUint64s(LITTLE_ENDIAN, LOW_WORD_FIRST, []byte{
|
||||
0x0f, 0xed, 0xcb, 0xa9, 0x87, 0x65, 0x43, 0x21,
|
||||
0x00, 0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77,
|
||||
})
|
||||
if len(results) != 2 {
|
||||
t.Errorf("expected 2 values, got %v", len(results))
|
||||
}
|
||||
if results[0] != 0x21436587a9cbed0f {
|
||||
t.Errorf("expected 0x21436587a9cbed0f, got 0x%016x", results[0])
|
||||
}
|
||||
if results[1] != 0x7766554433221100 {
|
||||
t.Errorf("expected 0x7766554433221100, got 0x%016x", results[1])
|
||||
}
|
||||
|
||||
results = bytesToUint64s(LITTLE_ENDIAN, HIGH_WORD_FIRST, []byte{
|
||||
0x0f, 0xed, 0xcb, 0xa9, 0x87, 0x65, 0x43, 0x21,
|
||||
0x00, 0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77,
|
||||
})
|
||||
if len(results) != 2 {
|
||||
t.Errorf("expected 2 values, got %v", len(results))
|
||||
}
|
||||
if results[0] != 0xed0fa9cb65872143 {
|
||||
t.Errorf("expected 0xed0fa9cb65872143, got 0x%016x", results[0])
|
||||
}
|
||||
if results[1] != 0x1100332255447766 {
|
||||
t.Errorf("expected 0x1100332255447766, got 0x%016x", results[1])
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func TestFloat64ToBytes(t *testing.T) {
|
||||
var out []byte
|
||||
|
||||
out = float64ToBytes(BIG_ENDIAN, HIGH_WORD_FIRST, 1.2345678)
|
||||
if len(out) != 8 {
|
||||
t.Errorf("expected 8 bytes, got %v", len(out))
|
||||
}
|
||||
if out[0] != 0x3f || out[1] != 0xf3 || out[2] != 0xc0 || out[3] != 0xca ||
|
||||
out[4] != 0x2a || out[5] != 0x5b || out[6] != 0x1d || out[7] != 0x5d {
|
||||
t.Errorf("expected {0x3f, 0xf3, 0xc0, 0xca, 0x2a, 0x5b, 0x1d, 0x5d}, got {0x%02x, 0x%02x, 0x%02x, 0x%02x, 0x%02x, 0x%02x, 0x%02x, 0x%02x}",
|
||||
out[0], out[1], out[2], out[3], out[4], out[5], out[6], out[7])
|
||||
}
|
||||
|
||||
out = float64ToBytes(BIG_ENDIAN, LOW_WORD_FIRST, 1.2345678)
|
||||
if len(out) != 8 {
|
||||
t.Errorf("expected 8 bytes, got %v", len(out))
|
||||
}
|
||||
if out[0] != 0x1d || out[1] != 0x5d || out[2] != 0x2a || out[3] != 0x5b ||
|
||||
out[4] != 0xc0 || out[5] != 0xca || out[6] != 0x3f || out[7] != 0xf3 {
|
||||
t.Errorf("expected {0x1d, 0x5d, 0x2a, 0x5b, 0xc0, 0xca, 0x3f, 0xf3}, got {0x%02x, 0x%02x, 0x%02x, 0x%02x, 0x%02x, 0x%02x, 0x%02x, 0x%02x}",
|
||||
out[0], out[1], out[2], out[3], out[4], out[5], out[6], out[7])
|
||||
}
|
||||
|
||||
out = float64ToBytes(LITTLE_ENDIAN, LOW_WORD_FIRST, 1.2345678)
|
||||
if len(out) != 8 {
|
||||
t.Errorf("expected 8 bytes, got %v", len(out))
|
||||
}
|
||||
|
||||
if out[0] != 0x5d || out[1] != 0x1d || out[2] != 0x5b || out[3] != 0x2a ||
|
||||
out[4] != 0xca || out[5] != 0xc0 || out[6] != 0xf3 || out[7] != 0x3f {
|
||||
t.Errorf("expected {0x5d, 0x1d, 0x5b, 0x2a, 0xca, 0xc0, 0xf3, 0x3f}, got {0x%02x, 0x%02x, 0x%02x, 0x%02x, 0x%02x, 0x%02x, 0x%02x, 0x%02x}",
|
||||
out[0], out[1], out[2], out[3], out[4], out[5], out[6], out[7])
|
||||
}
|
||||
|
||||
out = float64ToBytes(LITTLE_ENDIAN, HIGH_WORD_FIRST, 1.2345678)
|
||||
if len(out) != 8 {
|
||||
t.Errorf("expected 8 bytes, got %v", len(out))
|
||||
}
|
||||
if out[0] != 0xf3 || out[1] != 0x3f || out[2] != 0xca || out[3] != 0xc0 ||
|
||||
out[4] != 0x5b || out[5] != 0x2a || out[6] != 0x5d || out[7] != 0x1d {
|
||||
t.Errorf("expected {0xf3, 0x3f, 0xca, 0xc0, 0x5b, 0x2a, 0x5d, 0x1d}, got {0x%02x, 0x%02x, 0x%02x, 0x%02x, 0x%02x, 0x%02x, 0x%02x, 0x%02x}",
|
||||
out[0], out[1], out[2], out[3], out[4], out[5], out[6], out[7])
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func TestBytesToFloat64s(t *testing.T) {
|
||||
var results []float64
|
||||
|
||||
results = bytesToFloat64s(BIG_ENDIAN, HIGH_WORD_FIRST, []byte{
|
||||
0x3f, 0xf3, 0xc0, 0xca, 0x2a, 0x5b, 0x1d, 0x5d,
|
||||
0x40, 0x09, 0x21, 0xfb, 0x5f, 0xff, 0xe9, 0x5e,
|
||||
})
|
||||
if len(results) != 2 {
|
||||
t.Errorf("expected 2 values, got %v", len(results))
|
||||
}
|
||||
if results[0] != 1.2345678 {
|
||||
t.Errorf("expected 1.2345678, got %.08f", results[0])
|
||||
}
|
||||
if results[1] != 3.14159274101 {
|
||||
t.Errorf("expected 3.14159274101, got %.09f", results[1])
|
||||
}
|
||||
|
||||
results = bytesToFloat64s(BIG_ENDIAN, LOW_WORD_FIRST, []byte{
|
||||
0x1d, 0x5d, 0x2a, 0x5b, 0xc0, 0xca, 0x3f, 0xf3,
|
||||
0xe9, 0x5e, 0x5f, 0xff, 0x21, 0xfb, 0x40, 0x09,
|
||||
})
|
||||
if len(results) != 2 {
|
||||
t.Errorf("expected 2 values, got %v", len(results))
|
||||
}
|
||||
if results[0] != 1.2345678 {
|
||||
t.Errorf("expected 1.234, got %.08f", results[0])
|
||||
}
|
||||
if results[1] != 3.14159274101 {
|
||||
t.Errorf("expected 3.14159274101, got %.09f", results[1])
|
||||
}
|
||||
|
||||
results = bytesToFloat64s(LITTLE_ENDIAN, LOW_WORD_FIRST, []byte{
|
||||
0x5d, 0x1d, 0x5b, 0x2a, 0xca, 0xc0, 0xf3, 0x3f,
|
||||
0x5e, 0xe9, 0xff, 0x5f, 0xfb, 0x21, 0x09, 0x40,
|
||||
})
|
||||
if len(results) != 2 {
|
||||
t.Errorf("expected 2 values, got %v", len(results))
|
||||
}
|
||||
if results[0] != 1.2345678 {
|
||||
t.Errorf("expected 1.234, got %.08f", results[0])
|
||||
}
|
||||
if results[1] != 3.14159274101 {
|
||||
t.Errorf("expected 3.14159274101, got %.09f", results[1])
|
||||
}
|
||||
|
||||
results = bytesToFloat64s(LITTLE_ENDIAN, HIGH_WORD_FIRST, []byte{
|
||||
0xf3, 0x3f, 0xca, 0xc0, 0x5b, 0x2a, 0x5d, 0x1d,
|
||||
0x09, 0x40, 0xfb, 0x21, 0xff, 0x5f, 0x5e, 0xe9,
|
||||
})
|
||||
if len(results) != 2 {
|
||||
t.Errorf("expected 2 values, got %v", len(results))
|
||||
}
|
||||
if results[0] != 1.2345678 {
|
||||
t.Errorf("expected 1.234, got %.08f", results[0])
|
||||
}
|
||||
if results[1] != 3.14159274101 {
|
||||
t.Errorf("expected 3.14159274101, got %.09f", results[1])
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func TestDecodeBools(t *testing.T) {
|
||||
var results []bool
|
||||
|
||||
results = decodeBools(1, []byte{0x01})
|
||||
if len(results) != 1 {
|
||||
t.Errorf("expected 1 value, got %v", len(results))
|
||||
}
|
||||
if results[0] != true {
|
||||
t.Errorf("expected true, got false")
|
||||
}
|
||||
|
||||
results = decodeBools(1, []byte{0x0f})
|
||||
if len(results) != 1 {
|
||||
t.Errorf("expected 1 value, got %v", len(results))
|
||||
}
|
||||
if results[0] != true {
|
||||
t.Errorf("expected true, got false")
|
||||
}
|
||||
|
||||
results = decodeBools(9, []byte{0x75, 0x03})
|
||||
if len(results) != 9 {
|
||||
t.Errorf("expected 9 values, got %v", len(results))
|
||||
}
|
||||
for i, b := range []bool{
|
||||
true, false, true, false, // 0x05
|
||||
true, true, true, false, // 0x07
|
||||
true, } { // 0x01
|
||||
if b != results[i] {
|
||||
t.Errorf("expected %v at %v, got %v", b, i, results[i])
|
||||
}
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func TestEncodeBools(t *testing.T) {
|
||||
var results []byte
|
||||
|
||||
results = encodeBools([]bool{false, true, false, true, })
|
||||
if len(results) != 1 {
|
||||
t.Errorf("expected 1 byte, got %v", len(results))
|
||||
}
|
||||
if results[0] != 0x0a {
|
||||
t.Errorf("expected 0x0a, got 0x%02x", results[0])
|
||||
}
|
||||
|
||||
results = encodeBools([]bool{true, false, true, })
|
||||
if len(results) != 1 {
|
||||
t.Errorf("expected 1 byte, got %v", len(results))
|
||||
}
|
||||
if results[0] != 0x05 {
|
||||
t.Errorf("expected 0x05, got 0x%02x", results[0])
|
||||
}
|
||||
|
||||
results = encodeBools([]bool{true, false, false, true, false, true, true, false,
|
||||
true, true, true, false, true, true, true, false,
|
||||
false, true})
|
||||
if len(results) != 3 {
|
||||
t.Errorf("expected 3 bytes, got %v", len(results))
|
||||
}
|
||||
if results[0] != 0x69 || results[1] != 0x77 || results[2] != 0x02 {
|
||||
t.Errorf("expected {0x69, 0x77, 0x02}, got {0x%02x, 0x%02x, 0x%02x}",
|
||||
results[0], results[1], results[2])
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
340
examples/tcp_server.go
Normal file
340
examples/tcp_server.go
Normal file
@@ -0,0 +1,340 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"math"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/simonvetter/modbus"
|
||||
)
|
||||
|
||||
const (
|
||||
MINUS_ONE int16 = -1
|
||||
)
|
||||
|
||||
/*
|
||||
* Simple modbus server example.
|
||||
*
|
||||
* This file is intended to be a demo of the modbus server.
|
||||
* It shows how to create and start a server, as well as how
|
||||
* to write a handler object.
|
||||
* Feel free to use it as boilerplate for simple servers.
|
||||
*/
|
||||
|
||||
// run this with go run examples/tcp_server.go
|
||||
func main() {
|
||||
var server *modbus.ModbusServer
|
||||
var err error
|
||||
var eh *exampleHandler
|
||||
var ticker *time.Ticker
|
||||
|
||||
// create the handler object
|
||||
eh = &exampleHandler{}
|
||||
|
||||
// create the server object
|
||||
server, err = modbus.NewServer(&modbus.ServerConfiguration{
|
||||
// listen on localhost port 5502
|
||||
URL: "tcp://localhost:5502",
|
||||
// close idle connections after 30s of inactivity
|
||||
Timeout: 30 * time.Second,
|
||||
// accept 5 concurrent connections max.
|
||||
MaxClients: 5,
|
||||
}, eh)
|
||||
if err != nil {
|
||||
fmt.Printf("failed to create server: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
// start accepting client connections
|
||||
// note that Start() returns as soon as the server is started
|
||||
err = server.Start()
|
||||
if err != nil {
|
||||
fmt.Printf("failed to start server: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
// increment a 32-bit uptime counter every second.
|
||||
// (this counter is exposed as input registers 200-201 for demo purposes)
|
||||
ticker = time.NewTicker(1 * time.Second)
|
||||
for {
|
||||
<-ticker.C
|
||||
|
||||
// since the handler methods are called from multiple goroutines,
|
||||
// use locking where appropriate to avoid concurrency issues.
|
||||
eh.lock.Lock()
|
||||
eh.uptime++
|
||||
eh.lock.Unlock()
|
||||
}
|
||||
|
||||
// never reached
|
||||
return
|
||||
}
|
||||
|
||||
// Example handler object, passed to the NewServer() constructor above.
|
||||
type exampleHandler struct {
|
||||
// this lock is used to avoid concurrency issues between goroutines, as
|
||||
// handler methods are called from different goroutines
|
||||
// (1 goroutine per client)
|
||||
lock sync.RWMutex
|
||||
|
||||
// simple uptime counter, incremented in the main() above and exposed
|
||||
// as a 32-bit input register (2 consecutive 16-bit modbus registers).
|
||||
uptime uint32
|
||||
|
||||
// these are here to hold client-provided (written) values, for both coils and
|
||||
// holding registers
|
||||
coils [100]bool
|
||||
holdingReg1 uint16
|
||||
holdingReg2 uint16
|
||||
|
||||
// this is a 16-bit signed integer
|
||||
holdingReg3 int16
|
||||
|
||||
// this is a 32-bit unsigned integer
|
||||
holdingReg4 uint32
|
||||
}
|
||||
|
||||
// Coil handler method.
|
||||
// This method gets called whenever a valid modbus request asking for a coil operation is
|
||||
// received by the server.
|
||||
// It exposes 100 read/writable coils at addresses 0-99, except address 80 which is
|
||||
// read-only.
|
||||
// (read them with ./modbus-cli --target tcp://localhost:5502 rc:0+99, write to register n
|
||||
// with ./modbus-cli --target tcp://localhost:5502 wr:n:<true|false>)
|
||||
func (eh *exampleHandler) HandleCoils(req *modbus.CoilsRequest) (res []bool, err error) {
|
||||
if req.UnitId != 1 {
|
||||
// only accept unit ID #1
|
||||
// note: we're merely filtering here, but we could as well use the unit
|
||||
// ID field to support multiple register maps in a single server.
|
||||
err = modbus.ErrIllegalFunction
|
||||
return
|
||||
}
|
||||
|
||||
// make sure that all registers covered by this request actually exist
|
||||
if int(req.Addr) + int(req.Quantity) > len(eh.coils) {
|
||||
err = modbus.ErrIllegalDataAddress
|
||||
return
|
||||
}
|
||||
|
||||
// since we're manipulating variables shared between multiple goroutines,
|
||||
// acquire a lock to avoid concurrency issues.
|
||||
eh.lock.Lock()
|
||||
// release the lock upon return
|
||||
defer eh.lock.Unlock()
|
||||
|
||||
// loop through `req.Quantity` registers, from address `req.Addr` to
|
||||
// `req.Addr + req.Quantity - 1`, which here is conveniently `req.Addr + i`
|
||||
for i := 0; i < int(req.Quantity); i++ {
|
||||
// ignore the write if the current register address is 80
|
||||
if req.IsWrite && int(req.Addr) + i != 80 {
|
||||
// assign the value
|
||||
eh.coils[int(req.Addr) + i] = req.Args[i]
|
||||
}
|
||||
// append the value of the requested register to res so they can be
|
||||
// sent back to the client
|
||||
res = append(res, eh.coils[int(req.Addr) + i])
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// Discrete input handler method.
|
||||
// Note that we're returning ErrIllegalFunction unconditionally.
|
||||
// This will cause the client to receive "illegal function", which is the modbus way of
|
||||
// reporting that this server does not support/implement the discrete input type.
|
||||
func (eh *exampleHandler) HandleDiscreteInputs(req *modbus.DiscreteInputsRequest) (res []bool, err error) {
|
||||
// this is the equivalent of saying
|
||||
// "discrete inputs are not supported by this device"
|
||||
// (try it with modbus-cli --target tcp://localhost:5502 rdi:1)
|
||||
err = modbus.ErrIllegalFunction
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// Holding register handler method.
|
||||
// This method gets called whenever a valid modbus request asking for a holding register
|
||||
// operation (either read or write) received by the server.
|
||||
func (eh *exampleHandler) HandleHoldingRegisters(req *modbus.HoldingRegistersRequest) (res []uint16, err error) {
|
||||
var regAddr uint16
|
||||
|
||||
if req.UnitId != 1 {
|
||||
// only accept unit ID #1
|
||||
err = modbus.ErrIllegalFunction
|
||||
return
|
||||
}
|
||||
|
||||
// since we're manipulating variables shared between multiple goroutines,
|
||||
// acquire a lock to avoid concurrency issues.
|
||||
eh.lock.Lock()
|
||||
// release the lock upon return
|
||||
defer eh.lock.Unlock()
|
||||
|
||||
// loop through `quantity` registers
|
||||
for i := 0; i < int(req.Quantity); i++ {
|
||||
// compute the target register address
|
||||
regAddr = req.Addr + uint16(i)
|
||||
|
||||
switch regAddr {
|
||||
// expose the static, read-only value of 0xff00 in register 100
|
||||
case 100:
|
||||
res = append(res, 0xff00)
|
||||
|
||||
// expose holdingReg1 in register 101 (RW)
|
||||
case 101:
|
||||
if req.IsWrite {
|
||||
eh.holdingReg1 = req.Args[i]
|
||||
}
|
||||
res = append(res, eh.holdingReg1)
|
||||
|
||||
// expose holdingReg2 in register 102 (RW)
|
||||
case 102:
|
||||
if req.IsWrite {
|
||||
// only accept values 2 and 4
|
||||
switch req.Args[i] {
|
||||
case 2, 4:
|
||||
eh.holdingReg2 = req.Args[i]
|
||||
|
||||
// make note of the change (e.g. for auditing purposes)
|
||||
fmt.Printf("%s set reg#102 to %v\n", req.ClientAddr, eh.holdingReg2)
|
||||
default:
|
||||
// if the written value is neither 2 nor 4,
|
||||
// return a modbus "illegal data value" to
|
||||
// let the client know that the value is
|
||||
// not acceptable.
|
||||
err = modbus.ErrIllegalDataValue
|
||||
return
|
||||
}
|
||||
}
|
||||
res = append(res, eh.holdingReg2)
|
||||
|
||||
// expose eh.holdingReg3 in register 103 (RW)
|
||||
// note: eh.holdingReg3 is a signed 16-bit integer
|
||||
case 103:
|
||||
if req.IsWrite {
|
||||
// cast the 16-bit unsigned integer passed by the server
|
||||
// to a 16-bit signed integer when writing
|
||||
eh.holdingReg3 = int16(req.Args[i])
|
||||
}
|
||||
// cast the 16-bit signed integer from the handler to a 16-bit unsigned
|
||||
// integer so that we can append it to `res`.
|
||||
res = append(res, uint16(eh.holdingReg3))
|
||||
|
||||
|
||||
// expose the 16 most-significant bits of eh.holdingReg4 in register 200
|
||||
case 200:
|
||||
if req.IsWrite {
|
||||
eh.holdingReg4 =
|
||||
((uint32(req.Args[i]) << 16) & 0xffff0000 |
|
||||
(eh.holdingReg4 & 0x0000ffff))
|
||||
}
|
||||
res = append(res, uint16((eh.holdingReg4 >> 16) & 0x0000ffff))
|
||||
|
||||
// expose the 16 least-significant bits of eh.holdingReg4 in register 201
|
||||
case 201:
|
||||
if req.IsWrite {
|
||||
eh.holdingReg4 =
|
||||
(uint32(req.Args[i]) & 0x0000ffff |
|
||||
(eh.holdingReg4 & 0xffff0000))
|
||||
}
|
||||
res = append(res, uint16(eh.holdingReg4 & 0x0000ffff))
|
||||
|
||||
|
||||
// any other address is unknown
|
||||
default:
|
||||
err = modbus.ErrIllegalDataAddress
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// Input register handler method.
|
||||
// This method gets called whenever a valid modbus request asking for an input register
|
||||
// operation is received by the server.
|
||||
// Note that input registers are always read-only as per the modbus spec.
|
||||
func (eh *exampleHandler) HandleInputRegisters(req *modbus.InputRegistersRequest) (res []uint16, err error) {
|
||||
var unixTs_s uint32
|
||||
var minusOne int16 = -1
|
||||
|
||||
if req.UnitId != 1 {
|
||||
// only accept unit ID #1
|
||||
err = modbus.ErrIllegalFunction
|
||||
return
|
||||
}
|
||||
|
||||
// get the current unix timestamp, converted as a 32-bit unsigned integer for
|
||||
// simplicity
|
||||
unixTs_s = uint32(time.Now().Unix() & 0xffffffff)
|
||||
|
||||
// loop through all register addresses from req.addr to req.addr + req.Quantity - 1
|
||||
for regAddr := req.Addr; regAddr < req.Addr + req.Quantity; regAddr++ {
|
||||
switch regAddr {
|
||||
case 100:
|
||||
// return the static value 0x1111 at address 100, as an unsigned
|
||||
// 16-bit integer
|
||||
// (read it with modbus-cli --target tcp://localhost:5502 ri:uint16:100)
|
||||
res = append(res, 0x1111)
|
||||
|
||||
case 101:
|
||||
// return the static value -1 at address 101, as a signed 16-bit
|
||||
// integer
|
||||
// (read it with modbus-cli --target tcp://localhost:5502 ri:int16:101)
|
||||
res = append(res, uint16(minusOne))
|
||||
|
||||
|
||||
// expose our uptime counter, encoded as a 32-bit unsigned integer in
|
||||
// input registers 200-201
|
||||
// (read it with modbus-cli --target tcp://localhost:5502 ri:uint32:200)
|
||||
case 200:
|
||||
// return the 16 most significant bits of the uptime counter
|
||||
// (using locking to avoid concurrency issues)
|
||||
eh.lock.RLock()
|
||||
res = append(res, uint16((eh.uptime >> 16) & 0xffff))
|
||||
eh.lock.RUnlock()
|
||||
|
||||
case 201:
|
||||
// return the 16 least significant bits of the uptime counter
|
||||
// (again, using locking to avoid concurrency issues)
|
||||
eh.lock.RLock()
|
||||
res = append(res, uint16(eh.uptime & 0xffff))
|
||||
eh.lock.RUnlock()
|
||||
|
||||
|
||||
// expose the current unix timestamp, encoded as a 32-bit unsigned integer
|
||||
// in input registers 202-203
|
||||
// (read it with modbus-cli --target tcp://localhost:5502 ri:uint32:202)
|
||||
case 202:
|
||||
// return the 16 most significant bits of the current unix time
|
||||
res = append(res, uint16((unixTs_s >> 16) & 0xffff))
|
||||
|
||||
case 203:
|
||||
// return the 16 least significant bits of the current unix time
|
||||
res = append(res, uint16(unixTs_s & 0xffff))
|
||||
|
||||
|
||||
// return 3.1415, encoded as a 32-bit floating point number in input
|
||||
// registers 300-301
|
||||
// (read it with modbus-cli --target tcp://localhost:5502 ri:float32:300)
|
||||
case 300:
|
||||
// returh the 16 most significant bits of the number
|
||||
res = append(res, uint16((math.Float32bits(3.1415) >> 16) & 0xffff))
|
||||
|
||||
case 301:
|
||||
// returh the 16 least significant bits of the number
|
||||
res = append(res, uint16((math.Float32bits(3.1415)) & 0xffff))
|
||||
|
||||
|
||||
// attempting to access any input register address other than
|
||||
// those defined above will result in an illegal data address
|
||||
// exception client-side.
|
||||
default:
|
||||
err = modbus.ErrIllegalDataAddress
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
92
examples/tls_client.go
Normal file
92
examples/tls_client.go
Normal file
@@ -0,0 +1,92 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"crypto/tls"
|
||||
"crypto/x509"
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
"github.com/simonvetter/modbus"
|
||||
)
|
||||
|
||||
/*
|
||||
* Modbus client with TLS example.
|
||||
*
|
||||
* This file is intended to be a demo of the modbus client in TCP+TLS
|
||||
* mode. It shows how to load certificates from files and how to
|
||||
* configure the client to use them.
|
||||
*/
|
||||
|
||||
func main() {
|
||||
var client *modbus.ModbusClient
|
||||
var err error
|
||||
var clientKeyPair tls.Certificate
|
||||
var serverCertPool *x509.CertPool
|
||||
var regs []uint16
|
||||
|
||||
// load the client certificate and its associated private key, which
|
||||
// are used to authenticate the client to the server
|
||||
clientKeyPair, err = tls.LoadX509KeyPair(
|
||||
"certs/client.cert.pem", "certs/client.key.pem")
|
||||
if err != nil {
|
||||
fmt.Printf("failed to load client key pair: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
// load either the server certificate or the certificate of the CA
|
||||
// (Certificate Authority) which signed the server certificate
|
||||
serverCertPool, err = modbus.LoadCertPool("certs/server.cert.pem")
|
||||
if err != nil {
|
||||
fmt.Printf("failed to load server certificate/CA: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
// create a client targetting host secure-plc on port 802 using
|
||||
// modbus TCP over TLS (MBAPS)
|
||||
client, err = modbus.NewClient(&modbus.ClientConfiguration{
|
||||
// tcp+tls is the moniker for MBAPS (modbus/tcp encapsulated in
|
||||
// TLS),
|
||||
// 802/tcp is the IANA-registered port for MBAPS.
|
||||
URL: "tcp+tls://secure-plc:802",
|
||||
// set the client-side cert and key
|
||||
TLSClientCert: &clientKeyPair,
|
||||
// set the server/CA certificate
|
||||
TLSRootCAs: serverCertPool,
|
||||
})
|
||||
if err != nil {
|
||||
fmt.Printf("failed to create modbus client: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
// now that the client is created and configured, attempt to connect
|
||||
err = client.Open()
|
||||
if err != nil {
|
||||
fmt.Printf("failed to connect: %v\n", err)
|
||||
os.Exit(2)
|
||||
}
|
||||
|
||||
// read two 16-bit holding registers at address 0x4000
|
||||
regs, err = client.ReadRegisters(0x4000, 2, modbus.HOLDING_REGISTER)
|
||||
if err != nil {
|
||||
fmt.Printf("failed to read registers 0x4000 and 0x4001: %v\n", err)
|
||||
} else {
|
||||
fmt.Printf("register 0x4000: 0x%04x\n", regs[0])
|
||||
fmt.Printf("register 0x4001: 0x%04x\n", regs[1])
|
||||
}
|
||||
|
||||
// set register 0x4002 to 500
|
||||
err = client.WriteRegister(0x4002, 500)
|
||||
if err != nil {
|
||||
fmt.Printf("failed to write to register 0x4002: %v\n", err)
|
||||
} else {
|
||||
fmt.Printf("set register 0x4002 to 500\n")
|
||||
}
|
||||
|
||||
// close the connection
|
||||
err = client.Close()
|
||||
if err != nil {
|
||||
fmt.Printf("failed to close connection: %v\n", err)
|
||||
}
|
||||
|
||||
os.Exit(0)
|
||||
}
|
||||
255
examples/tls_server.go
Normal file
255
examples/tls_server.go
Normal file
@@ -0,0 +1,255 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"crypto/x509"
|
||||
"crypto/tls"
|
||||
"fmt"
|
||||
"os"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/simonvetter/modbus"
|
||||
)
|
||||
|
||||
/* Modbus TCP+TLS (MBAPS or Modbus Security) server example.
|
||||
*
|
||||
* This file is intended to be a demo of the modbus server in a tcp+tls
|
||||
* configuration.
|
||||
* It shows how to configure and start a server, as well as how to use
|
||||
* client roles to perform authorization in the handler.
|
||||
* Feel free to use it as boilerplate for simple servers.
|
||||
*
|
||||
* This server simulates a simple wall clock device, exposing a 32-bit unix
|
||||
* timestamp in holding registers #0 and 1.
|
||||
* The timestamp is incremented every second by the main loop.
|
||||
*
|
||||
* Access control is done by way of Modbus Roles, which are encoded in the
|
||||
* client certificate as an X509 extension:
|
||||
* - any client can read the clock regardless of their role, provided that their
|
||||
* certificate is accepted by the server,
|
||||
* - only clients with the "operator" role specified in their certificate can
|
||||
* set the time.
|
||||
*
|
||||
* Certificates with no, invalid or multiple Modbus Role extensions will have
|
||||
* their role set to an empty string (req.ClientRole == "").
|
||||
*
|
||||
* Requests from clients with certificates not passing TLS verification are
|
||||
* rejected at the TLS layer (i.e. before reaching the Modbus layer).
|
||||
*
|
||||
*
|
||||
* The following commands can be used to create self-signed server and client
|
||||
* certificates:
|
||||
* $ mkdir certs
|
||||
*
|
||||
* create the server key pair:
|
||||
* $ openssl req -x509 -newkey rsa:4096 -sha256 -days 360 -nodes \
|
||||
* -keyout certs/server.key.pem -out certs/server.cert.pem \
|
||||
* -subj "/CN=TEST SERVER CERT DO NOT USE/" -addext "subjectAltName=DNS:localhost" \
|
||||
* -addext "keyUsage=keyCertSign,digitalSignature,keyEncipherment" \
|
||||
* -addext "extendedKeyUsage=critical,serverAuth"
|
||||
*
|
||||
* create a client certificate with the "user" role:
|
||||
* $ openssl req -x509 -newkey rsa:4096 -sha256 -days 360 -nodes \
|
||||
* -keyout certs/user-client.key.pem -out certs/user-client.cert.pem \
|
||||
* -subj "/CN=TEST CLIENT CERT DO NOT USE/" \
|
||||
* -addext "keyUsage=keyCertSign,digitalSignature,keyEncipherment" \
|
||||
* -addext "extendedKeyUsage=critical,clientAuth" \
|
||||
* -addext "1.3.6.1.4.1.50316.802.1=ASN1:UTF8String:user"
|
||||
*
|
||||
* create another client certificate with the "operator" role:
|
||||
* $ openssl req -x509 -newkey rsa:4096 -sha256 -days 360 -nodes \
|
||||
* -keyout certs/operator-client.key.pem -out certs/operator-client.cert.pem \
|
||||
* -subj "/CN=TEST CLIENT CERT DO NOT USE/" \
|
||||
* -addext "keyUsage=keyCertSign,digitalSignature,keyEncipherment" \
|
||||
* -addext "extendedKeyUsage=critical,clientAuth" \
|
||||
* -addext "1.3.6.1.4.1.50316.802.1=ASN1:UTF8String:operator"
|
||||
*
|
||||
* create a file containing both client certificates (for use by the server as an
|
||||
* 'allowed client list'):
|
||||
* $ cat certs/user-client.cert.pem certs/operator-client.cert.pem >certs/clients.cert.pem
|
||||
*
|
||||
* start the server:
|
||||
* $ go run examples/tls_server.go
|
||||
*
|
||||
* in another shell, read the clock with modbus-cli as the 'user' role:
|
||||
* $ go run cmd/modbus-cli.go --target tcp+tls://localhost:5802 --cert certs/user-client.cert.pem \
|
||||
* --key certs/user-client.key.pem --ca certs/server.cert.pem rh:uint32:0
|
||||
*
|
||||
* attempting to set the clock as 'user' should fail with Illegal Function:
|
||||
* $ go run cmd/modbus-cli.go --target tcp+tls://localhost:5802 --cert certs/user-client.cert.pem \
|
||||
* --key certs/user-client.key.pem --ca certs/server.cert.pem wr:uint32:0:1598692358
|
||||
*
|
||||
* setting the clock as 'operator' should succeed:
|
||||
* $ go run cmd/modbus-cli.go --target tcp+tls://localhost:5802 --cert certs/operator-client.cert.pem \
|
||||
* --key certs/operator-client.key.pem --ca certs/server.cert.pem wr:uint32:0:1598692358
|
||||
*
|
||||
* reading the cock as 'operator' should also work:
|
||||
* $ go run cmd/modbus-cli.go --target tcp+tls://localhost:5802 --cert certs/operator-client.cert.pem \
|
||||
* --key certs/operator-client.key.pem --ca certs/server.cert.pem rh:uint32:0
|
||||
*/
|
||||
|
||||
func main() {
|
||||
var err error
|
||||
var eh *exampleHandler
|
||||
var server *modbus.ModbusServer
|
||||
var serverKeyPair tls.Certificate
|
||||
var clientCertPool *x509.CertPool
|
||||
var ticker *time.Ticker
|
||||
|
||||
// create the handler object
|
||||
eh = &exampleHandler{}
|
||||
|
||||
// load the server certificate and its associated private key, which
|
||||
// are used to authenticate the server to the client.
|
||||
// note that a tls.Certificate object can contain both the cert and its key,
|
||||
// which is the case here.
|
||||
serverKeyPair, err = tls.LoadX509KeyPair(
|
||||
"certs/server.cert.pem", "certs/server.key.pem")
|
||||
if err != nil {
|
||||
fmt.Printf("failed to load server key pair: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
// load TLS client authentication material, which could either be:
|
||||
// - the CA (Certificate Authority) certificate(s) used to sign client certs,
|
||||
// - the list of allowed client certs, if client certificates are self-signed or
|
||||
// if client certificate pinning is required.
|
||||
clientCertPool, err = modbus.LoadCertPool("certs/clients.cert.pem")
|
||||
if err != nil {
|
||||
fmt.Printf("failed to load CA/client certificates: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
// create the server object
|
||||
server, err = modbus.NewServer(&modbus.ServerConfiguration{
|
||||
// listen on localhost port 5802
|
||||
URL: "tcp+tls://localhost:5802",
|
||||
// accept 10 concurrent connections max.
|
||||
MaxClients: 10,
|
||||
// close idle connections after 1min of inactivity
|
||||
Timeout: 60 * time.Second,
|
||||
// use serverKeyPair as server certificate + server private key
|
||||
TLSServerCert: &serverKeyPair,
|
||||
// use the client cert/CA pool to verify client certificates
|
||||
TLSClientCAs: clientCertPool,
|
||||
}, eh)
|
||||
if err != nil {
|
||||
fmt.Printf("failed to create server: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
// start accepting client connections
|
||||
// note that Start() returns as soon as the server is started
|
||||
err = server.Start()
|
||||
if err != nil {
|
||||
fmt.Printf("failed to start server: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
fmt.Println("server started")
|
||||
|
||||
ticker = time.NewTicker(1 * time.Second)
|
||||
for {
|
||||
<-ticker.C
|
||||
|
||||
// increment the clock every second.
|
||||
// lock the handler object while updating the clock register to avoid
|
||||
// concurrency issues as each client is served from a dedicated goroutine.
|
||||
eh.lock.Lock()
|
||||
eh.clock++
|
||||
eh.lock.Unlock()
|
||||
}
|
||||
|
||||
// never reached
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// Example handler object, passed to the NewServer() constructor above.
|
||||
type exampleHandler struct {
|
||||
// this lock is used to avoid concurrency issues between goroutines, as
|
||||
// handler methods are called from different goroutines
|
||||
// (1 goroutine per client)
|
||||
lock sync.RWMutex
|
||||
|
||||
// unix timestamp register, incremented in the main() function above and exposed
|
||||
// as a 32-bit holding register (2 consecutive 16-bit modbus registers).
|
||||
clock uint32
|
||||
}
|
||||
|
||||
// Holding register handler method.
|
||||
// This method gets called whenever a valid modbus request asking for a holding register
|
||||
// operation is received by the server.
|
||||
func (eh *exampleHandler) HandleHoldingRegisters(req *modbus.HoldingRegistersRequest) (res []uint16, err error) {
|
||||
var regAddr uint16
|
||||
|
||||
// require the "operator" role for write operations (i.e. set the clock).
|
||||
if req.IsWrite && req.ClientRole != "operator" {
|
||||
fmt.Printf("write access denied: client %s missing the 'operator' role (role: '%s')\n",
|
||||
req.ClientAddr, req.ClientRole)
|
||||
err = modbus.ErrIllegalFunction
|
||||
return
|
||||
}
|
||||
|
||||
// since we're manipulating variables accessed from multiple goroutines,
|
||||
// acquire a lock to avoid concurrency issues.
|
||||
eh.lock.Lock()
|
||||
// release the lock upon return
|
||||
defer eh.lock.Unlock()
|
||||
|
||||
// loop through `quantity` registers
|
||||
for i := 0; i < int(req.Quantity); i++ {
|
||||
// compute the target register address
|
||||
regAddr = req.Addr + uint16(i)
|
||||
|
||||
switch regAddr {
|
||||
// expose the 16 most-significant bits of the clock in register #0
|
||||
case 0:
|
||||
if req.IsWrite {
|
||||
eh.clock =
|
||||
((uint32(req.Args[i]) << 16) & 0xffff0000 |
|
||||
(eh.clock & 0x0000ffff))
|
||||
}
|
||||
res = append(res, uint16((eh.clock >> 16) & 0x0000ffff))
|
||||
|
||||
// expose the 16 least-significant bits of the clock in register #1
|
||||
case 1:
|
||||
if req.IsWrite {
|
||||
eh.clock =
|
||||
(uint32(req.Args[i]) & 0x0000ffff |
|
||||
(eh.clock & 0xffff0000))
|
||||
}
|
||||
res = append(res, uint16(eh.clock & 0x0000ffff))
|
||||
|
||||
// any other address is unknown
|
||||
default:
|
||||
err = modbus.ErrIllegalDataAddress
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// input registers are not used by this server.
|
||||
func (eh *exampleHandler) HandleInputRegisters(req *modbus.InputRegistersRequest) (res []uint16, err error) {
|
||||
// this is the equivalent of saying
|
||||
// "input registers are not supported by this device"
|
||||
err = modbus.ErrIllegalFunction
|
||||
return
|
||||
}
|
||||
|
||||
// coils are not used by this server.
|
||||
func (eh *exampleHandler) HandleCoils(req *modbus.CoilsRequest) (res []bool, err error) {
|
||||
// this is the equivalent of saying
|
||||
// "coils are not supported by this device"
|
||||
err = modbus.ErrIllegalFunction
|
||||
return
|
||||
}
|
||||
|
||||
// discrete inputs are not used by this server.
|
||||
func (eh *exampleHandler) HandleDiscreteInputs(req *modbus.DiscreteInputsRequest) (res []bool, err error) {
|
||||
// this is the equivalent of saying
|
||||
// "discrete inputs are not supported by this device"
|
||||
err = modbus.ErrIllegalFunction
|
||||
return
|
||||
}
|
||||
7
go.mod
Normal file
7
go.mod
Normal file
@@ -0,0 +1,7 @@
|
||||
module git.whblueocean.cn/communication-protocol/modbus
|
||||
|
||||
// module github.com/simonvetter/modbus
|
||||
|
||||
go 1.16
|
||||
|
||||
require github.com/goburrow/serial v0.1.0
|
||||
2
go.sum
Normal file
2
go.sum
Normal file
@@ -0,0 +1,2 @@
|
||||
github.com/goburrow/serial v0.1.0 h1:v2T1SQa/dlUqQiYIT8+Cu7YolfqAi3K96UmhwYyuSrA=
|
||||
github.com/goburrow/serial v0.1.0/go.mod h1:sAiqG0nRVswsm1C97xsttiYCzSLBmUZ/VSlVLZJ8haA=
|
||||
81
logger.go
Normal file
81
logger.go
Normal file
@@ -0,0 +1,81 @@
|
||||
package modbus
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"log"
|
||||
)
|
||||
|
||||
type logger struct {
|
||||
prefix string
|
||||
customLogger *log.Logger
|
||||
}
|
||||
|
||||
func newLogger(prefix string, customLogger *log.Logger) (l *logger) {
|
||||
l = &logger{
|
||||
prefix: prefix,
|
||||
customLogger: customLogger,
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func (l *logger) Info(msg string) {
|
||||
l.write(fmt.Sprintf("%s [info]: %s\n", l.prefix, msg))
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func (l *logger) Infof(format string, msg ...interface{}) {
|
||||
l.write(fmt.Sprintf("%s [info]: %s\n", l.prefix, fmt.Sprintf(format, msg...)))
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func (l *logger) Warning(msg string) {
|
||||
l.write(fmt.Sprintf("%s [warn]: %s\n", l.prefix, msg))
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func (l *logger) Warningf(format string, msg ...interface{}) {
|
||||
l.write(fmt.Sprintf("%s [warn]: %s\n", l.prefix, fmt.Sprintf(format, msg...)))
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func (l *logger) Error(msg string) {
|
||||
l.write(fmt.Sprintf("%s [error]: %s\n", l.prefix, msg))
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func (l *logger) Errorf(format string, msg ...interface{}) {
|
||||
l.write(fmt.Sprintf("%s [error]: %s\n", l.prefix, fmt.Sprintf(format, msg...)))
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func (l *logger) Fatal(msg string) {
|
||||
l.Error(msg)
|
||||
os.Exit(1)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func (l *logger) Fatalf(format string, msg ...interface{}) {
|
||||
l.Errorf(format, msg...)
|
||||
os.Exit(1)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func (l *logger) write(msg string) {
|
||||
if l.customLogger == nil {
|
||||
os.Stdout.WriteString(msg)
|
||||
} else {
|
||||
l.customLogger.Print(msg)
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
43
logger_test.go
Normal file
43
logger_test.go
Normal file
@@ -0,0 +1,43 @@
|
||||
package modbus
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"log"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestClientCustomLogger(t *testing.T) {
|
||||
var buf bytes.Buffer
|
||||
var logger *log.Logger
|
||||
|
||||
logger = log.New(&buf, "external-prefix: ", 0)
|
||||
|
||||
_, _ = NewClient(&ClientConfiguration{
|
||||
Logger: logger,
|
||||
URL: "sometype://sometarget",
|
||||
})
|
||||
|
||||
if buf.String() != "external-prefix: modbus-client(sometarget) [error]: unsupported client type 'sometype'\n" {
|
||||
t.Errorf("unexpected logger output '%s'", buf.String())
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func TestServerCustomLogger(t *testing.T) {
|
||||
var buf bytes.Buffer
|
||||
var logger *log.Logger
|
||||
|
||||
logger = log.New(&buf, "external-prefix: ", 0)
|
||||
|
||||
_, _ = NewServer(&ServerConfiguration{
|
||||
Logger: logger,
|
||||
URL: "tcp://",
|
||||
}, nil)
|
||||
|
||||
if buf.String() != "external-prefix: modbus-server() [error]: missing host part in URL 'tcp://'\n" {
|
||||
t.Errorf("unexpected logger output '%s'", buf.String())
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
132
modbus.go
Normal file
132
modbus.go
Normal file
@@ -0,0 +1,132 @@
|
||||
package modbus
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
)
|
||||
|
||||
type pdu struct {
|
||||
unitId uint8
|
||||
functionCode uint8
|
||||
payload []byte
|
||||
}
|
||||
|
||||
type Error string
|
||||
|
||||
// Error implements the error interface.
|
||||
func (me Error) Error() (s string) {
|
||||
s = string(me)
|
||||
return
|
||||
}
|
||||
|
||||
const (
|
||||
// coils
|
||||
fcReadCoils uint8 = 0x01
|
||||
fcWriteSingleCoil uint8 = 0x05
|
||||
fcWriteMultipleCoils uint8 = 0x0f
|
||||
|
||||
// discrete inputs
|
||||
fcReadDiscreteInputs uint8 = 0x02
|
||||
|
||||
// 16-bit input/holding registers
|
||||
fcReadHoldingRegisters uint8 = 0x03
|
||||
fcReadInputRegisters uint8 = 0x04
|
||||
fcWriteSingleRegister uint8 = 0x06
|
||||
fcWriteMultipleRegisters uint8 = 0x10
|
||||
fcMaskWriteRegister uint8 = 0x16
|
||||
fcReadWriteMultipleRegisters uint8 = 0x17
|
||||
fcReadFifoQueue uint8 = 0x18
|
||||
|
||||
// file access
|
||||
fcReadFileRecord uint8 = 0x14
|
||||
fcWriteFileRecord uint8 = 0x15
|
||||
|
||||
// customize
|
||||
fcCustomize uint8 = 0x29
|
||||
|
||||
// exception codes
|
||||
exIllegalFunction uint8 = 0x01
|
||||
exIllegalDataAddress uint8 = 0x02
|
||||
exIllegalDataValue uint8 = 0x03
|
||||
exServerDeviceFailure uint8 = 0x04
|
||||
exAcknowledge uint8 = 0x05
|
||||
exServerDeviceBusy uint8 = 0x06
|
||||
exMemoryParityError uint8 = 0x08
|
||||
exGWPathUnavailable uint8 = 0x0a
|
||||
exGWTargetFailedToRespond uint8 = 0x0b
|
||||
|
||||
// errors
|
||||
ErrConfigurationError Error = "configuration error"
|
||||
ErrRequestTimedOut Error = "request timed out"
|
||||
ErrIllegalFunction Error = "illegal function"
|
||||
ErrIllegalDataAddress Error = "illegal data address"
|
||||
ErrIllegalDataValue Error = "illegal data value"
|
||||
ErrServerDeviceFailure Error = "server device failure"
|
||||
ErrAcknowledge Error = "request acknowledged"
|
||||
ErrServerDeviceBusy Error = "server device busy"
|
||||
ErrMemoryParityError Error = "memory parity error"
|
||||
ErrGWPathUnavailable Error = "gateway path unavailable"
|
||||
ErrGWTargetFailedToRespond Error = "gateway target device failed to respond"
|
||||
ErrBadCRC Error = "bad crc"
|
||||
ErrShortFrame Error = "short frame"
|
||||
ErrProtocolError Error = "protocol error"
|
||||
ErrBadUnitId Error = "bad unit id"
|
||||
ErrBadTransactionId Error = "bad transaction id"
|
||||
ErrUnknownProtocolId Error = "unknown protocol identifier"
|
||||
ErrUnexpectedParameters Error = "unexpected parameters"
|
||||
)
|
||||
|
||||
// mapExceptionCodeToError turns a modbus exception code into a higher level Error object.
|
||||
func mapExceptionCodeToError(exceptionCode uint8) (err error) {
|
||||
switch exceptionCode {
|
||||
case exIllegalFunction:
|
||||
err = ErrIllegalFunction
|
||||
case exIllegalDataAddress:
|
||||
err = ErrIllegalDataAddress
|
||||
case exIllegalDataValue:
|
||||
err = ErrIllegalDataValue
|
||||
case exServerDeviceFailure:
|
||||
err = ErrServerDeviceFailure
|
||||
case exAcknowledge:
|
||||
err = ErrAcknowledge
|
||||
case exMemoryParityError:
|
||||
err = ErrMemoryParityError
|
||||
case exServerDeviceBusy:
|
||||
err = ErrServerDeviceBusy
|
||||
case exGWPathUnavailable:
|
||||
err = ErrGWPathUnavailable
|
||||
case exGWTargetFailedToRespond:
|
||||
err = ErrGWTargetFailedToRespond
|
||||
default:
|
||||
err = fmt.Errorf("unknown exception code (%v)", exceptionCode)
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// mapErrorToExceptionCode turns an Error object into a modbus exception code.
|
||||
func mapErrorToExceptionCode(err error) (exceptionCode uint8) {
|
||||
switch err {
|
||||
case ErrIllegalFunction:
|
||||
exceptionCode = exIllegalFunction
|
||||
case ErrIllegalDataAddress:
|
||||
exceptionCode = exIllegalDataAddress
|
||||
case ErrIllegalDataValue:
|
||||
exceptionCode = exIllegalDataValue
|
||||
case ErrServerDeviceFailure:
|
||||
exceptionCode = exServerDeviceFailure
|
||||
case ErrAcknowledge:
|
||||
exceptionCode = exAcknowledge
|
||||
case ErrMemoryParityError:
|
||||
exceptionCode = exMemoryParityError
|
||||
case ErrServerDeviceBusy:
|
||||
exceptionCode = exServerDeviceBusy
|
||||
case ErrGWPathUnavailable:
|
||||
exceptionCode = exGWPathUnavailable
|
||||
case ErrGWTargetFailedToRespond:
|
||||
exceptionCode = exGWTargetFailedToRespond
|
||||
default:
|
||||
exceptionCode = exServerDeviceFailure
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
269
rtu_transport.go
Normal file
269
rtu_transport.go
Normal file
@@ -0,0 +1,269 @@
|
||||
package modbus
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"log"
|
||||
"time"
|
||||
)
|
||||
|
||||
const (
|
||||
maxRTUFrameLength int = 256
|
||||
)
|
||||
|
||||
type rtuTransport struct {
|
||||
logger *logger
|
||||
link rtuLink
|
||||
timeout time.Duration
|
||||
lastActivity time.Time
|
||||
t35 time.Duration
|
||||
t1 time.Duration
|
||||
}
|
||||
|
||||
type rtuLink interface {
|
||||
Close() (error)
|
||||
Read([]byte) (int, error)
|
||||
Write([]byte) (int, error)
|
||||
SetDeadline(time.Time) (error)
|
||||
}
|
||||
|
||||
// Returns a new RTU transport.
|
||||
func newRTUTransport(link rtuLink, addr string, speed uint, timeout time.Duration, customLogger *log.Logger) (rt *rtuTransport) {
|
||||
rt = &rtuTransport{
|
||||
logger: newLogger(fmt.Sprintf("rtu-transport(%s)", addr), customLogger),
|
||||
link: link,
|
||||
timeout: timeout,
|
||||
t1: serialCharTime(speed),
|
||||
}
|
||||
|
||||
if speed >= 19200 {
|
||||
// for baud rates equal to or greater than 19200 bauds, a fixed value of
|
||||
// 1750 uS is specified for t3.5.
|
||||
rt.t35 = 1750 * time.Microsecond
|
||||
} else {
|
||||
// for lower baud rates, the inter-frame delay should be 3.5 character times
|
||||
rt.t35 = (serialCharTime(speed) * 35) / 10
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// Closes the rtu link.
|
||||
func (rt *rtuTransport) Close() (err error) {
|
||||
err = rt.link.Close()
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// Runs a request across the rtu link and returns a response.
|
||||
func (rt *rtuTransport) ExecuteRequest(req *pdu) (res *pdu, err error) {
|
||||
var ts time.Time
|
||||
var t time.Duration
|
||||
var n int
|
||||
|
||||
// set an i/o deadline on the link
|
||||
err = rt.link.SetDeadline(time.Now().Add(rt.timeout))
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
// if the line was active less than 3.5 char times ago,
|
||||
// let t3.5 expire before transmitting
|
||||
t = time.Since(rt.lastActivity.Add(rt.t35))
|
||||
if t < 0 {
|
||||
time.Sleep(t * (-1))
|
||||
}
|
||||
|
||||
ts = time.Now()
|
||||
|
||||
// build an RTU ADU out of the request object and
|
||||
// send the final ADU+CRC on the wire
|
||||
n, err = rt.link.Write(rt.assembleRTUFrame(req))
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
// estimate how long the serial line was busy for.
|
||||
// note that on most platforms, Write() will be buffered and return
|
||||
// immediately rather than block until the buffer is drained
|
||||
rt.lastActivity = ts.Add(time.Duration(n) * rt.t1)
|
||||
|
||||
// observe inter-frame delays
|
||||
time.Sleep(rt.lastActivity.Add(rt.t35).Sub(time.Now()))
|
||||
|
||||
// read the response back from the wire
|
||||
res, err = rt.readRTUFrame()
|
||||
|
||||
if err == ErrBadCRC || err == ErrProtocolError || err == ErrShortFrame {
|
||||
// wait for and flush any data coming off the link to allow
|
||||
// devices to re-sync
|
||||
time.Sleep(time.Duration(maxRTUFrameLength) * rt.t1)
|
||||
discard(rt.link)
|
||||
}
|
||||
|
||||
// mark the time if we heard anything back
|
||||
if err != ErrRequestTimedOut {
|
||||
rt.lastActivity = time.Now()
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// Reads a request from the rtu link.
|
||||
func (rt *rtuTransport) ReadRequest() (req *pdu, err error) {
|
||||
// reading requests from RTU links is currently unsupported
|
||||
err = fmt.Errorf("unimplemented")
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// Writes a response to the rtu link.
|
||||
func (rt *rtuTransport) WriteResponse(res *pdu) (err error) {
|
||||
var n int
|
||||
|
||||
// build an RTU ADU out of the request object and
|
||||
// send the final ADU+CRC on the wire
|
||||
n, err = rt.link.Write(rt.assembleRTUFrame(res))
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
rt.lastActivity = time.Now().Add(rt.t1 * time.Duration(n))
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// Waits for, reads and decodes a frame from the rtu link.
|
||||
func (rt *rtuTransport) readRTUFrame() (res *pdu, err error) {
|
||||
var rxbuf []byte
|
||||
var byteCount int
|
||||
var bytesNeeded int
|
||||
var crc crc
|
||||
|
||||
rxbuf = make([]byte, maxRTUFrameLength)
|
||||
|
||||
// read the serial ADU header: unit id (1 byte), function code (1 byte) and
|
||||
// PDU length/exception code (1 byte)
|
||||
byteCount, err = io.ReadFull(rt.link, rxbuf[0:3])
|
||||
if (byteCount > 0 || err == nil) && byteCount != 3 {
|
||||
err = ErrShortFrame
|
||||
return
|
||||
}
|
||||
if err != nil && err != io.ErrUnexpectedEOF {
|
||||
return
|
||||
}
|
||||
|
||||
// figure out how many further bytes to read
|
||||
bytesNeeded, err = expectedResponseLenth(uint8(rxbuf[1]), uint8(rxbuf[2]))
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
// we need to read 2 additional bytes of CRC after the payload
|
||||
bytesNeeded += 2
|
||||
|
||||
// never read more than the max allowed frame length
|
||||
if byteCount + bytesNeeded > maxRTUFrameLength {
|
||||
err = ErrProtocolError
|
||||
return
|
||||
}
|
||||
|
||||
byteCount, err = io.ReadFull(rt.link, rxbuf[3:3 + bytesNeeded])
|
||||
if err != nil && err != io.ErrUnexpectedEOF {
|
||||
return
|
||||
}
|
||||
if byteCount != bytesNeeded {
|
||||
rt.logger.Warningf("expected %v bytes, received %v", bytesNeeded, byteCount)
|
||||
err = ErrShortFrame
|
||||
return
|
||||
}
|
||||
|
||||
// compute the CRC on the entire frame, excluding the CRC
|
||||
crc.init()
|
||||
crc.add(rxbuf[0:3 + bytesNeeded - 2])
|
||||
|
||||
// compare CRC values
|
||||
if !crc.isEqual(rxbuf[3 + bytesNeeded - 2], rxbuf[3 + bytesNeeded - 1]) {
|
||||
err = ErrBadCRC
|
||||
return
|
||||
}
|
||||
|
||||
res = &pdu{
|
||||
unitId: rxbuf[0],
|
||||
functionCode: rxbuf[1],
|
||||
// pass the byte count + trailing data as payload, withtout the CRC
|
||||
payload: rxbuf[2:3 + bytesNeeded - 2],
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// Turns a PDU object into bytes.
|
||||
func (rt *rtuTransport) assembleRTUFrame(p *pdu) (adu []byte) {
|
||||
var crc crc
|
||||
|
||||
adu = append(adu, p.unitId)
|
||||
adu = append(adu, p.functionCode)
|
||||
adu = append(adu, p.payload...)
|
||||
|
||||
// run the ADU through the CRC generator
|
||||
crc.init()
|
||||
crc.add(adu)
|
||||
|
||||
// append the CRC to the ADU
|
||||
adu = append(adu, crc.value()...)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// Computes the expected length of a modbus RTU response.
|
||||
func expectedResponseLenth(responseCode uint8, responseLength uint8) (byteCount int, err error) {
|
||||
switch responseCode {
|
||||
case fcReadHoldingRegisters,
|
||||
fcReadInputRegisters,
|
||||
fcReadCoils,
|
||||
fcReadDiscreteInputs: byteCount = int(responseLength)
|
||||
case fcWriteSingleRegister,
|
||||
fcWriteMultipleRegisters,
|
||||
fcWriteSingleCoil,
|
||||
fcWriteMultipleCoils: byteCount = 3
|
||||
case fcMaskWriteRegister: byteCount = 5
|
||||
case fcReadHoldingRegisters | 0x80,
|
||||
fcReadInputRegisters | 0x80,
|
||||
fcReadCoils | 0x80,
|
||||
fcReadDiscreteInputs | 0x80,
|
||||
fcWriteSingleRegister | 0x80,
|
||||
fcWriteMultipleRegisters | 0x80,
|
||||
fcWriteSingleCoil | 0x80,
|
||||
fcWriteMultipleCoils | 0x80,
|
||||
fcMaskWriteRegister | 0x80: byteCount = 0
|
||||
default: err = ErrProtocolError
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// Discards the contents of the link's rx buffer, eating up to 1kB of data.
|
||||
// Note that on a serial line, this call may block for up to serialConf.Timeout
|
||||
// i.e. 10ms.
|
||||
func discard(link rtuLink) {
|
||||
var rxbuf = make([]byte, 1024)
|
||||
|
||||
link.SetDeadline(time.Now().Add(500 * time.Microsecond))
|
||||
io.ReadFull(link, rxbuf)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// Returns how long it takes to send 1 byte on a serial line at the
|
||||
// specified baud rate.
|
||||
func serialCharTime(rate_bps uint) (ct time.Duration) {
|
||||
// note: an RTU byte on the wire is:
|
||||
// - 1 start bit,
|
||||
// - 8 data bits,
|
||||
// - 1 parity or stop bit
|
||||
// - 1 stop bit
|
||||
ct = (11) * time.Second / time.Duration(rate_bps)
|
||||
|
||||
return
|
||||
}
|
||||
189
rtu_transport_test.go
Normal file
189
rtu_transport_test.go
Normal file
@@ -0,0 +1,189 @@
|
||||
package modbus
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"io"
|
||||
"net"
|
||||
"time"
|
||||
)
|
||||
|
||||
func TestAssembleRTUFrame(t *testing.T) {
|
||||
var rt *rtuTransport
|
||||
var frame []byte
|
||||
|
||||
rt = &rtuTransport{}
|
||||
|
||||
frame = rt.assembleRTUFrame(&pdu{
|
||||
unitId: 0x33,
|
||||
functionCode: 0x11,
|
||||
payload: []byte{0x22, 0x33, 0x44, 0x55},
|
||||
})
|
||||
// expect 1 byte of unit id, 1 byte of function code, 4 bytes of payload and
|
||||
// 2 bytes of CRC
|
||||
if len(frame) != 8 {
|
||||
t.Errorf("expected 8 bytes, got %v", len(frame))
|
||||
}
|
||||
for i, b := range []byte{
|
||||
0x33, 0x11, // unit id and function code
|
||||
0x22, 0x33, // payload
|
||||
0x44, 0x55, // payload
|
||||
0xf0, 0x93, // CRC
|
||||
} {
|
||||
if frame[i] != b {
|
||||
t.Errorf("expected 0x%02x at position %v, got 0x%02x", b, i, frame[i])
|
||||
}
|
||||
}
|
||||
|
||||
frame = rt.assembleRTUFrame(&pdu{
|
||||
unitId: 0x31,
|
||||
functionCode: 0x06,
|
||||
payload: []byte{0x12, 0x34},
|
||||
})
|
||||
// expect 1 byte of unit if, 1 byte of function code, 2 bytes of payload and
|
||||
// 2 bytes of CRC
|
||||
if len(frame) != 6 {
|
||||
t.Errorf("expected 6 bytes, got %v", len(frame))
|
||||
}
|
||||
for i, b := range []byte{
|
||||
0x31, 0x06, // unit id and function code
|
||||
0x12, 0x34, // payload
|
||||
0xe3, 0xae, // CRC
|
||||
} {
|
||||
if frame[i] != b {
|
||||
t.Errorf("expected 0x%02x at position %v, got 0x%02x", b, i, frame[i])
|
||||
}
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
func TestRTUTransportReadRTUFrame(t *testing.T) {
|
||||
var rt *rtuTransport
|
||||
var p1, p2 net.Conn
|
||||
var txchan chan []byte
|
||||
var err error
|
||||
var res *pdu
|
||||
|
||||
txchan = make(chan []byte, 2)
|
||||
p1, p2 = net.Pipe()
|
||||
go feedTestPipe(t, txchan, p1)
|
||||
|
||||
|
||||
rt = newRTUTransport(p2, "", 9600, 10 * time.Millisecond, nil)
|
||||
|
||||
// read a valid response (illegal data address)
|
||||
txchan <- []byte{
|
||||
0x31, 0x82, // unit id and response code
|
||||
0x02, // exception code
|
||||
0xc1, 0x6e, // CRC
|
||||
}
|
||||
res, err = rt.readRTUFrame()
|
||||
if err != nil {
|
||||
t.Errorf("readRTUFrame() should have succeeded, got %v", err)
|
||||
}
|
||||
if res.unitId != 0x31 {
|
||||
t.Errorf("expected 0x31 as unit id, got 0x%02x", res.unitId)
|
||||
}
|
||||
if res.functionCode != 0x82 {
|
||||
t.Errorf("expected 0x82 as function code, got 0x%02x", res.functionCode)
|
||||
}
|
||||
if len(res.payload) != 1 {
|
||||
t.Errorf("expected a length of 1, got %v", len(res.payload))
|
||||
}
|
||||
if res.payload[0] != 0x02 {
|
||||
t.Errorf("expected {0x02} as payload, got {0x%02x}",
|
||||
res.payload[0])
|
||||
}
|
||||
|
||||
// read a frame with a bad crc
|
||||
txchan <- []byte{
|
||||
0x30, 0x82, // unit id and response code
|
||||
0x12, // exception code
|
||||
0xc0, 0xa2, // CRC
|
||||
}
|
||||
res, err = rt.readRTUFrame()
|
||||
if err != ErrBadCRC {
|
||||
t.Errorf("readRTUFrame() should have returned ErrBadCrc, got %v", err)
|
||||
}
|
||||
|
||||
// read a longer, valid response
|
||||
txchan <- []byte{
|
||||
0x31, 0x03, // unit id and response code
|
||||
0x04, // length
|
||||
0x11, 0x22, // register #1
|
||||
0x33, 0x44, // register #2
|
||||
0x7b, 0xc5, // CRC
|
||||
}
|
||||
res, err = rt.readRTUFrame()
|
||||
if err != nil {
|
||||
t.Errorf("readRTUFrame() should have succeeded, got %v", err)
|
||||
}
|
||||
if res.unitId != 0x31 {
|
||||
t.Errorf("expected 0x31 as unit id, got 0x%02x", res.unitId)
|
||||
}
|
||||
if res.functionCode != 0x03 {
|
||||
t.Errorf("expected 0x03 as function code, got 0x%02x", res.functionCode)
|
||||
}
|
||||
if len(res.payload) != 5 {
|
||||
t.Errorf("expected a length of 5, got %v", len(res.payload))
|
||||
}
|
||||
for i, b := range []byte{
|
||||
0x04,
|
||||
0x11, 0x22,
|
||||
0x33, 0x44,
|
||||
} {
|
||||
if res.payload[i] != b {
|
||||
t.Errorf("expected 0x%02x at position %v, got 0x%02x",
|
||||
b, i, res.payload[i])
|
||||
}
|
||||
}
|
||||
|
||||
p1.Close()
|
||||
p2.Close()
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func feedTestPipe(t *testing.T, in chan []byte, out io.WriteCloser) {
|
||||
var err error
|
||||
var txbuf []byte
|
||||
|
||||
for {
|
||||
// grab a slice of bytes from the channel
|
||||
txbuf = <-in
|
||||
|
||||
// write this slice to the pipe
|
||||
_, err = out.Write(txbuf)
|
||||
if err != nil {
|
||||
t.Errorf("failed to write to test pipe: %v", err)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func TestModbusRTUSerialCharTime(t *testing.T) {
|
||||
var d time.Duration
|
||||
|
||||
d = serialCharTime(38400)
|
||||
// expect 11 bits at 38400bps: 11 * (1/38400) = 286.458uS
|
||||
if d != time.Duration(286458) * time.Nanosecond {
|
||||
t.Errorf("unexpected serial char duration: %v", d)
|
||||
}
|
||||
|
||||
d = serialCharTime(19200)
|
||||
// expect 11 bits at 19200bps: 11 * (1/19200) = 572.916uS
|
||||
if d != time.Duration(572916) * time.Nanosecond {
|
||||
t.Errorf("unexpected serial char duration: %v", d)
|
||||
}
|
||||
|
||||
d = serialCharTime(9600)
|
||||
// expect 11 bits at 9600bps: 11 * (1/9600) = 1.145833ms
|
||||
if d != time.Duration(1145833) * time.Nanosecond {
|
||||
t.Errorf("unexpected serial char duration: %v", d)
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
103
serial.go
Normal file
103
serial.go
Normal file
@@ -0,0 +1,103 @@
|
||||
package modbus
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/goburrow/serial"
|
||||
)
|
||||
|
||||
// serialPortWrapper wraps a serial.Port (i.e. physical port) to
|
||||
// 1) satisfy the rtuLink interface and
|
||||
// 2) add Read() deadline/timeout support.
|
||||
type serialPortWrapper struct {
|
||||
conf *serialPortConfig
|
||||
port serial.Port
|
||||
deadline time.Time
|
||||
}
|
||||
|
||||
type serialPortConfig struct {
|
||||
Device string
|
||||
Speed uint
|
||||
DataBits uint
|
||||
Parity uint
|
||||
StopBits uint
|
||||
}
|
||||
|
||||
func newSerialPortWrapper(conf *serialPortConfig) (spw *serialPortWrapper) {
|
||||
spw = &serialPortWrapper{
|
||||
conf: conf,
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func (spw *serialPortWrapper) Open() (err error) {
|
||||
var parity string
|
||||
|
||||
switch spw.conf.Parity {
|
||||
case PARITY_NONE: parity = "N"
|
||||
case PARITY_EVEN: parity = "E"
|
||||
case PARITY_ODD: parity = "O"
|
||||
}
|
||||
|
||||
spw.port, err = serial.Open(&serial.Config{
|
||||
Address: spw.conf.Device,
|
||||
BaudRate: int(spw.conf.Speed),
|
||||
DataBits: int(spw.conf.DataBits),
|
||||
Parity: parity,
|
||||
StopBits: int(spw.conf.StopBits),
|
||||
Timeout: 10 * time.Millisecond,
|
||||
})
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// Closes the serial port.
|
||||
func (spw *serialPortWrapper) Close() (err error) {
|
||||
err = spw.port.Close()
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// Reads bytes from the underlying serial port.
|
||||
// If Read() is called after the deadline, a timeout error is returned without
|
||||
// attempting to read from the serial port.
|
||||
// If Read() is called before the deadline, a read attempt to the serial port
|
||||
// is made. At this point, one of two things can happen:
|
||||
// - the serial port's receive buffer has one or more bytes and port.Read()
|
||||
// returns immediately (partial or full read),
|
||||
// - the serial port's receive buffer is empty: port.Read() blocks for
|
||||
// up to 10ms and returns serial.ErrTimeout. The serial timeout error is
|
||||
// masked and Read() returns with no data.
|
||||
// As the higher-level methods use io.ReadFull(), Read() will be called
|
||||
// as many times as necessary until either enough bytes have been read or an
|
||||
// error is returned (ErrRequestTimedOut or any other i/o error).
|
||||
func (spw *serialPortWrapper) Read(rxbuf []byte) (cnt int, err error) {
|
||||
// return a timeout error if the deadline has passed
|
||||
if time.Now().After(spw.deadline) {
|
||||
err = ErrRequestTimedOut
|
||||
return
|
||||
}
|
||||
|
||||
cnt, err = spw.port.Read(rxbuf)
|
||||
// mask serial.ErrTimeout errors from the serial port
|
||||
if err != nil && err == serial.ErrTimeout {
|
||||
err = nil
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// Sends the bytes over the wire.
|
||||
func (spw *serialPortWrapper) Write(txbuf []byte) (cnt int, err error) {
|
||||
cnt, err = spw.port.Write(txbuf)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// Saves the i/o deadline (only used by Read).
|
||||
func (spw *serialPortWrapper) SetDeadline(deadline time.Time) (err error) {
|
||||
spw.deadline = deadline
|
||||
|
||||
return
|
||||
}
|
||||
901
server.go
Normal file
901
server.go
Normal file
@@ -0,0 +1,901 @@
|
||||
package modbus
|
||||
|
||||
import (
|
||||
"crypto/tls"
|
||||
"crypto/x509"
|
||||
"encoding/asn1"
|
||||
"errors"
|
||||
"fmt"
|
||||
"log"
|
||||
"net"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
// Modbus Role PEM OID (see R-21 of the MBAPS spec)
|
||||
var modbusRoleOID asn1.ObjectIdentifier = asn1.ObjectIdentifier{
|
||||
1, 3, 6, 1, 4, 1, 50316, 802, 1,
|
||||
}
|
||||
|
||||
// Server configuration object.
|
||||
type ServerConfiguration struct {
|
||||
// URL defines where to listen at e.g. tcp://[::]:502
|
||||
URL string
|
||||
// Timeout sets the idle session timeout (client connections will
|
||||
// be closed if idle for this long)
|
||||
Timeout time.Duration
|
||||
// MaxClients sets the maximum number of concurrent client connections
|
||||
MaxClients uint
|
||||
// TLSServerCert sets the server-side TLS key pair (tcp+tls only)
|
||||
TLSServerCert *tls.Certificate
|
||||
// TLSClientCAs sets the list of CA certificates used to authenticate
|
||||
// client connections (tcp+tls only). Leaf (i.e. client) certificates can
|
||||
// also be used in case of self-signed certs, or if cert pinning is required.
|
||||
TLSClientCAs *x509.CertPool
|
||||
// Logger provides a custom sink for log messages.
|
||||
// If nil, messages will be written to stdout.
|
||||
Logger *log.Logger
|
||||
}
|
||||
|
||||
// Request object passed to the coil handler.
|
||||
type CoilsRequest struct {
|
||||
ClientAddr string // the source (client) IP address
|
||||
ClientRole string // the client role as encoded in the client certificate (tcp+tls only)
|
||||
UnitId uint8 // the requested unit id (slave id)
|
||||
Addr uint16 // the base coil address requested
|
||||
Quantity uint16 // the number of consecutive coils covered by this request
|
||||
// (first address: Addr, last address: Addr + Quantity - 1)
|
||||
IsWrite bool // true if the request is a write, false if a read
|
||||
Args []bool // a slice of bool values of the coils to be set, ordered
|
||||
// from Addr to Addr + Quantity - 1 (for writes only)
|
||||
}
|
||||
|
||||
// Request object passed to the discrete input handler.
|
||||
type DiscreteInputsRequest struct {
|
||||
ClientAddr string // the source (client) IP address
|
||||
ClientRole string // the client role as encoded in the client certificate (tcp+tls only)
|
||||
UnitId uint8 // the requested unit id (slave id)
|
||||
Addr uint16 // the base discrete input address requested
|
||||
Quantity uint16 // the number of consecutive discrete inputs covered by this request
|
||||
}
|
||||
|
||||
// Request object passed to the holding register handler.
|
||||
type HoldingRegistersRequest struct {
|
||||
ClientAddr string // the source (client) IP address
|
||||
ClientRole string // the client role as encoded in the client certificate (tcp+tls only)
|
||||
UnitId uint8 // the requested unit id (slave id)
|
||||
Addr uint16 // the base register address requested
|
||||
Quantity uint16 // the number of consecutive registers covered by this request
|
||||
IsWrite bool // true if the request is a write, false if a read
|
||||
Args []uint16 // a slice of register values to be set, ordered from
|
||||
// Addr to Addr + Quantity - 1 (for writes only)
|
||||
}
|
||||
|
||||
// Request object passed to the input register handler.
|
||||
type InputRegistersRequest struct {
|
||||
ClientAddr string // the source (client) IP address
|
||||
ClientRole string // the client role as encoded in the client certificate (tcp+tls only)
|
||||
UnitId uint8 // the requested unit id (slave id)
|
||||
Addr uint16 // the base register address requested
|
||||
Quantity uint16 // the number of consecutive registers covered by this request
|
||||
}
|
||||
|
||||
// The RequestHandler interface should be implemented by the handler
|
||||
// object passed to NewServer (see reqHandler in NewServer()).
|
||||
// After decoding and validating an incoming request, the server will
|
||||
// invoke the appropriate handler function, depending on the function code
|
||||
// of the request.
|
||||
type RequestHandler interface {
|
||||
// HandleCoils handles the read coils (0x01), write single coil (0x05)
|
||||
// and write multiple coils (0x0f) function codes.
|
||||
// A CoilsRequest object is passed to the handler (see above).
|
||||
//
|
||||
// Expected return values:
|
||||
// - res: a slice of bools containing the coil values to be sent to back
|
||||
// to the client (only sent for reads),
|
||||
// - err: either nil if no error occurred, a modbus error (see
|
||||
// mapErrorToExceptionCode() in modbus.go for a complete list),
|
||||
// or any other error.
|
||||
// If nil, a positive modbus response is sent back to the client
|
||||
// along with the returned data.
|
||||
// If non-nil, a negative modbus response is sent back, with the
|
||||
// exception code set depending on the error
|
||||
// (again, see mapErrorToExceptionCode()).
|
||||
HandleCoils (req *CoilsRequest) (res []bool, err error)
|
||||
|
||||
// HandleDiscreteInputs handles the read discrete inputs (0x02) function code.
|
||||
// A DiscreteInputsRequest oibject is passed to the handler (see above).
|
||||
//
|
||||
// Expected return values:
|
||||
// - res: a slice of bools containing the discrete input values to be
|
||||
// sent back to the client,
|
||||
// - err: either nil if no error occurred, a modbus error (see
|
||||
// mapErrorToExceptionCode() in modbus.go for a complete list),
|
||||
// or any other error.
|
||||
HandleDiscreteInputs (req *DiscreteInputsRequest) (res []bool, err error)
|
||||
|
||||
// HandleHoldingRegisters handles the read holding registers (0x03),
|
||||
// write single register (0x06) and write multiple registers (0x10).
|
||||
// A HoldingRegistersRequest object is passed to the handler (see above).
|
||||
//
|
||||
// Expected return values:
|
||||
// - res: a slice of uint16 containing the register values to be sent
|
||||
// to back to the client (only sent for reads),
|
||||
// - err: either nil if no error occurred, a modbus error (see
|
||||
// mapErrorToExceptionCode() in modbus.go for a complete list),
|
||||
// or any other error.
|
||||
HandleHoldingRegisters (req *HoldingRegistersRequest) (res []uint16, err error)
|
||||
|
||||
// HandleInputRegisters handles the read input registers (0x04) function code.
|
||||
// An InputRegistersRequest object is passed to the handler (see above).
|
||||
//
|
||||
// Expected return values:
|
||||
// - res: a slice of uint16 containing the register values to be sent
|
||||
// back to the client,
|
||||
// - err: either nil if no error occurred, a modbus error (see
|
||||
// mapErrorToExceptionCode() in modbus.go for a complete list),
|
||||
// or any other error.
|
||||
HandleInputRegisters (req *InputRegistersRequest) (res []uint16, err error)
|
||||
}
|
||||
|
||||
// Modbus server object.
|
||||
type ModbusServer struct {
|
||||
conf ServerConfiguration
|
||||
logger *logger
|
||||
lock sync.Mutex
|
||||
started bool
|
||||
handler RequestHandler
|
||||
tcpListener net.Listener
|
||||
tcpClients []net.Conn
|
||||
transportType transportType
|
||||
}
|
||||
|
||||
// Returns a new modbus server.
|
||||
// reqHandler should be a user-provided handler object satisfying the RequestHandler
|
||||
// interface.
|
||||
func NewServer(conf *ServerConfiguration, reqHandler RequestHandler) (
|
||||
ms *ModbusServer, err error) {
|
||||
var serverType string
|
||||
var splitURL []string
|
||||
|
||||
ms = &ModbusServer{
|
||||
conf: *conf,
|
||||
handler: reqHandler,
|
||||
}
|
||||
|
||||
splitURL = strings.SplitN(ms.conf.URL, "://", 2)
|
||||
if len(splitURL) == 2 {
|
||||
serverType = splitURL[0]
|
||||
ms.conf.URL = splitURL[1]
|
||||
}
|
||||
|
||||
ms.logger = newLogger(
|
||||
fmt.Sprintf("modbus-server(%s)", ms.conf.URL), ms.conf.Logger)
|
||||
|
||||
if ms.conf.URL == "" {
|
||||
ms.logger.Errorf("missing host part in URL '%s'", conf.URL)
|
||||
err = ErrConfigurationError
|
||||
return
|
||||
}
|
||||
|
||||
switch serverType {
|
||||
case "tcp":
|
||||
if ms.conf.Timeout == 0 {
|
||||
ms.conf.Timeout = 120 * time.Second
|
||||
}
|
||||
|
||||
if ms.conf.MaxClients == 0 {
|
||||
ms.conf.MaxClients = 10
|
||||
}
|
||||
|
||||
ms.transportType = modbusTCP
|
||||
|
||||
case "tcp+tls":
|
||||
if ms.conf.Timeout == 0 {
|
||||
ms.conf.Timeout = 120 * time.Second
|
||||
}
|
||||
|
||||
if ms.conf.MaxClients == 0 {
|
||||
ms.conf.MaxClients = 10
|
||||
}
|
||||
|
||||
// expect a server-side certificate
|
||||
if ms.conf.TLSServerCert == nil {
|
||||
ms.logger.Errorf("missing server certificate")
|
||||
err = ErrConfigurationError
|
||||
return
|
||||
}
|
||||
|
||||
// expect a CertPool object containing at least 1 CA or
|
||||
// leaf certificate to validate client-side certificates
|
||||
if ms.conf.TLSClientCAs == nil {
|
||||
ms.logger.Errorf("missing CA/client certificates")
|
||||
err = ErrConfigurationError
|
||||
return
|
||||
}
|
||||
|
||||
ms.transportType = modbusTCPOverTLS
|
||||
|
||||
default:
|
||||
err = ErrConfigurationError
|
||||
return
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// Starts accepting client connections.
|
||||
func (ms *ModbusServer) Start() (err error) {
|
||||
ms.lock.Lock()
|
||||
defer ms.lock.Unlock()
|
||||
|
||||
if ms.started {
|
||||
return
|
||||
}
|
||||
|
||||
switch ms.transportType {
|
||||
case modbusTCP, modbusTCPOverTLS:
|
||||
// bind to a TCP socket
|
||||
ms.tcpListener, err = net.Listen("tcp", ms.conf.URL)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
// accept client connections in a goroutine
|
||||
go ms.acceptTCPClients()
|
||||
|
||||
default:
|
||||
err = ErrConfigurationError
|
||||
return
|
||||
}
|
||||
|
||||
ms.started = true
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// Stops accepting new client connections and closes any active session.
|
||||
func (ms *ModbusServer) Stop() (err error) {
|
||||
ms.lock.Lock()
|
||||
defer ms.lock.Unlock()
|
||||
|
||||
if !ms.started {
|
||||
return
|
||||
}
|
||||
|
||||
ms.started = false
|
||||
|
||||
if ms.transportType == modbusTCP || ms.transportType == modbusTCPOverTLS {
|
||||
// close the server socket if we're listening over TCP
|
||||
err = ms.tcpListener.Close()
|
||||
|
||||
// close all active TCP clients
|
||||
for _, sock := range ms.tcpClients{
|
||||
sock.Close()
|
||||
}
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// Accepts new client connections if the configured connection limit allows it.
|
||||
// Each connection is served from a dedicated goroutine to allow for concurrent
|
||||
// connections.
|
||||
func (ms *ModbusServer) acceptTCPClients() {
|
||||
var sock net.Conn
|
||||
var err error
|
||||
var accepted bool
|
||||
|
||||
for {
|
||||
sock, err = ms.tcpListener.Accept()
|
||||
if err != nil {
|
||||
// if the server socket has just been closed, return here as
|
||||
// this goroutine isn't going to see any new client connection
|
||||
if errors.Is(err, net.ErrClosed) {
|
||||
return
|
||||
}
|
||||
ms.logger.Warningf("failed to accept client connection: %v", err)
|
||||
continue
|
||||
}
|
||||
|
||||
ms.lock.Lock()
|
||||
// apply a connection limit
|
||||
if ms.started && uint(len(ms.tcpClients)) < ms.conf.MaxClients {
|
||||
accepted = true
|
||||
// add the new client connection to the pool
|
||||
ms.tcpClients = append(ms.tcpClients, sock)
|
||||
} else {
|
||||
accepted = false
|
||||
}
|
||||
ms.lock.Unlock()
|
||||
|
||||
if accepted {
|
||||
// spin a client handler goroutine to serve the new client
|
||||
go ms.handleTCPClient(sock)
|
||||
} else {
|
||||
ms.logger.Warningf("max. number of concurrent connections " +
|
||||
"reached, rejecting %v", sock.RemoteAddr())
|
||||
// discard the connection
|
||||
sock.Close()
|
||||
}
|
||||
}
|
||||
|
||||
// never reached
|
||||
return
|
||||
}
|
||||
|
||||
// Handles a TCP client connection.
|
||||
// Once handleTransport() returns (i.e. the connection has either closed, timed
|
||||
// out, or an unrecoverable error happened), the TCP socket is closed and removed
|
||||
// from the list of active client connections.
|
||||
func (ms *ModbusServer) handleTCPClient(sock net.Conn) {
|
||||
var err error
|
||||
var clientRole string
|
||||
var tlsSock net.Conn
|
||||
|
||||
switch ms.transportType {
|
||||
case modbusTCP:
|
||||
// serve modbus requests over the raw TCP connection
|
||||
ms.handleTransport(
|
||||
newTCPTransport(sock, ms.conf.Timeout, ms.conf.Logger),
|
||||
sock.RemoteAddr().String(), "")
|
||||
|
||||
case modbusTCPOverTLS:
|
||||
// start TLS negotiation over the raw TCP connection
|
||||
tlsSock, clientRole, err = ms.startTLS(sock)
|
||||
if err != nil {
|
||||
ms.logger.Warningf("TLS handshake with %s failed: %v",
|
||||
sock.RemoteAddr().String(), err)
|
||||
} else {
|
||||
// serve modbus requests over the TLS tunnel
|
||||
ms.handleTransport(
|
||||
newTCPTransport(tlsSock, ms.conf.Timeout, ms.conf.Logger),
|
||||
sock.RemoteAddr().String(), clientRole)
|
||||
}
|
||||
|
||||
default:
|
||||
ms.logger.Errorf("unimplemented transport type %v", ms.transportType)
|
||||
}
|
||||
|
||||
// once done, remove our connection from the list of active client conns
|
||||
ms.lock.Lock()
|
||||
for i := range ms.tcpClients {
|
||||
if ms.tcpClients[i] == sock {
|
||||
ms.tcpClients[i] = ms.tcpClients[len(ms.tcpClients)-1]
|
||||
ms.tcpClients = ms.tcpClients[:len(ms.tcpClients)-1]
|
||||
break
|
||||
}
|
||||
}
|
||||
ms.lock.Unlock()
|
||||
|
||||
// close the connection
|
||||
sock.Close()
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// For each request read from the transport, performs decoding and validation,
|
||||
// calls the user-provided handler, then encodes and writes the response
|
||||
// to the transport.
|
||||
func (ms *ModbusServer) handleTransport(t transport, clientAddr string, clientRole string) {
|
||||
var req *pdu
|
||||
var res *pdu
|
||||
var err error
|
||||
var addr uint16
|
||||
var quantity uint16
|
||||
|
||||
for {
|
||||
req, err = t.ReadRequest()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
switch req.functionCode {
|
||||
case fcReadCoils, fcReadDiscreteInputs:
|
||||
var coils []bool
|
||||
var resCount int
|
||||
|
||||
if len(req.payload) != 4 {
|
||||
err = ErrProtocolError
|
||||
break
|
||||
}
|
||||
|
||||
// decode address and quantity fields
|
||||
addr = bytesToUint16(BIG_ENDIAN, req.payload[0:2])
|
||||
quantity = bytesToUint16(BIG_ENDIAN, req.payload[2:4])
|
||||
|
||||
// ensure the reply never exceeds the maximum PDU length and we
|
||||
// never read past 0xffff
|
||||
if quantity > 2000 || quantity == 0 {
|
||||
err = ErrProtocolError
|
||||
break
|
||||
}
|
||||
if uint32(addr) + uint32(quantity) - 1 > 0xffff {
|
||||
err = ErrIllegalDataAddress
|
||||
break
|
||||
}
|
||||
|
||||
// invoke the appropriate handler
|
||||
if req.functionCode == fcReadCoils {
|
||||
coils, err = ms.handler.HandleCoils(&CoilsRequest{
|
||||
ClientAddr: clientAddr,
|
||||
ClientRole: clientRole,
|
||||
UnitId: req.unitId,
|
||||
Addr: addr,
|
||||
Quantity: quantity,
|
||||
IsWrite: false,
|
||||
Args: nil,
|
||||
})
|
||||
} else {
|
||||
coils, err = ms.handler.HandleDiscreteInputs(
|
||||
&DiscreteInputsRequest{
|
||||
ClientAddr: clientAddr,
|
||||
ClientRole: clientRole,
|
||||
UnitId: req.unitId,
|
||||
Addr: addr,
|
||||
Quantity: quantity,
|
||||
})
|
||||
}
|
||||
resCount = len(coils)
|
||||
|
||||
// make sure the handler returned the expected number of items
|
||||
if err == nil && resCount != int(quantity) {
|
||||
ms.logger.Errorf("handler returned %v bools, " +
|
||||
"expected %v", resCount, quantity)
|
||||
err = ErrServerDeviceFailure
|
||||
break
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
break
|
||||
}
|
||||
|
||||
// assemble a response PDU
|
||||
res = &pdu{
|
||||
unitId: req.unitId,
|
||||
functionCode: req.functionCode,
|
||||
payload: []byte{0},
|
||||
}
|
||||
|
||||
// byte count (1 byte for 8 coils)
|
||||
res.payload[0] = uint8(resCount / 8)
|
||||
if resCount % 8 != 0 {
|
||||
res.payload[0]++
|
||||
}
|
||||
|
||||
// coil values
|
||||
res.payload = append(res.payload, encodeBools(coils)...)
|
||||
|
||||
case fcWriteSingleCoil:
|
||||
if len(req.payload) != 4 {
|
||||
err = ErrProtocolError
|
||||
break
|
||||
}
|
||||
|
||||
// decode the address field
|
||||
addr = bytesToUint16(BIG_ENDIAN, req.payload[0:2])
|
||||
|
||||
// validate the value field (should be either 0xff00 or 0x0000)
|
||||
if ((req.payload[2] != 0xff && req.payload[2] != 0x00) ||
|
||||
req.payload[3] != 0x00) {
|
||||
err = ErrProtocolError
|
||||
break
|
||||
}
|
||||
|
||||
// invoke the coil handler
|
||||
_, err = ms.handler.HandleCoils(&CoilsRequest{
|
||||
ClientAddr: clientAddr,
|
||||
ClientRole: clientRole,
|
||||
UnitId: req.unitId,
|
||||
Addr: addr,
|
||||
Quantity: 1, // request for a single coil
|
||||
IsWrite: true, // this is a write request
|
||||
Args: []bool{(req.payload[2] == 0xff)},
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
break
|
||||
}
|
||||
|
||||
// assemble a response PDU
|
||||
res = &pdu{
|
||||
unitId: req.unitId,
|
||||
functionCode: req.functionCode,
|
||||
}
|
||||
|
||||
// echo the address and value in the response
|
||||
res.payload = append(res.payload,
|
||||
uint16ToBytes(BIG_ENDIAN, addr)...)
|
||||
res.payload = append(res.payload,
|
||||
req.payload[2], req.payload[3])
|
||||
|
||||
case fcWriteMultipleCoils:
|
||||
var expectedLen int
|
||||
|
||||
if len(req.payload) < 6 {
|
||||
err = ErrProtocolError
|
||||
break
|
||||
}
|
||||
|
||||
// decode address and quantity fields
|
||||
addr = bytesToUint16(BIG_ENDIAN, req.payload[0:2])
|
||||
quantity = bytesToUint16(BIG_ENDIAN, req.payload[2:4])
|
||||
|
||||
// ensure the reply never exceeds the maximum PDU length and we
|
||||
// never read past 0xffff
|
||||
if quantity > 0x7b0 || quantity == 0 {
|
||||
err = ErrProtocolError
|
||||
break
|
||||
}
|
||||
if uint32(addr) + uint32(quantity) - 1 > 0xffff {
|
||||
err = ErrIllegalDataAddress
|
||||
break
|
||||
}
|
||||
|
||||
// validate the byte count field (1 byte for 8 coils)
|
||||
expectedLen = int(quantity) / 8
|
||||
if quantity % 8 != 0 {
|
||||
expectedLen++
|
||||
}
|
||||
|
||||
if req.payload[4] != uint8(expectedLen) {
|
||||
err = ErrProtocolError
|
||||
break
|
||||
}
|
||||
|
||||
// make sure we have enough bytes
|
||||
if len(req.payload) - 5 != expectedLen {
|
||||
err = ErrProtocolError
|
||||
break
|
||||
}
|
||||
|
||||
// invoke the coil handler
|
||||
_, err = ms.handler.HandleCoils(&CoilsRequest{
|
||||
ClientAddr: clientAddr,
|
||||
ClientRole: clientRole,
|
||||
UnitId: req.unitId,
|
||||
Addr: addr,
|
||||
Quantity: quantity,
|
||||
IsWrite: true, // this is a write request
|
||||
Args: decodeBools(quantity, req.payload[5:]),
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
break
|
||||
}
|
||||
|
||||
// assemble a response PDU
|
||||
res = &pdu{
|
||||
unitId: req.unitId,
|
||||
functionCode: req.functionCode,
|
||||
}
|
||||
|
||||
// echo the address and quantity in the response
|
||||
res.payload = append(res.payload,
|
||||
uint16ToBytes(BIG_ENDIAN, addr)...)
|
||||
res.payload = append(res.payload,
|
||||
uint16ToBytes(BIG_ENDIAN, quantity)...)
|
||||
|
||||
case fcReadHoldingRegisters, fcReadInputRegisters:
|
||||
var regs []uint16
|
||||
var resCount int
|
||||
|
||||
if len(req.payload) != 4 {
|
||||
err = ErrProtocolError
|
||||
break
|
||||
}
|
||||
|
||||
// decode address and quantity fields
|
||||
addr = bytesToUint16(BIG_ENDIAN, req.payload[0:2])
|
||||
quantity = bytesToUint16(BIG_ENDIAN, req.payload[2:4])
|
||||
|
||||
// ensure the reply never exceeds the maximum PDU length and we
|
||||
// never read past 0xffff
|
||||
if quantity > 0x007d || quantity == 0 {
|
||||
err = ErrProtocolError
|
||||
break
|
||||
}
|
||||
if uint32(addr) + uint32(quantity) - 1 > 0xffff {
|
||||
err = ErrIllegalDataAddress
|
||||
break
|
||||
}
|
||||
|
||||
// invoke the appropriate handler
|
||||
if req.functionCode == fcReadHoldingRegisters {
|
||||
regs, err = ms.handler.HandleHoldingRegisters(
|
||||
&HoldingRegistersRequest{
|
||||
ClientAddr: clientAddr,
|
||||
ClientRole: clientRole,
|
||||
UnitId: req.unitId,
|
||||
Addr: addr,
|
||||
Quantity: quantity,
|
||||
IsWrite: false,
|
||||
Args: nil,
|
||||
})
|
||||
} else {
|
||||
regs, err = ms.handler.HandleInputRegisters(
|
||||
&InputRegistersRequest{
|
||||
ClientAddr: clientAddr,
|
||||
ClientRole: clientRole,
|
||||
UnitId: req.unitId,
|
||||
Addr: addr,
|
||||
Quantity: quantity,
|
||||
})
|
||||
}
|
||||
resCount = len(regs)
|
||||
|
||||
// make sure the handler returned the expected number of items
|
||||
if err == nil && resCount != int(quantity) {
|
||||
ms.logger.Errorf("handler returned %v 16-bit values, " +
|
||||
"expected %v", resCount, quantity)
|
||||
err = ErrServerDeviceFailure
|
||||
break
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
break
|
||||
}
|
||||
|
||||
// assemble a response PDU
|
||||
res = &pdu{
|
||||
unitId: req.unitId,
|
||||
functionCode: req.functionCode,
|
||||
payload: []byte{0},
|
||||
}
|
||||
|
||||
// byte count (2 bytes per register)
|
||||
res.payload[0] = uint8(resCount * 2)
|
||||
|
||||
// register values
|
||||
res.payload = append(res.payload,
|
||||
uint16sToBytes(BIG_ENDIAN, regs)...)
|
||||
|
||||
case fcWriteSingleRegister:
|
||||
var value uint16
|
||||
|
||||
if len(req.payload) != 4 {
|
||||
err = ErrProtocolError
|
||||
break
|
||||
}
|
||||
|
||||
// decode address and value fields
|
||||
addr = bytesToUint16(BIG_ENDIAN, req.payload[0:2])
|
||||
value = bytesToUint16(BIG_ENDIAN, req.payload[2:4])
|
||||
|
||||
// invoke the handler
|
||||
_, err = ms.handler.HandleHoldingRegisters(
|
||||
&HoldingRegistersRequest{
|
||||
ClientAddr: clientAddr,
|
||||
ClientRole: clientRole,
|
||||
UnitId: req.unitId,
|
||||
Addr: addr,
|
||||
Quantity: 1, // request for a single register
|
||||
IsWrite: true, // request is a write
|
||||
Args: []uint16{value},
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
break
|
||||
}
|
||||
|
||||
// assemble a response PDU
|
||||
res = &pdu{
|
||||
unitId: req.unitId,
|
||||
functionCode: req.functionCode,
|
||||
}
|
||||
|
||||
// echo the address and value in the response
|
||||
res.payload = append(res.payload,
|
||||
uint16ToBytes(BIG_ENDIAN, addr)...)
|
||||
res.payload = append(res.payload,
|
||||
uint16ToBytes(BIG_ENDIAN, value)...)
|
||||
|
||||
case fcWriteMultipleRegisters:
|
||||
var expectedLen int
|
||||
|
||||
if len(req.payload) < 6 {
|
||||
err = ErrProtocolError
|
||||
break
|
||||
}
|
||||
|
||||
// decode address and quantity fields
|
||||
addr = bytesToUint16(BIG_ENDIAN, req.payload[0:2])
|
||||
quantity = bytesToUint16(BIG_ENDIAN, req.payload[2:4])
|
||||
|
||||
// ensure the reply never exceeds the maximum PDU length and we
|
||||
// never read past 0xffff
|
||||
if quantity > 0x007b || quantity == 0 {
|
||||
err = ErrProtocolError
|
||||
break
|
||||
}
|
||||
if uint32(addr) + uint32(quantity) - 1 > 0xffff {
|
||||
err = ErrIllegalDataAddress
|
||||
break
|
||||
}
|
||||
|
||||
// validate the byte count field (2 bytes per register)
|
||||
expectedLen = int(quantity) * 2
|
||||
|
||||
if req.payload[4] != uint8(expectedLen) {
|
||||
err = ErrProtocolError
|
||||
break
|
||||
}
|
||||
|
||||
// make sure we have enough bytes
|
||||
if len(req.payload) - 5 != expectedLen {
|
||||
err = ErrProtocolError
|
||||
break
|
||||
}
|
||||
|
||||
// invoke the holding register handler
|
||||
_, err = ms.handler.HandleHoldingRegisters(
|
||||
&HoldingRegistersRequest{
|
||||
ClientAddr: clientAddr,
|
||||
ClientRole: clientRole,
|
||||
UnitId: req.unitId,
|
||||
Addr: addr,
|
||||
Quantity: quantity,
|
||||
IsWrite: true, // this is a write request
|
||||
Args: bytesToUint16s(BIG_ENDIAN, req.payload[5:]),
|
||||
})
|
||||
if err != nil {
|
||||
break
|
||||
}
|
||||
|
||||
// assemble a response PDU
|
||||
res = &pdu{
|
||||
unitId: req.unitId,
|
||||
functionCode: req.functionCode,
|
||||
}
|
||||
|
||||
// echo the address and quantity in the response
|
||||
res.payload = append(res.payload,
|
||||
uint16ToBytes(BIG_ENDIAN, addr)...)
|
||||
res.payload = append(res.payload,
|
||||
uint16ToBytes(BIG_ENDIAN, quantity)...)
|
||||
|
||||
default:
|
||||
res = &pdu{
|
||||
// reply with the request target unit ID
|
||||
unitId: req.unitId,
|
||||
// set the error bit
|
||||
functionCode: (0x80 | req.functionCode),
|
||||
// set the exception code to illegal function to indicate that
|
||||
// the server does not know how to handle this function code.
|
||||
payload: []byte{exIllegalFunction},
|
||||
}
|
||||
}
|
||||
|
||||
// if there was no error processing the request but the response is nil
|
||||
// (which should never happen), emit a server failure exception code
|
||||
// and log an error
|
||||
if err == nil && res == nil {
|
||||
err = ErrServerDeviceFailure
|
||||
ms.logger.Errorf("internal server error (req: %v, res: %v, err: %v)",
|
||||
req, res, err)
|
||||
}
|
||||
|
||||
// map go errors to modbus errors, unless the error is a protocol error,
|
||||
// in which case close the transport and return.
|
||||
if err != nil {
|
||||
if err == ErrProtocolError {
|
||||
ms.logger.Warningf(
|
||||
"protocol error, closing link (client address: '%s')",
|
||||
clientAddr)
|
||||
t.Close()
|
||||
return
|
||||
} else {
|
||||
res = &pdu{
|
||||
unitId: req.unitId,
|
||||
functionCode: (0x80 | req.functionCode),
|
||||
payload: []byte{mapErrorToExceptionCode(err)},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// write the response to the transport
|
||||
err = t.WriteResponse(res)
|
||||
if err != nil {
|
||||
ms.logger.Warningf("failed to write response: %v", err)
|
||||
}
|
||||
|
||||
// avoid holding on to stale data
|
||||
req = nil
|
||||
res = nil
|
||||
}
|
||||
|
||||
// never reached
|
||||
return
|
||||
}
|
||||
|
||||
// startTLS performs a full TLS handshake (with client authentication) on tcpSock
|
||||
// and returns a 'wrapped' clear-text socket suitable for use by the TCP transport.
|
||||
func (ms *ModbusServer) startTLS(tcpSock net.Conn) (
|
||||
tlsSock *tls.Conn, clientRole string, err error) {
|
||||
var connState tls.ConnectionState
|
||||
|
||||
// set a 30s timeout for the TLS handshake to complete
|
||||
err = tcpSock.SetDeadline(time.Now().Add(30 * time.Second))
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
// start TLS negotiation over the raw TCP connection
|
||||
tlsSock = tls.Server(tcpSock, &tls.Config{
|
||||
Certificates: []tls.Certificate{
|
||||
*ms.conf.TLSServerCert,
|
||||
},
|
||||
ClientCAs: ms.conf.TLSClientCAs,
|
||||
// require a valid (verified) certificate from the client
|
||||
// (see R-06, R-08 and R-10 of the MBAPS spec)
|
||||
ClientAuth: tls.RequireAndVerifyClientCert,
|
||||
// mandate TLSv1.2 or higher (see R-01 of the MBAPS spec)
|
||||
MinVersion: tls.VersionTLS12,
|
||||
})
|
||||
|
||||
// complete the full TLS handshake (with client cert validation)
|
||||
err = tlsSock.Handshake()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
// look for and extract the client's role, if any
|
||||
connState = tlsSock.ConnectionState()
|
||||
if len(connState.PeerCertificates) == 0 {
|
||||
err = errors.New("no client certificate received")
|
||||
return
|
||||
}
|
||||
// From the tls.ConnectionState doc:
|
||||
// "The first element is the leaf certificate that the connection is
|
||||
// verified against."
|
||||
clientRole = ms.extractRole(connState.PeerCertificates[0])
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// extractRole looks for Modbus Role extensions in a certificate and returns the
|
||||
// role as a string.
|
||||
// If no role extension is found, a nil string is returned (R-23).
|
||||
// If multiple or invalid role extensions are found, a nil string is returned (R-65, R-22).
|
||||
func (ms *ModbusServer) extractRole(cert *x509.Certificate) (role string) {
|
||||
var err error
|
||||
var found bool
|
||||
var badCert bool
|
||||
|
||||
// walk through all extensions looking for Modbus Role OIDs
|
||||
for _, ext := range cert.Extensions {
|
||||
if ext.Id.Equal(modbusRoleOID) {
|
||||
|
||||
// there must be only one role extension per cert (R-65)
|
||||
if found {
|
||||
ms.logger.Warning("client certificate contains more than one role OIDs")
|
||||
badCert = true
|
||||
break
|
||||
}
|
||||
found = true
|
||||
|
||||
// the role extension must use UTF8String encoding (R-22)
|
||||
// (the ASN1 tag for UTF8String is 0x0c)
|
||||
if len(ext.Value) < 2 || ext.Value[0] != 0x0c {
|
||||
badCert = true
|
||||
break
|
||||
}
|
||||
|
||||
// extract the ASN1 string
|
||||
_, err = asn1.Unmarshal(ext.Value, &role)
|
||||
if err != nil {
|
||||
ms.logger.Warningf("failed to decode Modbus Role extension: %v", err)
|
||||
badCert = true
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// blank the role if we found more than one Role extension
|
||||
if badCert {
|
||||
role = ""
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
658
server_tcp_test.go
Normal file
658
server_tcp_test.go
Normal file
@@ -0,0 +1,658 @@
|
||||
package modbus
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func TestTCPServerWithConcurrentConnections(t *testing.T) {
|
||||
var server *ModbusServer
|
||||
var err error
|
||||
var coils []bool
|
||||
var c1 *ModbusClient
|
||||
var c2 *ModbusClient
|
||||
var c3 *ModbusClient
|
||||
var th *tcpTestHandler
|
||||
|
||||
th = &tcpTestHandler{}
|
||||
|
||||
server, err = NewServer(&ServerConfiguration{
|
||||
URL: "tcp://localhost:5502",
|
||||
MaxClients: 2,
|
||||
}, th)
|
||||
if err != nil {
|
||||
t.Errorf("failed to create server: %v", err)
|
||||
}
|
||||
|
||||
err = server.Start()
|
||||
if err != nil {
|
||||
t.Errorf("failed to start server: %v", err)
|
||||
}
|
||||
|
||||
// create 3 modbus clients
|
||||
c1, err = NewClient(&ClientConfiguration{
|
||||
URL: "tcp://localhost:5502",
|
||||
})
|
||||
if err != nil {
|
||||
t.Errorf("failed to create client: %v", err)
|
||||
}
|
||||
c2, err = NewClient(&ClientConfiguration{
|
||||
URL: "tcp://localhost:5502",
|
||||
})
|
||||
if err != nil {
|
||||
t.Errorf("failed to create client: %v", err)
|
||||
}
|
||||
c3, err = NewClient(&ClientConfiguration{
|
||||
URL: "tcp://localhost:5502",
|
||||
})
|
||||
if err != nil {
|
||||
t.Errorf("failed to create client: %v", err)
|
||||
}
|
||||
|
||||
// the server should have zero client connections so far
|
||||
server.lock.Lock()
|
||||
if len(server.tcpClients) != 0 {
|
||||
t.Errorf("expected server.tcpClients to hold 0 entries, got: %v",
|
||||
len(server.tcpClients))
|
||||
}
|
||||
server.lock.Unlock()
|
||||
|
||||
// connect client #1
|
||||
err = c1.Open()
|
||||
if err != nil {
|
||||
t.Errorf("c1.Connect() should have succeeded, got: %v", err)
|
||||
}
|
||||
c1.SetUnitId(9)
|
||||
|
||||
// the server should have 1 client connection at this point
|
||||
time.Sleep(time.Millisecond)
|
||||
server.lock.Lock()
|
||||
if len(server.tcpClients) != 1 {
|
||||
t.Errorf("expected server.tcpClients to hold 1 entry, got: %v",
|
||||
len(server.tcpClients))
|
||||
}
|
||||
server.lock.Unlock()
|
||||
|
||||
// connect client #2
|
||||
err = c2.Open()
|
||||
if err != nil {
|
||||
t.Errorf("c2.Connect() should have succeeded, got: %v", err)
|
||||
}
|
||||
c2.SetUnitId(9)
|
||||
|
||||
time.Sleep(time.Millisecond)
|
||||
// the server should now have 2 client connections, its maximum allowed
|
||||
server.lock.Lock()
|
||||
if len(server.tcpClients) != 2 {
|
||||
t.Errorf("expected server.tcpClients to hold 2 entries, got: %v",
|
||||
len(server.tcpClients))
|
||||
}
|
||||
server.lock.Unlock()
|
||||
|
||||
// connect client #3
|
||||
err = c3.Open()
|
||||
if err != nil {
|
||||
t.Errorf("c3.Connect() should have succeeded, got: %v", err)
|
||||
}
|
||||
c3.SetUnitId(9)
|
||||
|
||||
// since the previous client was rejected, the active connection count
|
||||
// should stay at 2
|
||||
server.lock.Lock()
|
||||
if len(server.tcpClients) != 2 {
|
||||
t.Errorf("expected server.tcpClients to hold 2 entries, got: %v",
|
||||
len(server.tcpClients))
|
||||
}
|
||||
server.lock.Unlock()
|
||||
|
||||
// c1 and c2 should both be able to make requests while c3 should error out
|
||||
// as it has been disconnected (conn closed)
|
||||
coils, err = c1.ReadCoils(0x0000, 2)
|
||||
if err != nil {
|
||||
t.Errorf("c1.ReadCoils() should have succeeded, got: %v", err)
|
||||
}
|
||||
if coils[0] == true || coils[1] == true {
|
||||
t.Errorf("expected {false, false}, got: %v", coils)
|
||||
}
|
||||
|
||||
coils, err = c2.ReadCoils(0x0003, 5)
|
||||
if err != nil {
|
||||
t.Errorf("c2.ReadCoils() should have succeeded, got: %v", err)
|
||||
}
|
||||
if coils[0] != false || coils[1] != false {
|
||||
t.Errorf("expected {false, false}, got: %v", coils)
|
||||
}
|
||||
|
||||
_, err = c3.ReadCoil(0x0001)
|
||||
if err == nil {
|
||||
t.Errorf("c3.ReadCoil() should have failed")
|
||||
}
|
||||
|
||||
// close c2 and make sure the connection is freed
|
||||
c2.Close()
|
||||
time.Sleep(time.Millisecond)
|
||||
server.lock.Lock()
|
||||
if len(server.tcpClients) != 1 {
|
||||
t.Errorf("expected server.tcpClients to hold 1 entry, got: %v",
|
||||
len(server.tcpClients))
|
||||
}
|
||||
server.lock.Unlock()
|
||||
|
||||
// reconnect c2
|
||||
err = c2.Open()
|
||||
if err != nil {
|
||||
t.Errorf("c2.Open should have succeeded, got: %v", err)
|
||||
}
|
||||
|
||||
// write to the coil at address #1
|
||||
err = c2.WriteCoil(0x0001, true)
|
||||
if err != nil {
|
||||
t.Errorf("c2.WriteCoil() should have succeeded, got: %v", err)
|
||||
}
|
||||
|
||||
server.lock.Lock()
|
||||
if len(server.tcpClients) != 2 {
|
||||
t.Errorf("expected server.tcpClients to hold 2 entries, got: %v",
|
||||
len(server.tcpClients))
|
||||
}
|
||||
server.lock.Unlock()
|
||||
|
||||
// check the coil value with c1
|
||||
coils, err = c1.ReadCoils(0x0000, 2)
|
||||
if err != nil {
|
||||
t.Errorf("c1.ReadCoils() should have succeeded, got: %v", err)
|
||||
}
|
||||
if coils[0] != false || coils[1] != true {
|
||||
t.Errorf("expected {false, true}, got: %v", coils)
|
||||
}
|
||||
|
||||
// close c1 and make sure the connection is freed
|
||||
c1.Close()
|
||||
time.Sleep(time.Millisecond)
|
||||
server.lock.Lock()
|
||||
if len(server.tcpClients) != 1 {
|
||||
t.Errorf("expected server.tcpClients to hold 1 entry, got: %v",
|
||||
len(server.tcpClients))
|
||||
}
|
||||
server.lock.Unlock()
|
||||
|
||||
// stopping the server should disconnect all clients
|
||||
server.Stop()
|
||||
|
||||
time.Sleep(time.Millisecond)
|
||||
server.lock.Lock()
|
||||
if len(server.tcpClients) != 0 {
|
||||
t.Errorf("expected server.tcpClients to hold 0 entries, got: %v",
|
||||
len(server.tcpClients))
|
||||
}
|
||||
server.lock.Unlock()
|
||||
|
||||
// c2 should have been disconnected
|
||||
coils, err = c2.ReadCoils(0x0003, 5)
|
||||
if err == nil {
|
||||
t.Errorf("c2.ReadCoils() should have failed")
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func TestTCPServerCoilsAndDiscreteInputs(t *testing.T) {
|
||||
var server *ModbusServer
|
||||
var err error
|
||||
var coils []bool
|
||||
var dis []bool
|
||||
var client *ModbusClient
|
||||
var th *tcpTestHandler
|
||||
|
||||
th = &tcpTestHandler{}
|
||||
|
||||
server, err = NewServer(&ServerConfiguration{
|
||||
URL: "tcp://localhost:5504",
|
||||
MaxClients: 2,
|
||||
}, th)
|
||||
if err != nil {
|
||||
t.Errorf("failed to create server: %v", err)
|
||||
}
|
||||
|
||||
err = server.Start()
|
||||
if err != nil {
|
||||
t.Errorf("failed to start server: %v", err)
|
||||
}
|
||||
|
||||
client, err = NewClient(&ClientConfiguration{
|
||||
URL: "tcp://localhost:5504",
|
||||
})
|
||||
if err != nil {
|
||||
t.Errorf("failed to create client: %v", err)
|
||||
}
|
||||
|
||||
err = client.Open()
|
||||
if err != nil {
|
||||
t.Errorf("client.Open() should have succeeded, got: %v", err)
|
||||
}
|
||||
client.SetUnitId(9)
|
||||
|
||||
// make sure both coils and discrete inputs are all false/0
|
||||
coils, err = client.ReadCoils(0x0000, 10)
|
||||
if err != nil {
|
||||
t.Errorf("client.ReadCoils() should have succeeded, got: %v", err)
|
||||
}
|
||||
for i := 0; i < 10; i++ {
|
||||
if coils[i] != false {
|
||||
t.Errorf("expected coil at addr 0x%04x to be false", i)
|
||||
}
|
||||
}
|
||||
|
||||
dis, err = client.ReadDiscreteInputs(0x0000, 10)
|
||||
if err != nil {
|
||||
t.Errorf("client.ReadDiscreteInputs() should have succeeded, got: %v", err)
|
||||
}
|
||||
for i := 0; i < 10; i++ {
|
||||
if dis[i] != false {
|
||||
t.Errorf("expected discrete input at addr 0x%04x to be false", i)
|
||||
}
|
||||
}
|
||||
|
||||
// set discrete inputs to random values
|
||||
th.di = [10]bool{
|
||||
false, false, false, true, false, true, true, true, true, true,
|
||||
}
|
||||
|
||||
// read the discrete inputs again
|
||||
dis, err = client.ReadDiscreteInputs(0x0000, 10)
|
||||
if err != nil {
|
||||
t.Errorf("client.ReadDiscreteInput() should have succeeded, got: %v", err)
|
||||
}
|
||||
for i, b := range [10]bool{
|
||||
false, false, false, true, false, true, true, true, true, true,
|
||||
} {
|
||||
if dis[i] != b {
|
||||
t.Errorf("expected discrete input at addr 0x%04x to be %v", i, b)
|
||||
}
|
||||
}
|
||||
|
||||
// reading past the array size should return ErrIllegalDataAddress
|
||||
_, err = client.ReadDiscreteInputs(0x000a, 1)
|
||||
if err != ErrIllegalDataAddress {
|
||||
t.Errorf("expected ErrIllegalDataAddress, got: %v", err)
|
||||
}
|
||||
_, err = client.ReadCoils(0x000a, 1)
|
||||
if err != ErrIllegalDataAddress {
|
||||
t.Errorf("expected ErrIllegalDataAddress, got: %v", err)
|
||||
}
|
||||
_, err = client.ReadDiscreteInputs(0x8, 3)
|
||||
if err != ErrIllegalDataAddress {
|
||||
t.Errorf("expected ErrIllegalDataAddress, got: %v", err)
|
||||
}
|
||||
_, err = client.ReadCoils(0x8, 3)
|
||||
if err != ErrIllegalDataAddress {
|
||||
t.Errorf("expected ErrIllegalDataAddress, got: %v", err)
|
||||
}
|
||||
|
||||
// the coils shouldn't have changed
|
||||
coils, err = client.ReadCoils(0x0000, 10)
|
||||
if err != nil {
|
||||
t.Errorf("client.ReadCoils() should have succeeded, got: %v", err)
|
||||
}
|
||||
for i := 0; i < 10; i++ {
|
||||
if coils[i] != false {
|
||||
t.Errorf("expected coil at addr 0x%04x to be false", i)
|
||||
}
|
||||
}
|
||||
|
||||
// write to a single coil
|
||||
err = client.WriteCoil(0x0004, true)
|
||||
if err != nil {
|
||||
t.Errorf("client.WriteCoil() should have succeeded, got: %v", err)
|
||||
}
|
||||
|
||||
// make sure it has been written to
|
||||
coils, err = client.ReadCoils(0x0003, 3)
|
||||
if err != nil {
|
||||
t.Errorf("client.ReadCoils() should have succeeded, got: %v", err)
|
||||
}
|
||||
for i, v := range []bool{false, true, false,} {
|
||||
if coils[i] != v {
|
||||
t.Errorf("expected coil at addr 0x%04x to be %v", 3 + i, v)
|
||||
}
|
||||
}
|
||||
|
||||
// write to multiple coils at once
|
||||
err = client.WriteCoils(0x0005, []bool{
|
||||
true, false, true, true,
|
||||
})
|
||||
if err != nil {
|
||||
t.Errorf("client.WriteCoils() should have succeeded, got: %v", err)
|
||||
}
|
||||
|
||||
// make sure the write went through
|
||||
coils, err = client.ReadCoils(0x0005, 4)
|
||||
if err != nil {
|
||||
t.Errorf("client.ReadCoils() should have succeeded, got: %v", err)
|
||||
}
|
||||
for i, v := range []bool{true, false, true, true,} {
|
||||
if coils[i] != v {
|
||||
t.Errorf("expected coil at addr 0x%04x to be %v", 3 + i, v)
|
||||
}
|
||||
}
|
||||
|
||||
// switch to another unit ID and make sure both coil and discrete input operations
|
||||
// return ErrIllegalFunction
|
||||
client.SetUnitId(5)
|
||||
err = client.WriteCoils(0x0005, []bool{
|
||||
true, false, true, true,
|
||||
})
|
||||
if err != ErrIllegalFunction {
|
||||
t.Errorf("client.WriteCoils() should have returned ErrIllegalFunction, got: %v", err)
|
||||
}
|
||||
err = client.WriteCoil(0x0005, false)
|
||||
if err != ErrIllegalFunction {
|
||||
t.Errorf("client.WriteCoil() should have returned ErrIllegalFunction, got: %v", err)
|
||||
}
|
||||
coils, err = client.ReadCoils(0x0005, 1)
|
||||
if err != ErrIllegalFunction {
|
||||
t.Errorf("client.ReadCoils() should have returned ErrIllegalFunction, got: %v", err)
|
||||
}
|
||||
coils, err = client.ReadDiscreteInputs(0x0005, 1)
|
||||
if err != ErrIllegalFunction {
|
||||
t.Errorf("client.ReadDiscreteInputs() should have returned ErrIllegalFunction, got: %v", err)
|
||||
}
|
||||
|
||||
client.Close()
|
||||
server.Stop()
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func TestTCPServerHoldingAndInputRegisters(t *testing.T) {
|
||||
var server *ModbusServer
|
||||
var err error
|
||||
var client *ModbusClient
|
||||
var th *tcpTestHandler
|
||||
var regs []uint16
|
||||
|
||||
th = &tcpTestHandler{}
|
||||
|
||||
server, err = NewServer(&ServerConfiguration{
|
||||
URL: "tcp://localhost:5504",
|
||||
MaxClients: 2,
|
||||
}, th)
|
||||
if err != nil {
|
||||
t.Errorf("failed to create server: %v", err)
|
||||
}
|
||||
|
||||
err = server.Start()
|
||||
if err != nil {
|
||||
t.Errorf("failed to start server: %v", err)
|
||||
}
|
||||
|
||||
client, err = NewClient(&ClientConfiguration{
|
||||
URL: "tcp://localhost:5504",
|
||||
})
|
||||
if err != nil {
|
||||
t.Errorf("failed to create client: %v", err)
|
||||
}
|
||||
|
||||
err = client.Open()
|
||||
if err != nil {
|
||||
t.Errorf("client.Open() should have succeeded, got: %v", err)
|
||||
}
|
||||
client.SetUnitId(9)
|
||||
|
||||
// all 10 input registers should be 0x0000
|
||||
regs, err = client.ReadRegisters(0x0000, 10, INPUT_REGISTER)
|
||||
if err != nil {
|
||||
t.Errorf("client.ReadRegisters() should have succeeded, got: %v", err)
|
||||
}
|
||||
for i := 0; i < 10; i++ {
|
||||
if regs[i] != 0x0000 {
|
||||
t.Errorf("expected 0x0000 at position %v, got: 0x%04x", i, regs[i])
|
||||
}
|
||||
}
|
||||
|
||||
// assign some values to the handler's input registers
|
||||
for i := range th.input {
|
||||
th.input[i] = 0xa710 + uint16(i)
|
||||
}
|
||||
|
||||
regs, err = client.ReadRegisters(0x0000, 10, INPUT_REGISTER)
|
||||
if err != nil {
|
||||
t.Errorf("client.ReadRegisters() should have succeeded, got: %v", err)
|
||||
}
|
||||
for i := 0; i < 10; i++ {
|
||||
if regs[i] != 0xa710 + uint16(i) {
|
||||
t.Errorf("expected 0x%04x at position %v, got: 0x%04x",
|
||||
0xa710 + uint16(i), i, regs[i])
|
||||
}
|
||||
}
|
||||
|
||||
// reading addr 0x0009 (the very last register) should succeed
|
||||
regs, err = client.ReadRegisters(0x0009, 1, INPUT_REGISTER)
|
||||
if err != nil {
|
||||
t.Errorf("client.ReadRegisters() should have succeeded, got: %v", err)
|
||||
}
|
||||
if regs[0] != 0xa719 {
|
||||
t.Errorf("expected 0xa719 at address 9, saw: 0x%04x", regs[0])
|
||||
}
|
||||
|
||||
// reading past address 0x000a should fail
|
||||
regs, err = client.ReadRegisters(0x0001, 10, INPUT_REGISTER)
|
||||
if err != ErrIllegalDataAddress {
|
||||
t.Errorf("client.ReadRegisters() should have returned ErrIllegalDataAddress, got: %v", err)
|
||||
}
|
||||
regs, err = client.ReadRegisters(0x0000, 11, INPUT_REGISTER)
|
||||
if err != ErrIllegalDataAddress {
|
||||
t.Errorf("client.ReadRegisters() should have returned ErrIllegalDataAddress, got: %v", err)
|
||||
}
|
||||
|
||||
// all 10 holding registers should still be 0x0000
|
||||
regs, err = client.ReadRegisters(0x0000, 10, HOLDING_REGISTER)
|
||||
if err != nil {
|
||||
t.Errorf("client.ReadRegisters() should have succeeded, got: %v", err)
|
||||
}
|
||||
for i := 0; i < 10; i++ {
|
||||
if regs[i] != 0x0000 {
|
||||
t.Errorf("expected 0x0000 at position %v, got: 0x%04x", i, regs[i])
|
||||
}
|
||||
}
|
||||
|
||||
// write to a single valid register (with opcode 0x06)
|
||||
err = client.WriteRegister(0x0007, 0xfea1)
|
||||
if err != nil {
|
||||
t.Errorf("client.WriteRegister() should have succeeded, got: %v", err)
|
||||
}
|
||||
|
||||
// make sure it has been written to
|
||||
regs, err = client.ReadRegisters(0x0005, 5, HOLDING_REGISTER)
|
||||
if err != nil {
|
||||
t.Errorf("client.ReadRegisters() should have succeeded, got: %v", err)
|
||||
}
|
||||
for i := 0; i < 5; i++ {
|
||||
if i != 2 && regs[i] != 0x0000 {
|
||||
t.Errorf("expected 0x0000 at position %v, got: 0x%04x", i, regs[i])
|
||||
}
|
||||
if i == 2 && regs[i] != 0xfea1 {
|
||||
t.Errorf("expected 0xfea1 at position %v, got: 0x%04x", i, regs[i])
|
||||
}
|
||||
}
|
||||
|
||||
// check values in the handler as well
|
||||
for i := 0; i < 10; i++ {
|
||||
if i != 7 && th.holding[i] != 0x0000 {
|
||||
t.Errorf("expected 0x0000 at handler index %v, got: 0x%04x", i, regs[i])
|
||||
}
|
||||
if i == 7 && th.holding[i] != 0xfea1 {
|
||||
t.Errorf("expected 0xfea1 at handler index %v, got: 0x%04x", i, regs[i])
|
||||
}
|
||||
}
|
||||
|
||||
// write multiple registers at once (with function code 0x10)
|
||||
err = client.WriteRegisters(0x0001, []uint16{
|
||||
0x0c11, 0x0c22, 0x0c33, 0x0c44,
|
||||
0x0c55, 0x0c66, 0x0c77, 0x0c88,
|
||||
0x0c99,
|
||||
})
|
||||
if err != nil {
|
||||
t.Errorf("client.WriteRegisters() should have succeeded, got: %v", err)
|
||||
}
|
||||
|
||||
// write to a single valid register (with opcode 0x06)
|
||||
err = client.WriteRegister(0x0000, 0x0c00)
|
||||
if err != nil {
|
||||
t.Errorf("client.WriteRegister() should have succeeded, got: %v", err)
|
||||
}
|
||||
|
||||
// make sure they have all been written to
|
||||
regs, err = client.ReadRegisters(0x0000, 10, HOLDING_REGISTER)
|
||||
if err != nil {
|
||||
t.Errorf("client.ReadRegisters() should have succeeded, got: %v", err)
|
||||
}
|
||||
for i := 0; i < 10; i++ {
|
||||
if regs[i] != 0x0c00 + uint16(0x11 * i) {
|
||||
t.Errorf("expected ox%04x at position %v, got: 0x%04x",
|
||||
0x0c00 + uint16(0x11 * i), i, regs[i])
|
||||
}
|
||||
}
|
||||
|
||||
// check values in the handler as well
|
||||
for i := 0; i < 10; i++ {
|
||||
if th.holding[i] != 0x0c00 + uint16(0x11 * i) {
|
||||
t.Errorf("expected 0xfea1 at handler index %v, got: 0x%04x", i, regs[i])
|
||||
}
|
||||
}
|
||||
|
||||
// reading addr 0x0009 (the very last register) should succeed
|
||||
regs, err = client.ReadRegisters(0x0009, 1, HOLDING_REGISTER)
|
||||
if err != nil {
|
||||
t.Errorf("client.ReadRegisters() should have succeeded, got: %v", err)
|
||||
}
|
||||
if regs[0] != 0x0c99 {
|
||||
t.Errorf("expected 0x0c99 at address 9, saw: 0x%04x", regs[0])
|
||||
}
|
||||
|
||||
// reading past address 0x000a should fail
|
||||
regs, err = client.ReadRegisters(0x0001, 10, HOLDING_REGISTER)
|
||||
if err != ErrIllegalDataAddress {
|
||||
t.Errorf("client.ReadRegisters() should have returned ErrIllegalDataAddress, got: %v", err)
|
||||
}
|
||||
regs, err = client.ReadRegisters(0x0000, 11, HOLDING_REGISTER)
|
||||
if err != ErrIllegalDataAddress {
|
||||
t.Errorf("client.ReadRegisters() should have returned ErrIllegalDataAddress, got: %v", err)
|
||||
}
|
||||
|
||||
// switch to another unit ID and make sure both holding and input register operations
|
||||
// return ErrIllegalFunction
|
||||
client.SetUnitId(2)
|
||||
err = client.WriteRegisters(0x0005, []uint16{
|
||||
0x0000, 0x0001,
|
||||
})
|
||||
if err != ErrIllegalFunction {
|
||||
t.Errorf("client.WriteRegisters() should have returned ErrIllegalFunction, got: %v", err)
|
||||
}
|
||||
err = client.WriteRegister(0x0001, 0xffff)
|
||||
if err != ErrIllegalFunction {
|
||||
t.Errorf("client.WriteRegister() should have returned ErrIllegalFunction, got: %v", err)
|
||||
}
|
||||
regs, err = client.ReadRegisters(0x0005, 1, HOLDING_REGISTER)
|
||||
if err != ErrIllegalFunction {
|
||||
t.Errorf("client.ReadRegisters() should have returned ErrIllegalFunction, got: %v", err)
|
||||
}
|
||||
regs, err = client.ReadRegisters(0x0005, 1, INPUT_REGISTER)
|
||||
if err != ErrIllegalFunction {
|
||||
t.Errorf("client.ReadRegisters() should have returned ErrIllegalFunction, got: %v", err)
|
||||
}
|
||||
|
||||
client.Close()
|
||||
server.Stop()
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
type tcpTestHandler struct {
|
||||
coils [10]bool
|
||||
di [10]bool
|
||||
input [10]uint16
|
||||
holding [10]uint16
|
||||
}
|
||||
|
||||
func (th *tcpTestHandler) HandleCoils(req *CoilsRequest) (res []bool, err error) {
|
||||
if req.UnitId != 9 {
|
||||
// only reply to unit ID #9
|
||||
err = ErrIllegalFunction
|
||||
return
|
||||
}
|
||||
|
||||
if req.Addr + req.Quantity > uint16(len(th.coils)) {
|
||||
err = ErrIllegalDataAddress
|
||||
return
|
||||
}
|
||||
|
||||
for i := 0; i < int(req.Quantity); i++ {
|
||||
if req.IsWrite {
|
||||
th.coils[int(req.Addr) + i] = req.Args[i]
|
||||
}
|
||||
res = append(res, th.coils[int(req.Addr) + i])
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func (th *tcpTestHandler) HandleDiscreteInputs(req *DiscreteInputsRequest) (res []bool, err error) {
|
||||
if req.UnitId != 9 {
|
||||
// only reply to unit ID #9
|
||||
err = ErrIllegalFunction
|
||||
return
|
||||
}
|
||||
|
||||
if req.Addr + req.Quantity > uint16(len(th.di)) {
|
||||
err = ErrIllegalDataAddress
|
||||
return
|
||||
}
|
||||
|
||||
for i := 0; i < int(req.Quantity); i++ {
|
||||
res = append(res, th.di[int(req.Addr) + i])
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func (th *tcpTestHandler) HandleHoldingRegisters(req *HoldingRegistersRequest) (res []uint16, err error) {
|
||||
if req.UnitId != 9 {
|
||||
// only reply to unit ID #9
|
||||
err = ErrIllegalFunction
|
||||
return
|
||||
}
|
||||
|
||||
if req.Addr + req.Quantity > uint16(len(th.holding)) {
|
||||
err = ErrIllegalDataAddress
|
||||
return
|
||||
}
|
||||
|
||||
for i := 0; i < int(req.Quantity); i++ {
|
||||
if req.IsWrite {
|
||||
th.holding[int(req.Addr) + i] = req.Args[i]
|
||||
}
|
||||
res = append(res, th.holding[int(req.Addr) + i])
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func (th *tcpTestHandler) HandleInputRegisters(req *InputRegistersRequest) (res []uint16, err error) {
|
||||
if req.UnitId != 9 {
|
||||
// only reply to unit ID #9
|
||||
err = ErrIllegalFunction
|
||||
return
|
||||
}
|
||||
|
||||
if req.Addr + req.Quantity > uint16(len(th.input)) {
|
||||
err = ErrIllegalDataAddress
|
||||
return
|
||||
}
|
||||
|
||||
for i := 0; i < int(req.Quantity); i++ {
|
||||
res = append(res, th.input[int(req.Addr) + i])
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
572
server_tls_test.go
Normal file
572
server_tls_test.go
Normal file
@@ -0,0 +1,572 @@
|
||||
package modbus
|
||||
|
||||
import (
|
||||
"crypto/tls"
|
||||
"crypto/x509"
|
||||
"crypto/x509/pkix"
|
||||
"encoding/pem"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
const (
|
||||
clientCertWithRoleOID string = `
|
||||
-----BEGIN CERTIFICATE-----
|
||||
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
|
||||
hG79y2z0UfKJstN+
|
||||
-----END CERTIFICATE-----
|
||||
`
|
||||
|
||||
clientKeyWithRoleOID string = `
|
||||
-----BEGIN PRIVATE KEY-----
|
||||
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
|
||||
isPLG4c6aPGxSbHirNfl6tBSngDy+A==
|
||||
-----END PRIVATE KEY-----
|
||||
`
|
||||
)
|
||||
|
||||
// TestTLSServer tests the TLS layer of the modbus server.
|
||||
func TestTLSServer(t *testing.T) {
|
||||
var err error
|
||||
var server *ModbusServer
|
||||
var serverKeyPair tls.Certificate
|
||||
var client1KeyPair tls.Certificate
|
||||
var client2KeyPair tls.Certificate
|
||||
var clientCp *x509.CertPool
|
||||
var serverCp *x509.CertPool
|
||||
var th *tlsTestHandler
|
||||
var c1 *ModbusClient
|
||||
var c2 *ModbusClient
|
||||
var regs []uint16
|
||||
var coils []bool
|
||||
|
||||
th = &tlsTestHandler{}
|
||||
|
||||
// load server keypair (from client_tls_test.go)
|
||||
serverKeyPair, err = tls.X509KeyPair([]byte(serverCert), []byte(serverKey))
|
||||
if err != nil {
|
||||
t.Errorf("failed to load test server key pair: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
// load the first client keypair (from client_tls_test.go)
|
||||
// this client cert doesn't have any Modbus Role extension
|
||||
client1KeyPair, err = tls.X509KeyPair([]byte(clientCert), []byte(clientKey))
|
||||
if err != nil {
|
||||
t.Errorf("failed to load test client key pair: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
// load the second client keypair (defined above)
|
||||
// this client cert has an "operator2" Modbus Role extension
|
||||
client2KeyPair, err = tls.X509KeyPair(
|
||||
[]byte(clientCertWithRoleOID), []byte(clientKeyWithRoleOID))
|
||||
if err != nil {
|
||||
t.Errorf("failed to load test client key pair: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
// load the server cert into the client CA cert pool to get the server cert
|
||||
// accepted by clients
|
||||
clientCp = x509.NewCertPool()
|
||||
if !clientCp.AppendCertsFromPEM([]byte(serverCert)) {
|
||||
t.Errorf("failed to load test server cert into cert pool")
|
||||
}
|
||||
|
||||
// start with an empty server cert pool initially to reject the client
|
||||
// certificate
|
||||
serverCp = x509.NewCertPool()
|
||||
|
||||
server, err = NewServer(&ServerConfiguration{
|
||||
URL: "tcp+tls://localhost:5802",
|
||||
MaxClients: 2,
|
||||
TLSServerCert: &serverKeyPair,
|
||||
TLSClientCAs: serverCp,
|
||||
}, th)
|
||||
if err != nil {
|
||||
t.Errorf("failed to create server: %v", err)
|
||||
}
|
||||
|
||||
err = server.Start()
|
||||
if err != nil {
|
||||
t.Errorf("failed to start server: %v", err)
|
||||
}
|
||||
|
||||
// create 2 modbus clients
|
||||
c1, err = NewClient(&ClientConfiguration{
|
||||
URL: "tcp+tls://localhost:5802",
|
||||
TLSClientCert: &client1KeyPair,
|
||||
TLSRootCAs: clientCp,
|
||||
})
|
||||
if err != nil {
|
||||
t.Errorf("failed to create client: %v", err)
|
||||
}
|
||||
c2, err = NewClient(&ClientConfiguration{
|
||||
URL: "tcp+tls://localhost:5802",
|
||||
TLSClientCert: &client2KeyPair,
|
||||
TLSRootCAs: clientCp,
|
||||
})
|
||||
if err != nil {
|
||||
t.Errorf("failed to create client: %v", err)
|
||||
}
|
||||
|
||||
// attempt to connect and use the first client. since its cert
|
||||
// is not trusted by the server, a TLS error should occur on the first
|
||||
// request.
|
||||
err = c1.Open()
|
||||
if err != nil {
|
||||
t.Errorf("c1.Open() should have succeeded")
|
||||
}
|
||||
coils, err = c1.ReadCoils(0, 5)
|
||||
if err == nil {
|
||||
t.Error("c1.ReadCoils() should have failed")
|
||||
}
|
||||
c1.Close()
|
||||
|
||||
// now place both client certs in the server's authorized client list
|
||||
// to get them past the TLS client cert validation procedure
|
||||
if !serverCp.AppendCertsFromPEM([]byte(clientCert)) {
|
||||
t.Errorf("failed to load client#1 cert into cert pool")
|
||||
}
|
||||
if !serverCp.AppendCertsFromPEM([]byte(clientCertWithRoleOID)) {
|
||||
t.Errorf("failed to load client#2 cert into cert pool")
|
||||
}
|
||||
|
||||
// connect both clients: should succeed
|
||||
err = c1.Open()
|
||||
if err != nil {
|
||||
t.Error("c1.Open() should have succeeded")
|
||||
}
|
||||
|
||||
err = c2.Open()
|
||||
if err != nil {
|
||||
t.Error("c2.Open() should have succeeded")
|
||||
}
|
||||
|
||||
// client #2 (with 'operator2' role) should have read/write access to coils while
|
||||
// client #1 (without role) should only be able to read.
|
||||
err = c1.WriteCoil(0, true)
|
||||
if err != ErrIllegalFunction {
|
||||
t.Errorf("c1.WriteCoil() should have failed with %v, got: %v",
|
||||
ErrIllegalFunction, err)
|
||||
}
|
||||
|
||||
coils, err = c1.ReadCoils(0, 5)
|
||||
if err != nil {
|
||||
t.Errorf("c1.ReadCoils() should have succeeded, got: %v", err)
|
||||
}
|
||||
if coils[0] {
|
||||
t.Errorf("coils[0] should have been false")
|
||||
}
|
||||
|
||||
err = c2.WriteCoil(0, true)
|
||||
if err != nil {
|
||||
t.Errorf("c2.WriteCoil() should have succeeded, got: %v", err)
|
||||
}
|
||||
|
||||
coils, err = c2.ReadCoils(0, 5)
|
||||
if err != nil {
|
||||
t.Errorf("c2.ReadCoils() should have succeeded, got: %v", err)
|
||||
}
|
||||
if !coils[0] {
|
||||
t.Errorf("coils[0] should have been true")
|
||||
}
|
||||
|
||||
coils, err = c1.ReadCoils(0, 5)
|
||||
if err != nil {
|
||||
t.Errorf("c1.ReadCoils() should have succeeded, got: %v", err)
|
||||
}
|
||||
if !coils[0] {
|
||||
t.Errorf("coils[0] should have been true")
|
||||
}
|
||||
|
||||
// client #1 should only be allowed access to holding registers of unit id #1
|
||||
// while client#2 should be allowed access to holding registers of unit ids #1 and #4
|
||||
c1.SetUnitId(1)
|
||||
err = c1.WriteRegister(2, 100)
|
||||
if err != nil {
|
||||
t.Errorf("c1.WriteRegister() should have succeeded, got: %v", err)
|
||||
}
|
||||
|
||||
c1.SetUnitId(4)
|
||||
err = c1.WriteRegister(2, 200)
|
||||
if err != ErrIllegalFunction {
|
||||
t.Errorf("c1.WriteRegister() should have failed with %v, got: %v",
|
||||
ErrIllegalFunction, err)
|
||||
}
|
||||
|
||||
c2.SetUnitId(1)
|
||||
regs, err = c2.ReadRegisters(1, 2, HOLDING_REGISTER)
|
||||
if err != nil {
|
||||
t.Errorf("c2.ReadRegisters() should have succeeded, got: %v", err)
|
||||
}
|
||||
if regs[0] != 0 || regs[1] != 100 {
|
||||
t.Errorf("unexpected register values: %v", regs)
|
||||
}
|
||||
|
||||
c2.SetUnitId(4)
|
||||
err = c2.WriteRegister(2, 200)
|
||||
if err != nil {
|
||||
t.Errorf("c2.WriteRegister() should have succeeded, got: %v", err)
|
||||
}
|
||||
|
||||
regs, err = c2.ReadRegisters(1, 2, HOLDING_REGISTER)
|
||||
if err != nil {
|
||||
t.Errorf("c2.ReadRegisters() should have succeeded, got: %v", err)
|
||||
}
|
||||
if regs[0] != 0 || regs[1] != 200 {
|
||||
t.Errorf("unexpected register values: %v", regs)
|
||||
}
|
||||
|
||||
// close the server and all client connections
|
||||
server.Stop()
|
||||
|
||||
// make sure all underlying TCP client connections have been freed
|
||||
time.Sleep(10 * time.Millisecond)
|
||||
server.lock.Lock()
|
||||
if len(server.tcpClients) != 0 {
|
||||
t.Errorf("expected 0 client connections, saw: %v", len(server.tcpClients))
|
||||
}
|
||||
server.lock.Unlock()
|
||||
|
||||
// cleanup
|
||||
c1.Close()
|
||||
c2.Close()
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
type tlsTestHandler struct {
|
||||
coils [10]bool
|
||||
holdingId1 [10]uint16
|
||||
holdingId4 [10]uint16
|
||||
}
|
||||
|
||||
func (th *tlsTestHandler) HandleCoils(req *CoilsRequest) (res []bool, err error) {
|
||||
// coils access is allowed to any client with a valid cert, but
|
||||
// the "operator2" role is required to write
|
||||
if req.IsWrite && req.ClientRole != "operator2" {
|
||||
err = ErrIllegalFunction
|
||||
return
|
||||
}
|
||||
|
||||
if req.Addr + req.Quantity > uint16(len(th.coils)) {
|
||||
err = ErrIllegalDataAddress
|
||||
return
|
||||
}
|
||||
|
||||
for i := 0; i < int(req.Quantity); i++ {
|
||||
if req.IsWrite {
|
||||
th.coils[int(req.Addr) + i] = req.Args[i]
|
||||
}
|
||||
res = append(res, th.coils[int(req.Addr) + i])
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func (th *tlsTestHandler) HandleDiscreteInputs(req *DiscreteInputsRequest) (res []bool, err error) {
|
||||
// there are no digital inputs on this device
|
||||
err = ErrIllegalDataAddress
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func (th *tlsTestHandler) HandleHoldingRegisters(req *HoldingRegistersRequest) (res []uint16, err error) {
|
||||
// gate unit id #4 behind the "operator2" role while access to unit id #1
|
||||
// is allowed to any valid cert
|
||||
if req.UnitId == 0x04 {
|
||||
if req.ClientRole != "operator2" {
|
||||
err = ErrIllegalFunction
|
||||
return
|
||||
}
|
||||
|
||||
if req.Addr + req.Quantity > uint16(len(th.holdingId4)) {
|
||||
err = ErrIllegalDataAddress
|
||||
return
|
||||
}
|
||||
|
||||
for i := 0; i < int(req.Quantity); i++ {
|
||||
if req.IsWrite {
|
||||
th.holdingId4[int(req.Addr) + i] = req.Args[i]
|
||||
}
|
||||
res = append(res, th.holdingId4[int(req.Addr) + i])
|
||||
}
|
||||
} else if req.UnitId == 0x01 {
|
||||
if req.Addr + req.Quantity > uint16(len(th.holdingId1)) {
|
||||
err = ErrIllegalDataAddress
|
||||
return
|
||||
}
|
||||
|
||||
for i := 0; i < int(req.Quantity); i++ {
|
||||
if req.IsWrite {
|
||||
th.holdingId1[int(req.Addr) + i] = req.Args[i]
|
||||
}
|
||||
res = append(res, th.holdingId1[int(req.Addr) + i])
|
||||
}
|
||||
} else {
|
||||
err = ErrIllegalFunction
|
||||
return
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func (th *tlsTestHandler) HandleInputRegisters(req *InputRegistersRequest) (res []uint16, err error) {
|
||||
// there are no inputs registers on this device
|
||||
err = ErrIllegalDataAddress
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func TestServerExtractRole(t *testing.T) {
|
||||
var ms *ModbusServer
|
||||
var pemBlock *pem.Block
|
||||
var x509Cert *x509.Certificate
|
||||
var err error
|
||||
var role string
|
||||
|
||||
ms = &ModbusServer{
|
||||
logger: newLogger("test-server-role-extraction", nil),
|
||||
}
|
||||
|
||||
// load a client cert without role OID
|
||||
pemBlock, _ = pem.Decode([]byte(clientCert))
|
||||
if err != nil {
|
||||
t.Errorf("failed to decode client cert: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
x509Cert, err = x509.ParseCertificate(pemBlock.Bytes)
|
||||
if err != nil {
|
||||
t.Errorf("failed to parse client cert: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
// calling extractRole on a cert without role extension should return an
|
||||
// empty string (see R-23 of the MBAPS spec)
|
||||
role = ms.extractRole(x509Cert)
|
||||
if role != "" {
|
||||
t.Errorf("role should have been empty, got: '%s'", role)
|
||||
}
|
||||
|
||||
// load a certificate with a single role extension of "operator2"
|
||||
pemBlock, _ = pem.Decode([]byte(clientCertWithRoleOID))
|
||||
if err != nil {
|
||||
t.Errorf("failed to decode client cert: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
x509Cert, err = x509.ParseCertificate(pemBlock.Bytes)
|
||||
if err != nil {
|
||||
t.Errorf("failed to parse client cert: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
role = ms.extractRole(x509Cert)
|
||||
if role != "operator2" {
|
||||
t.Errorf("role should have been 'operator2', got: '%s'", role)
|
||||
}
|
||||
|
||||
// build a certificate with multiple Modbus Role extensions: they should
|
||||
// all be rejected
|
||||
x509Cert = &x509.Certificate{
|
||||
Extensions: []pkix.Extension{
|
||||
{
|
||||
Id: modbusRoleOID,
|
||||
Value: []byte{
|
||||
0x0c, 0x04, 0x66, 0x77, 0x67, 0x78,
|
||||
// ^ ASN1:UTF8String
|
||||
// ^ length
|
||||
// ^ 4-byte string 'fwgx'
|
||||
},
|
||||
},
|
||||
{
|
||||
Id: modbusRoleOID,
|
||||
Value: []byte{
|
||||
0x0c, 0x02, 0x66, 0x67,
|
||||
// ^ ASN1:UTF8String
|
||||
// ^ length
|
||||
// ^ 2-byte string 'fwwf'
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
role = ms.extractRole(x509Cert)
|
||||
if role != "" {
|
||||
t.Errorf("role should have been empty, got: '%s'", role)
|
||||
}
|
||||
|
||||
// build a certificate with a single Modbus Role extension of the wrong
|
||||
// type: the role should be rejected
|
||||
x509Cert = &x509.Certificate{
|
||||
Extensions: []pkix.Extension{
|
||||
{
|
||||
Id: modbusRoleOID,
|
||||
Value: []byte{
|
||||
0x13, 0x04, 0x66, 0x77, 0x67, 0x78,
|
||||
// ^ ASN1:PrintableString
|
||||
// ^ length
|
||||
// ^ 4-byte string 'fwgx'
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
role = ms.extractRole(x509Cert)
|
||||
if role != "" {
|
||||
t.Errorf("role should have been empty, got: '%s'", role)
|
||||
}
|
||||
|
||||
// build a certificate with a single, short Modbus Role extension: the role
|
||||
// should be rejected
|
||||
x509Cert = &x509.Certificate{
|
||||
Extensions: []pkix.Extension{
|
||||
{
|
||||
Id: modbusRoleOID,
|
||||
Value: []byte{
|
||||
0x0c,
|
||||
// ^ ASN1:UTF8String
|
||||
// ^ missing length + payload bytes
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
role = ms.extractRole(x509Cert)
|
||||
if role != "" {
|
||||
t.Errorf("role should have been empty, got: '%s'", role)
|
||||
}
|
||||
|
||||
// build a certificate with one bad Modbus Role extension (short) and one
|
||||
// valid: they should both be rejected
|
||||
x509Cert = &x509.Certificate{
|
||||
Extensions: []pkix.Extension{
|
||||
{
|
||||
Id: modbusRoleOID,
|
||||
Value: []byte{
|
||||
0x0c,
|
||||
// ^ ASN1:UTF8String
|
||||
// ^ missing length + payload bytes
|
||||
},
|
||||
},
|
||||
{
|
||||
Id: modbusRoleOID,
|
||||
Value: []byte{
|
||||
0x0c, 0x02, 0x66, 0x67,
|
||||
// ^ ASN1:UTF8String
|
||||
// ^ length
|
||||
// ^ 2-byte string 'fwwf'
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
role = ms.extractRole(x509Cert)
|
||||
if role != "" {
|
||||
t.Errorf("role should have been empty, got: '%s'", role)
|
||||
}
|
||||
|
||||
// build a certificate with a single, valid Modbus Role extension: it should be
|
||||
// accepted
|
||||
x509Cert = &x509.Certificate{
|
||||
Extensions: []pkix.Extension{
|
||||
{
|
||||
Id: modbusRoleOID,
|
||||
Value: []byte{
|
||||
0x0c, 0x04, 0x66, 0x77, 0x67, 0x78,
|
||||
// ^ ASN1:UTF8String
|
||||
// ^ length
|
||||
// ^ 4-byte string 'fwgx'
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
role = ms.extractRole(x509Cert)
|
||||
if role != "fwgx" {
|
||||
t.Errorf("role should have been 'fwgx', got: '%s'", role)
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
205
tcp_transport.go
Normal file
205
tcp_transport.go
Normal file
@@ -0,0 +1,205 @@
|
||||
package modbus
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"log"
|
||||
"net"
|
||||
"time"
|
||||
)
|
||||
|
||||
const (
|
||||
maxTCPFrameLength int = 260
|
||||
mbapHeaderLength int = 7
|
||||
)
|
||||
|
||||
type tcpTransport struct {
|
||||
logger *logger
|
||||
socket net.Conn
|
||||
timeout time.Duration
|
||||
lastTxnId uint16
|
||||
}
|
||||
|
||||
// Returns a new TCP transport.
|
||||
func newTCPTransport(socket net.Conn, timeout time.Duration, customLogger *log.Logger) (tt *tcpTransport) {
|
||||
tt = &tcpTransport{
|
||||
socket: socket,
|
||||
timeout: timeout,
|
||||
logger: newLogger(fmt.Sprintf("tcp-transport(%s)", socket.RemoteAddr()), customLogger),
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// Closes the underlying tcp socket.
|
||||
func (tt *tcpTransport) Close() (err error) {
|
||||
err = tt.socket.Close()
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// Runs a request across the socket and returns a response.
|
||||
func (tt *tcpTransport) ExecuteRequest(req *pdu) (res *pdu, err error) {
|
||||
// set an i/o deadline on the socket (read and write)
|
||||
err = tt.socket.SetDeadline(time.Now().Add(tt.timeout))
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
// increase the transaction ID counter
|
||||
tt.lastTxnId++
|
||||
|
||||
_, err = tt.socket.Write(tt.assembleMBAPFrame(tt.lastTxnId, req))
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
res, err = tt.readResponse()
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// Reads a request from the socket.
|
||||
func (tt *tcpTransport) ReadRequest() (req *pdu, err error) {
|
||||
var txnId uint16
|
||||
|
||||
// set an i/o deadline on the socket (read and write)
|
||||
err = tt.socket.SetDeadline(time.Now().Add(tt.timeout))
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
req, txnId, err = tt.readMBAPFrame()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
// store the incoming transaction id
|
||||
tt.lastTxnId = txnId
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// Writes a response to the socket.
|
||||
func (tt *tcpTransport) WriteResponse(res *pdu) (err error) {
|
||||
_, err = tt.socket.Write(tt.assembleMBAPFrame(tt.lastTxnId, res))
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// Reads as many MBAP+modbus frames as necessary until either the response
|
||||
// matching tt.lastTxnId is received or an error occurs.
|
||||
func (tt *tcpTransport) readResponse() (res *pdu, err error) {
|
||||
var txnId uint16
|
||||
|
||||
for {
|
||||
// grab a frame
|
||||
res, txnId, err = tt.readMBAPFrame()
|
||||
|
||||
// ignore unknown protocol identifiers
|
||||
if err == ErrUnknownProtocolId {
|
||||
continue
|
||||
}
|
||||
|
||||
// abort on any other erorr
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
// ignore unknown transaction identifiers
|
||||
if tt.lastTxnId != txnId {
|
||||
tt.logger.Warningf("received unexpected transaction id " +
|
||||
"(expected 0x%04x, received 0x%04x)",
|
||||
tt.lastTxnId, txnId)
|
||||
continue
|
||||
}
|
||||
|
||||
break
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// Reads an entire frame (MBAP header + modbus PDU) from the socket.
|
||||
func (tt *tcpTransport) readMBAPFrame() (p *pdu, txnId uint16, err error) {
|
||||
var rxbuf []byte
|
||||
var bytesNeeded int
|
||||
var protocolId uint16
|
||||
var unitId uint8
|
||||
|
||||
// read the MBAP header
|
||||
rxbuf = make([]byte, mbapHeaderLength)
|
||||
_, err = io.ReadFull(tt.socket, rxbuf)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
// decode the transaction identifier
|
||||
txnId = bytesToUint16(BIG_ENDIAN, rxbuf[0:2])
|
||||
// decode the protocol identifier
|
||||
protocolId = bytesToUint16(BIG_ENDIAN, rxbuf[2:4])
|
||||
// store the source unit id
|
||||
unitId = rxbuf[6]
|
||||
|
||||
// determine how many more bytes we need to read
|
||||
bytesNeeded = int(bytesToUint16(BIG_ENDIAN, rxbuf[4:6]))
|
||||
|
||||
// the byte count includes the unit ID field, which we already have
|
||||
bytesNeeded--
|
||||
|
||||
// never read more than the max allowed frame length
|
||||
if bytesNeeded + mbapHeaderLength > maxTCPFrameLength {
|
||||
err = ErrProtocolError
|
||||
return
|
||||
}
|
||||
|
||||
// an MBAP length of 0 is illegal
|
||||
if bytesNeeded <= 0 {
|
||||
err = ErrProtocolError
|
||||
return
|
||||
}
|
||||
|
||||
// read the PDU
|
||||
rxbuf = make([]byte, bytesNeeded)
|
||||
_, err = io.ReadFull(tt.socket, rxbuf)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
// validate the protocol identifier
|
||||
if protocolId != 0x0000 {
|
||||
err = ErrUnknownProtocolId
|
||||
tt.logger.Warningf("received unexpected protocol id 0x%04x", protocolId)
|
||||
return
|
||||
}
|
||||
|
||||
// store unit id, function code and payload in the PDU object
|
||||
p = &pdu{
|
||||
unitId: unitId,
|
||||
functionCode: rxbuf[0],
|
||||
payload: rxbuf[1:],
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// Turns a PDU into an MBAP frame (MBAP header + PDU) and returns it as bytes.
|
||||
func (tt *tcpTransport) assembleMBAPFrame(txnId uint16, p *pdu) (payload []byte) {
|
||||
// transaction identifier
|
||||
payload = uint16ToBytes(BIG_ENDIAN, txnId)
|
||||
// protocol identifier (always 0x0000)
|
||||
payload = append(payload, 0x00, 0x00)
|
||||
// length (covers unit identifier + function code + payload fields)
|
||||
payload = append(payload, uint16ToBytes(BIG_ENDIAN, uint16(2 + len(p.payload)))...)
|
||||
// unit identifier
|
||||
payload = append(payload, p.unitId)
|
||||
// function code
|
||||
payload = append(payload, p.functionCode)
|
||||
// payload
|
||||
payload = append(payload, p.payload...)
|
||||
|
||||
return
|
||||
}
|
||||
373
tcp_transport_test.go
Normal file
373
tcp_transport_test.go
Normal file
@@ -0,0 +1,373 @@
|
||||
package modbus
|
||||
|
||||
import (
|
||||
"io"
|
||||
"net"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func TestAssembleMBAPFrame(t *testing.T) {
|
||||
var tt *tcpTransport
|
||||
var frame []byte
|
||||
|
||||
tt = &tcpTransport{}
|
||||
|
||||
frame = tt.assembleMBAPFrame(0x9219, &pdu{
|
||||
unitId: 0x33,
|
||||
functionCode: 0x11,
|
||||
payload: []byte{0x22, 0x33, 0x44, 0x55},
|
||||
})
|
||||
// expect 7 bytes of MBAP header + 1 bytes of function code + 4 bytes of payload
|
||||
if len(frame) != 12 {
|
||||
t.Errorf("expected 12 bytes, got %v", len(frame))
|
||||
}
|
||||
for i, b := range []byte{
|
||||
0x92, 0x19, // transaction identifier (big endian)
|
||||
0x00, 0x00, // protocol identifier
|
||||
0x00, 0x06, // length (big endian)
|
||||
0x33, 0x11, // unit id and function code
|
||||
0x22, 0x33, // payload
|
||||
0x44, 0x55, // payload
|
||||
} {
|
||||
if frame[i] != b {
|
||||
t.Errorf("expected 0x%02x at position %v, got 0x%02x", b, i, frame[i])
|
||||
}
|
||||
}
|
||||
|
||||
frame = tt.assembleMBAPFrame(0x921a, &pdu{
|
||||
unitId: 0x31,
|
||||
functionCode: 0x06,
|
||||
payload: []byte{0x12, 0x34},
|
||||
})
|
||||
// expect 7 bytes of MBAP header + 1 bytes of function code + 2 bytes of payload
|
||||
if len(frame) != 10 {
|
||||
t.Errorf("expected 10 bytes, got %v", len(frame))
|
||||
}
|
||||
for i, b := range []byte{
|
||||
0x92, 0x1a, // transaction identifier (big endian)
|
||||
0x00, 0x00, // protocol identifier
|
||||
0x00, 0x04, // length (big endian)
|
||||
0x31, 0x06, // unit id and function code
|
||||
0x12, 0x34, // payload
|
||||
} {
|
||||
if frame[i] != b {
|
||||
t.Errorf("expected 0x%02x at position %v, got 0x%02x", b, i, frame[i])
|
||||
}
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func TestTCPTransportReadResponse(t *testing.T) {
|
||||
var tt *tcpTransport
|
||||
var p1, p2 net.Conn
|
||||
var txchan chan []byte
|
||||
var err error
|
||||
var res *pdu
|
||||
|
||||
txchan = make(chan []byte, 2)
|
||||
p1, p2 = net.Pipe()
|
||||
go feedTestPipe(t, txchan, p1)
|
||||
|
||||
|
||||
tt = newTCPTransport(p2, 10 * time.Millisecond, nil)
|
||||
tt.lastTxnId = 0x9218
|
||||
|
||||
// read a valid response
|
||||
txchan <- []byte{
|
||||
0x92, 0x18, // transaction identifier (big endian)
|
||||
0x00, 0x00, // protocol identifier
|
||||
0x00, 0x04, // length (big endian)
|
||||
0x31, 0x06, // unit id and function code
|
||||
0x12, 0x34, // payload
|
||||
}
|
||||
res, err = tt.readResponse()
|
||||
if err != nil {
|
||||
t.Errorf("readResponse() should have succeeded, got %v", err)
|
||||
}
|
||||
if res.unitId != 0x31 {
|
||||
t.Errorf("expected 0x31 as unit id, got 0x%02x", res.unitId)
|
||||
}
|
||||
if res.functionCode != 0x06 {
|
||||
t.Errorf("expected 0x06 as function code, got 0x%02x", res.functionCode)
|
||||
}
|
||||
if len(res.payload) != 2 {
|
||||
t.Errorf("expected a length of 2, got %v", len(res.payload))
|
||||
}
|
||||
if res.payload[0] != 0x12 || res.payload[1] != 0x34 {
|
||||
t.Errorf("expected {0x12, 0x34} as payload, got {0x%02x, 0x%02x}",
|
||||
res.payload[0], res.payload[1])
|
||||
}
|
||||
|
||||
// read a frame with an unexpected transaction id followed by a frame with a
|
||||
// matching transaction id: the first frame should be silently skipped
|
||||
txchan <- []byte{
|
||||
0x92, 0x19, // transaction identifier (big endian)
|
||||
0x00, 0x00, // protocol identifier
|
||||
0x00, 0x04, // length (big endian)
|
||||
0x31, 0x06, // unit id and function code
|
||||
0x12, 0x34, // payload
|
||||
}
|
||||
txchan <- []byte{
|
||||
0x92, 0x18, // transaction identifier (big endian)
|
||||
0x00, 0x00, // protocol identifier
|
||||
0x00, 0x04, // length (big endian)
|
||||
0x39, 0x02, // unit id and function code
|
||||
0x10, 0x01, // payload
|
||||
}
|
||||
res, err = tt.readResponse()
|
||||
if err != nil {
|
||||
t.Errorf("readResponse() should have succeeded, got %v", err)
|
||||
}
|
||||
if res.unitId != 0x39 {
|
||||
t.Errorf("expected 0x39 as unit id, got 0x%02x", res.unitId)
|
||||
}
|
||||
if res.functionCode != 0x02 {
|
||||
t.Errorf("expected 0x02 as function code, got 0x%02x", res.functionCode)
|
||||
}
|
||||
if len(res.payload) != 2 {
|
||||
t.Errorf("expected a length of 2, got %v", len(res.payload))
|
||||
}
|
||||
if res.payload[0] != 0x10 || res.payload[1] != 0x01 {
|
||||
t.Errorf("expected {0x10, 0x01 as payload, got {0x%02x, 0x%02x}",
|
||||
res.payload[0], res.payload[1])
|
||||
}
|
||||
|
||||
// read a frame with an illegal length, preceded by a frame with an unexpected
|
||||
// protocol ID. While the first frame should be skipped without error,
|
||||
// the second should yield an ErrProtocolError.
|
||||
txchan <- []byte{
|
||||
0x92, 0x18, // transaction identifier (big endian)
|
||||
0x00, 0x01, // protocol identifier
|
||||
0x00, 0x04, // length (big endian)
|
||||
0x31, 0x06, // unit id and function code
|
||||
0x12, 0x34, // payload
|
||||
}
|
||||
txchan <- []byte{
|
||||
0x92, 0x18, // transaction identifier (big endian)
|
||||
0x00, 0x00, // protocol identifier
|
||||
0x00, 0x01, // length (big endian)
|
||||
0x31, // unit id
|
||||
}
|
||||
res, err = tt.readResponse()
|
||||
if err != ErrProtocolError {
|
||||
t.Errorf("readResponse() should have returned ErrProtocolError, got %v", err)
|
||||
}
|
||||
|
||||
// read a valid frame again
|
||||
txchan <- []byte{
|
||||
0x92, 0x18, // transaction identifier (big endian)
|
||||
0x00, 0x00, // protocol identifier
|
||||
0x00, 0x0a, // length (big endian)
|
||||
0x31, 0x32, // unit id and function code
|
||||
0x44, 0x55, // payload
|
||||
0x66, 0x77, // payload
|
||||
0x88, 0x99, // payload
|
||||
0xaa, 0xbb, // payload
|
||||
}
|
||||
res, err = tt.readResponse()
|
||||
if err != nil {
|
||||
t.Errorf("readResponse() should have succeeded, got %v", err)
|
||||
}
|
||||
if res.unitId != 0x31 {
|
||||
t.Errorf("expected 0x31 as unit id, got 0x%02x", res.unitId)
|
||||
}
|
||||
if res.functionCode != 0x32 {
|
||||
t.Errorf("expected 0x32 as response code, got 0x%02x", res.functionCode)
|
||||
}
|
||||
if len(res.payload) != 8 {
|
||||
t.Errorf("expected a length of 8, got %v", len(res.payload))
|
||||
}
|
||||
for i, b := range []byte{
|
||||
0x44, 0x55,
|
||||
0x66, 0x77,
|
||||
0x88, 0x99,
|
||||
0xaa, 0xbb,
|
||||
} {
|
||||
if res.payload[i] != b {
|
||||
t.Errorf("expected 0x%02x at position %v, got 0x%02x",
|
||||
b, i, res.payload[i])
|
||||
}
|
||||
}
|
||||
|
||||
// read a huge frame
|
||||
txchan <- []byte{
|
||||
0x92, 0x18, // transaction identifier (big endian)
|
||||
0x00, 0x00, // protocol identifier
|
||||
0x10, 0x0a, // length (big endian)
|
||||
0x31, // unit id
|
||||
}
|
||||
res, err = tt.readResponse()
|
||||
if err != ErrProtocolError {
|
||||
t.Errorf("readResponse() should have returned ErrProtocolError, got %v", err)
|
||||
}
|
||||
|
||||
p1.Close()
|
||||
p2.Close()
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func TestTCPTransportReadRequest(t *testing.T) {
|
||||
var tt *tcpTransport
|
||||
var p1, p2 net.Conn
|
||||
var txchan chan []byte
|
||||
var err error
|
||||
var req *pdu
|
||||
|
||||
txchan = make(chan []byte, 2)
|
||||
p1, p2 = net.Pipe()
|
||||
go feedTestPipe(t, txchan, p1)
|
||||
|
||||
|
||||
tt = newTCPTransport(p2, 10 * time.Millisecond, nil)
|
||||
tt.lastTxnId = 0x0a00
|
||||
|
||||
// push three frames in a row:
|
||||
// - the first with an unknown protocol ID
|
||||
txchan <- []byte{
|
||||
0x92, 0x18, // transaction identifier (big endian)
|
||||
0x00, 0x01, // protocol identifier
|
||||
0x00, 0x04, // length (big endian)
|
||||
0x31, 0x06, // unit id and function code
|
||||
0x12, 0x34, // payload
|
||||
}
|
||||
// - the second with an illegal length
|
||||
txchan <- []byte{
|
||||
0x92, 0x18, // transaction identifier (big endian)
|
||||
0x00, 0x00, // protocol identifier
|
||||
0x00, 0x01, // length (big endian)
|
||||
0x31, // unit id
|
||||
}
|
||||
// - the thid with a valid request
|
||||
txchan <- []byte{
|
||||
0x92, 0x18, // transaction identifier (big endian)
|
||||
0x00, 0x00, // protocol identifier
|
||||
0x00, 0x0a, // length (big endian)
|
||||
0xfa, 0x04, // unit id and function code
|
||||
0x44, 0x55, // payload
|
||||
0x66, 0x77, // payload
|
||||
0x88, 0x99, // payload
|
||||
0xaa, 0xbb, // payload
|
||||
}
|
||||
|
||||
// read the first frame
|
||||
req, err = tt.ReadRequest()
|
||||
if req != nil || err != ErrUnknownProtocolId {
|
||||
t.Errorf("ReadRequest() should have returned {nil, ErrUnknownProtocolId}, got {%v, %v}", req, err)
|
||||
}
|
||||
if tt.lastTxnId != 0x0a00 {
|
||||
t.Errorf("tt.lastTxnId should have been 0x0a00, saw 0x%02x", tt.lastTxnId)
|
||||
}
|
||||
|
||||
// read the second frame
|
||||
req, err = tt.ReadRequest()
|
||||
if req != nil || err != ErrProtocolError {
|
||||
t.Errorf("ReadRequest() should have returned {nil, ErrProtocolError}, got {%v, %v}", req, err)
|
||||
}
|
||||
if tt.lastTxnId != 0x0a00 {
|
||||
t.Errorf("tt.lastTxnId should have been 0x0a00, saw 0x%02x", tt.lastTxnId)
|
||||
}
|
||||
|
||||
// read the third frame
|
||||
req, err = tt.ReadRequest()
|
||||
if err != nil {
|
||||
t.Errorf("ReadRequest() should have succeeded, got %v", err)
|
||||
}
|
||||
if req == nil {
|
||||
t.Errorf("ReadREsponse() should have returned a non-nil request")
|
||||
}
|
||||
if req.unitId != 0xfa {
|
||||
t.Errorf("expected 0xfa as unit id, got 0x%02x", req.unitId)
|
||||
}
|
||||
if req.functionCode != 0x04 {
|
||||
t.Errorf("expected 0x04 as response code, got 0x%02x", req.functionCode)
|
||||
}
|
||||
if len(req.payload) != 8 {
|
||||
t.Errorf("expected a length of 8, got %v", len(req.payload))
|
||||
}
|
||||
for i, b := range []byte{
|
||||
0x44, 0x55,
|
||||
0x66, 0x77,
|
||||
0x88, 0x99,
|
||||
0xaa, 0xbb,
|
||||
} {
|
||||
if req.payload[i] != b {
|
||||
t.Errorf("expected 0x%02x at position %v, got 0x%02x",
|
||||
b, i, req.payload[i])
|
||||
}
|
||||
}
|
||||
if tt.lastTxnId != 0x9218 {
|
||||
t.Errorf("tt.lastTxnId should have been 0x0a00, saw 0x%02x", tt.lastTxnId)
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func TestTCPTransportWriteResponse(t *testing.T) {
|
||||
var tt *tcpTransport
|
||||
var p1, p2 net.Conn
|
||||
var done chan bool
|
||||
var err error
|
||||
|
||||
done = make(chan bool, 0)
|
||||
p1, p2 = net.Pipe()
|
||||
go func(t *testing.T, pipe net.Conn, done chan bool) {
|
||||
var err error
|
||||
var rxbuf []byte
|
||||
var expected []byte
|
||||
|
||||
expected = []byte{
|
||||
0xc0, 0x1f, // transaction identifier (big endian)
|
||||
0x00, 0x00, // protocol identifier
|
||||
0x00, 0x0b, // length (big endian)
|
||||
0x17, 0x06, // unit id and function code
|
||||
0x44, 0x55, // payload
|
||||
0x66, 0x77, // payload
|
||||
0x88, 0x99, // payload
|
||||
0xaa, 0xbb, // payload
|
||||
0xf4, // payload
|
||||
}
|
||||
|
||||
rxbuf = make([]byte, len(expected))
|
||||
_, err = io.ReadFull(pipe, rxbuf)
|
||||
if err != nil {
|
||||
t.Errorf("failed to read frame: %v", err)
|
||||
}
|
||||
|
||||
for i, b := range expected {
|
||||
if rxbuf[i] != b {
|
||||
t.Errorf("expected 0x%02x at position %v, got 0x%02x",
|
||||
b, i, rxbuf[i])
|
||||
}
|
||||
}
|
||||
|
||||
done<- true
|
||||
return
|
||||
}(t, p2, done)
|
||||
|
||||
|
||||
tt = newTCPTransport(p1, 10 * time.Millisecond, nil)
|
||||
tt.lastTxnId = 0xc01f
|
||||
|
||||
err = tt.WriteResponse(&pdu{
|
||||
unitId: 0x17,
|
||||
functionCode: 0x06,
|
||||
payload: []byte{
|
||||
0x44, 0x55, // payload
|
||||
0x66, 0x77, // payload
|
||||
0x88, 0x99, // payload
|
||||
0xaa, 0xbb, // payload
|
||||
0xf4, // payload
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
t.Errorf("WriteResponse() should have succeeded, got %v", err)
|
||||
}
|
||||
|
||||
// wait for the checker goroutine to return
|
||||
<-done
|
||||
|
||||
return
|
||||
}
|
||||
116
tls_utils.go
Normal file
116
tls_utils.go
Normal file
@@ -0,0 +1,116 @@
|
||||
package modbus
|
||||
|
||||
import (
|
||||
"crypto/x509"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"net"
|
||||
"os"
|
||||
"time"
|
||||
)
|
||||
|
||||
// LoadCertPool loads a certificate store from a file into a CertPool object.
|
||||
func LoadCertPool(filePath string) (cp *x509.CertPool, err error) {
|
||||
var buf []byte
|
||||
|
||||
// read the entire cert store, which may contain zero, one
|
||||
// or more certificates
|
||||
buf, err = ioutil.ReadFile(filePath)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
if len(buf) == 0 {
|
||||
err = fmt.Errorf("%v: empty file", filePath)
|
||||
return
|
||||
}
|
||||
|
||||
// add these certs to the pool
|
||||
cp = x509.NewCertPool()
|
||||
cp.AppendCertsFromPEM(buf)
|
||||
|
||||
// let the caller know if no usable certificate was found
|
||||
if len(cp.Subjects()) == 0 {
|
||||
err = fmt.Errorf("%v: no certificate found", filePath)
|
||||
return
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// tlsSockWrapper wraps a TLS socket to work around odd error handling in
|
||||
// TLSConn on internal connection state corruption.
|
||||
// tlsSockWrapper implements the net.Conn interface to allow its
|
||||
// use by the modbus TCP transport.
|
||||
type tlsSockWrapper struct {
|
||||
sock net.Conn
|
||||
}
|
||||
|
||||
func newTLSSockWrapper(sock net.Conn) (tsw *tlsSockWrapper) {
|
||||
tsw = &tlsSockWrapper{
|
||||
sock: sock,
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func (tsw *tlsSockWrapper) Read(buf []byte) (rlen int, err error) {
|
||||
rlen, err = tsw.sock.Read(buf)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func (tsw *tlsSockWrapper) Write(buf []byte) (wlen int, err error) {
|
||||
wlen, err = tsw.sock.Write(buf)
|
||||
|
||||
// since write timeouts corrupt the internal state of TLS sockets,
|
||||
// any subsequent read/write operation will fail and return the same write
|
||||
// timeout error (see https://pkg.go.dev/crypto/tls#Conn.SetWriteDeadline).
|
||||
// this isn't all that helpful to clients, which may be tricked into
|
||||
// retrying forever, treating timeout errors as transient.
|
||||
// to avoid this, close the TLS socket after the first write timeout.
|
||||
// this ensures that clients 1) get a timeout error on the first write timeout
|
||||
// and 2) get an ErrNetClosing "use of closed network connection" on subsequent
|
||||
// operations.
|
||||
if err != nil && os.IsTimeout(err) {
|
||||
tsw.sock.Close()
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func (tsw *tlsSockWrapper) Close() (err error) {
|
||||
err = tsw.sock.Close()
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func (tsw *tlsSockWrapper) SetDeadline(deadline time.Time) (err error) {
|
||||
err = tsw.sock.SetDeadline(deadline)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func (tsw *tlsSockWrapper) SetReadDeadline(deadline time.Time) (err error) {
|
||||
err = tsw.sock.SetReadDeadline(deadline)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func (tsw *tlsSockWrapper) SetWriteDeadline(deadline time.Time) (err error) {
|
||||
err = tsw.sock.SetWriteDeadline(deadline)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func (tsw *tlsSockWrapper) LocalAddr() (addr net.Addr) {
|
||||
addr = tsw.sock.LocalAddr()
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func (tsw *tlsSockWrapper) RemoteAddr() (addr net.Addr) {
|
||||
addr = tsw.sock.RemoteAddr()
|
||||
|
||||
return
|
||||
}
|
||||
151
tls_utils_test.go
Normal file
151
tls_utils_test.go
Normal file
@@ -0,0 +1,151 @@
|
||||
package modbus
|
||||
|
||||
import (
|
||||
"crypto/x509"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// random certs from /etc/ssl/certs
|
||||
const validCerts = `-----BEGIN CERTIFICATE-----
|
||||
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
|
||||
7uzXLg==
|
||||
-----END CERTIFICATE-----
|
||||
-----BEGIN CERTIFICATE-----
|
||||
MIIDnzCCAoegAwIBAgIBJjANBgkqhkiG9w0BAQUFADBxMQswCQYDVQQGEwJERTEc
|
||||
MBoGA1UEChMTRGV1dHNjaGUgVGVsZWtvbSBBRzEfMB0GA1UECxMWVC1UZWxlU2Vj
|
||||
IFRydXN0IENlbnRlcjEjMCEGA1UEAxMaRGV1dHNjaGUgVGVsZWtvbSBSb290IENB
|
||||
IDIwHhcNOTkwNzA5MTIxMTAwWhcNMTkwNzA5MjM1OTAwWjBxMQswCQYDVQQGEwJE
|
||||
RTEcMBoGA1UEChMTRGV1dHNjaGUgVGVsZWtvbSBBRzEfMB0GA1UECxMWVC1UZWxl
|
||||
U2VjIFRydXN0IENlbnRlcjEjMCEGA1UEAxMaRGV1dHNjaGUgVGVsZWtvbSBSb290
|
||||
IENBIDIwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCrC6M14IspFLEU
|
||||
ha88EOQ5bzVdSq7d6mGNlUn0b2SjGmBmpKlAIoTZ1KXleJMOaAGtuU1cOs7TuKhC
|
||||
QN/Po7qCWWqSG6wcmtoIKyUn+WkjR/Hg6yx6m/UTAtB+NHzCnjwAWav12gz1Mjwr
|
||||
rFDa1sPeg5TKqAyZMg4ISFZbavva4VhYAUlfckE8FQYBjl2tqriTtM2e66foai1S
|
||||
NNs671x1Udrb8zH57nGYMsRUFUQM+ZtV7a3fGAigo4aKSe5TBY8ZTNXeWHmb0moc
|
||||
QqvF1afPaA+W5OFhmHZhyJF81j4A4pFQh+GdCuatl9Idxjp9y7zaAzTVjlsB9WoH
|
||||
txa2bkp/AgMBAAGjQjBAMB0GA1UdDgQWBBQxw3kbuvVT1xfgiXotF2wKsyudMzAP
|
||||
BgNVHRMECDAGAQH/AgEFMA4GA1UdDwEB/wQEAwIBBjANBgkqhkiG9w0BAQUFAAOC
|
||||
AQEAlGRZrTlk5ynrE/5aw4sTV8gEJPB0d8Bg42f76Ymmg7+Wgnxu1MM9756Abrsp
|
||||
tJh6sTtU6zkXR34ajgv8HzFZMQSyzhfzLMdiNlXiItiJVbSYSKpk+tYcNthEeFpa
|
||||
IzpXl/V6ME+un2pMSyuOoAPjPuCp1NJ70rOo4nI8rZ7/gFnkm0W09juwzTkZmDLl
|
||||
6iFhkOQxIY40sfcvNUqFENrnijchvllj4PKFiDFT1FQUhXB59C4Gdyd1Lx+4ivn+
|
||||
xbrYNuSD7Odlt79jWvNGr4GUN9RBjNYj1h7P9WgbRGOiWrqnNVmh5XAFmw4jV5mU
|
||||
Cm26OWMohpLzGITY+9HPBVZkVw==
|
||||
-----END CERTIFICATE-----
|
||||
`
|
||||
|
||||
func TestLoadCertPool(t *testing.T) {
|
||||
var err error
|
||||
var cp *x509.CertPool
|
||||
var fd *os.File
|
||||
var path string
|
||||
|
||||
// attemp to load a non-existent file: should fail
|
||||
cp, err = LoadCertPool("non/existent/path/to/store")
|
||||
if err == nil {
|
||||
t.Errorf("LoadCertPool() should have failed")
|
||||
}
|
||||
|
||||
// create an empty file and attempt to load it: should fail
|
||||
fd, err = ioutil.TempFile("", "modbus_tls_utils_test")
|
||||
if err != nil {
|
||||
t.Errorf("failed to create temp file: %v", err)
|
||||
return
|
||||
}
|
||||
path = fd.Name()
|
||||
|
||||
defer os.Remove(path)
|
||||
err = fd.Close()
|
||||
if err != nil {
|
||||
t.Errorf("failed to close temp file: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
cp, err = LoadCertPool(path)
|
||||
if err == nil {
|
||||
t.Errorf("LoadCertPool() should have failed")
|
||||
}
|
||||
|
||||
// put garbage into a file and attempt to load it: should fail
|
||||
fd, err = ioutil.TempFile("", "modbus_tls_utils_test")
|
||||
if err != nil {
|
||||
t.Errorf("failed to create temp file: %v", err)
|
||||
}
|
||||
path = fd.Name()
|
||||
|
||||
defer os.Remove(path)
|
||||
_, err = fd.Write([]byte("somejunk"))
|
||||
if err != nil {
|
||||
t.Errorf("failed to write to temp file: %v", err)
|
||||
}
|
||||
err = fd.Close()
|
||||
if err != nil {
|
||||
t.Errorf("failed to close temp file: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
cp, err = LoadCertPool(path)
|
||||
if err == nil {
|
||||
t.Errorf("LoadCertPool() should have failed")
|
||||
}
|
||||
|
||||
// now write two certs to a file and try to load it: should succeed
|
||||
fd, err = ioutil.TempFile("", "modbus_tls_utils_test")
|
||||
if err != nil {
|
||||
t.Errorf("failed to create temp file: %v", err)
|
||||
}
|
||||
path = fd.Name()
|
||||
|
||||
defer os.Remove(path)
|
||||
_, err = fd.Write([]byte(validCerts))
|
||||
if err != nil {
|
||||
t.Errorf("failed to write to temp file: %v", err)
|
||||
}
|
||||
err = fd.Close()
|
||||
if err != nil {
|
||||
t.Errorf("failed to close temp file: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
cp, err = LoadCertPool(path)
|
||||
if err != nil {
|
||||
t.Errorf("LoadCertPool() should have succeeded, got: %v", err)
|
||||
}
|
||||
|
||||
// expect two certs in the cert pool
|
||||
if len(cp.Subjects()) != 2 {
|
||||
t.Errorf("expected 2 certs in the pool, saw: %v", len(cp.Subjects()))
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
18
transport.go
Normal file
18
transport.go
Normal file
@@ -0,0 +1,18 @@
|
||||
package modbus
|
||||
|
||||
type transportType uint
|
||||
const (
|
||||
modbusRTU transportType = 1
|
||||
modbusRTUOverTCP transportType = 2
|
||||
modbusRTUOverUDP transportType = 3
|
||||
modbusTCP transportType = 4
|
||||
modbusTCPOverTLS transportType = 5
|
||||
modbusTCPOverUDP transportType = 6
|
||||
)
|
||||
|
||||
type transport interface {
|
||||
Close() (error)
|
||||
ExecuteRequest(*pdu) (*pdu, error)
|
||||
ReadRequest() (*pdu, error)
|
||||
WriteResponse(*pdu) (error)
|
||||
}
|
||||
102
udp.go
Normal file
102
udp.go
Normal file
@@ -0,0 +1,102 @@
|
||||
package modbus
|
||||
|
||||
import (
|
||||
"net"
|
||||
"time"
|
||||
)
|
||||
|
||||
// udpSockWrapper wraps a net.UDPConn (UDP socket) to
|
||||
// allow transports to consume data off the network socket on
|
||||
// a byte per byte basis rather than datagram by datagram.
|
||||
type udpSockWrapper struct {
|
||||
leftoverCount int
|
||||
rxbuf []byte
|
||||
sock *net.UDPConn
|
||||
}
|
||||
|
||||
func newUDPSockWrapper(sock net.Conn) (usw *udpSockWrapper) {
|
||||
usw = &udpSockWrapper{
|
||||
rxbuf: make([]byte, maxTCPFrameLength),
|
||||
sock: sock.(*net.UDPConn),
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func (usw *udpSockWrapper) Read(buf []byte) (rlen int, err error) {
|
||||
var copied int
|
||||
|
||||
if usw.leftoverCount > 0 {
|
||||
// if we're holding onto any bytes from a previous datagram,
|
||||
// use them to satisfy the read (potentially partially)
|
||||
copied = copy(buf, usw.rxbuf[0:usw.leftoverCount])
|
||||
|
||||
if usw.leftoverCount > copied {
|
||||
// move any leftover bytes to the beginning of the buffer
|
||||
copy(usw.rxbuf, usw.rxbuf[copied:usw.leftoverCount])
|
||||
}
|
||||
// make a note of how many leftover bytes we have in the buffer
|
||||
usw.leftoverCount -= copied
|
||||
} else {
|
||||
// read up to maxTCPFrameLength bytes from the socket
|
||||
rlen, err = usw.sock.Read(usw.rxbuf)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
// copy as many bytes as possible to satisfy the read
|
||||
copied = copy(buf, usw.rxbuf[0:rlen])
|
||||
|
||||
if rlen > copied {
|
||||
// move any leftover bytes to the beginning of the buffer
|
||||
copy(usw.rxbuf, usw.rxbuf[copied:rlen])
|
||||
}
|
||||
// make a note of how many leftover bytes we have in the buffer
|
||||
usw.leftoverCount = rlen - copied
|
||||
}
|
||||
|
||||
rlen = copied
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func (usw *udpSockWrapper) Close() (err error) {
|
||||
err = usw.sock.Close()
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func (usw *udpSockWrapper) Write(buf []byte) (wlen int, err error) {
|
||||
wlen, err = usw.sock.Write(buf)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func (usw *udpSockWrapper) SetDeadline(deadline time.Time) (err error) {
|
||||
err = usw.sock.SetDeadline(deadline)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func (usw *udpSockWrapper) SetReadDeadline(deadline time.Time) (err error) {
|
||||
err = usw.sock.SetReadDeadline(deadline)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func (usw *udpSockWrapper) SetWriteDeadline(deadline time.Time) (err error) {
|
||||
err = usw.sock.SetWriteDeadline(deadline)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func (usw *udpSockWrapper) LocalAddr() (addr net.Addr) {
|
||||
addr = usw.sock.LocalAddr()
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func (usw *udpSockWrapper) RemoteAddr() (addr net.Addr) {
|
||||
addr = usw.sock.RemoteAddr()
|
||||
|
||||
return
|
||||
}
|
||||
166
udp_test.go
Normal file
166
udp_test.go
Normal file
@@ -0,0 +1,166 @@
|
||||
package modbus
|
||||
|
||||
import (
|
||||
"net"
|
||||
"os"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func TestUDPSockWrapper(t *testing.T) {
|
||||
var err error
|
||||
var usw *udpSockWrapper
|
||||
var sock1 *net.UDPConn
|
||||
var sock2 *net.UDPConn
|
||||
var addr *net.UDPAddr
|
||||
var txchan chan []byte
|
||||
var rxbuf []byte
|
||||
var count int
|
||||
|
||||
addr, err = net.ResolveUDPAddr("udp", "localhost:5502")
|
||||
if err != nil {
|
||||
t.Errorf("failed to resolve udp address: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
txchan = make(chan []byte, 4)
|
||||
// get a pair of UDP sockets ready to talk to each other
|
||||
sock1, err = net.ListenUDP("udp", addr)
|
||||
if err != nil {
|
||||
t.Errorf("failed to listen on udp socket: %v", err)
|
||||
return
|
||||
}
|
||||
err = sock1.SetReadDeadline(time.Now().Add(1 * time.Second))
|
||||
if err != nil {
|
||||
t.Errorf("failed to set deadline on udp socket: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
sock2, err = net.DialUDP("udp", nil, addr)
|
||||
if err != nil {
|
||||
t.Errorf("failed to open udp socket: %v", err)
|
||||
return
|
||||
}
|
||||
// the feedTestPipe goroutine will forward any slice of bytes
|
||||
// pushed into txchan over UDP to our test UDP sock wrapper object
|
||||
go feedTestPipe(t, txchan, sock2)
|
||||
|
||||
usw = newUDPSockWrapper(sock1)
|
||||
// push a valid RTU response (illegal data address) to the test pipe
|
||||
txchan <- []byte{
|
||||
0x31, 0x82, // unit id and response code
|
||||
0x02, // exception code
|
||||
0xc1, 0x6e, // CRC
|
||||
}
|
||||
// then push random junk
|
||||
txchan <-[]byte{
|
||||
0xaa, 0xbb, 0xcc,
|
||||
}
|
||||
// then some more
|
||||
txchan <-[]byte{
|
||||
0xdd, 0xee,
|
||||
}
|
||||
|
||||
// attempt to read 3 bytes: we should get them as the first datagram
|
||||
// is 5 bytes long
|
||||
rxbuf = make([]byte, 3)
|
||||
count, err = usw.Read(rxbuf)
|
||||
if err != nil {
|
||||
t.Errorf("usw.Read() should have succeeded, got: %v", err)
|
||||
}
|
||||
if count != 3 {
|
||||
t.Errorf("expected 3 bytes, got: %v", count)
|
||||
}
|
||||
for idx, val := range []byte{
|
||||
0x31, 0x82, 0x02,
|
||||
} {
|
||||
if rxbuf[idx] != val {
|
||||
t.Errorf("expected 0x%02x at pos %v, got: 0x%02x",
|
||||
val, idx, rxbuf[idx])
|
||||
}
|
||||
}
|
||||
|
||||
// attempt to read 1 byte: we should get the 4th byte of the
|
||||
// first datagram, of which we've been holding on to bytes #4 and 5
|
||||
rxbuf = make([]byte, 1)
|
||||
count, err = usw.Read(rxbuf)
|
||||
if err != nil {
|
||||
t.Errorf("usw.Read() should have succeeded, got: %v", err)
|
||||
}
|
||||
if count != 1 {
|
||||
t.Errorf("expected 1 byte, got: %v", count)
|
||||
}
|
||||
if rxbuf[0] != 0xc1 {
|
||||
t.Errorf("expected 0xc1 at pos 0, got: 0x%02x", rxbuf[0])
|
||||
}
|
||||
|
||||
// attempt to read 5 bytes: we should get the last byte of the
|
||||
// first datagram, which the udpSockWrapper object still holds in
|
||||
// its buffer
|
||||
rxbuf = make([]byte, 5)
|
||||
count, err = usw.Read(rxbuf)
|
||||
if err != nil {
|
||||
t.Errorf("usw.Read() should have succeeded, got: %v", err)
|
||||
}
|
||||
if count != 1 {
|
||||
t.Errorf("expected 1 byte, got: %v", count)
|
||||
}
|
||||
if rxbuf[0] != 0x6e {
|
||||
t.Errorf("expected 0x6e at pos 0, got: 0x%02x", rxbuf[0])
|
||||
}
|
||||
|
||||
// attempt to read 10 bytes: we should get all 3 bytes of the 2nd
|
||||
// datagram
|
||||
rxbuf = make([]byte, 10)
|
||||
count, err = usw.Read(rxbuf)
|
||||
if err != nil {
|
||||
t.Errorf("usw.Read() should have succeeded, got: %v", err)
|
||||
}
|
||||
if count != 3 {
|
||||
t.Errorf("expected 3 bytes, got: %v", count)
|
||||
}
|
||||
for idx, val := range []byte{
|
||||
0xaa, 0xbb, 0xcc,
|
||||
} {
|
||||
if rxbuf[idx] != val {
|
||||
t.Errorf("expected 0x%02x at pos %v, got: 0x%02x",
|
||||
val, idx, rxbuf[idx])
|
||||
}
|
||||
}
|
||||
|
||||
// attempt to read 40 bytes: we should get both bytes of the 3rd
|
||||
// datagram
|
||||
rxbuf = make([]byte, 40)
|
||||
count, err = usw.Read(rxbuf)
|
||||
if err != nil {
|
||||
t.Errorf("usw.Read() should have succeeded, got: %v", err)
|
||||
}
|
||||
if count != 2 {
|
||||
t.Errorf("expected 2 bytes, got: %v", count)
|
||||
}
|
||||
for idx, val := range []byte{
|
||||
0xdd, 0xee,
|
||||
} {
|
||||
if rxbuf[idx] != val {
|
||||
t.Errorf("expected 0x%02x at pos %v, got: 0x%02x",
|
||||
val, idx, rxbuf[idx])
|
||||
}
|
||||
}
|
||||
|
||||
// attempt to read 7 bytes: we should get a read timeout as we've
|
||||
// consumed all bytes from all datagrams and no more are coming
|
||||
rxbuf = make([]byte, 7)
|
||||
count, err = usw.Read(rxbuf)
|
||||
if !os.IsTimeout(err) {
|
||||
t.Errorf("usw.Read() should have failed with a timeout error, got: %v", err)
|
||||
}
|
||||
if count != 0 {
|
||||
t.Errorf("expected 0 bytes, got: %v", count)
|
||||
}
|
||||
|
||||
// cleanup
|
||||
sock1.Close()
|
||||
sock2.Close()
|
||||
|
||||
return
|
||||
}
|
||||
Reference in New Issue
Block a user