forked from simonvetter/modbus
-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathtls_server.go
255 lines (229 loc) · 8.91 KB
/
tls_server.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
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
}