Skip to content

Commit

Permalink
Add support for Modbus over TLS (Modbus/TCP Security) (sunspec#97)
Browse files Browse the repository at this point in the history
  • Loading branch information
activeshadow authored Jul 2, 2020
1 parent 50cb7d1 commit ab80e90
Show file tree
Hide file tree
Showing 2 changed files with 100 additions and 6 deletions.
52 changes: 47 additions & 5 deletions sunspec/core/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,27 @@ class ClientDevice(device.Device):
For :const:`TCP` devices, device IP port. Defaulted by modbus module
to 502.
tls :
For :const:`TCP` devices, use TLS (Modbus/TCP Security). Defaults
to `tls=False`.
cafile :
For :const:`TCP` devices, path to certificate authority (CA)
certificate to use for validating server certificates. Only used
if `tls=True`.
certfile :
For :const:`TCP` devices, path to client TLS certificate to use
for client authentication. Only used if `tls=True`.
keyfile :
For :const:`TCP` devices, path to client TLS key to use for
client authentication. Only used if `tls=True`.
insecure_skip_tls_verify :
Skip verification of server TLS certificate. Only used if
`tls=True`.
timeout :
Modbus request timeout in seconds. Fractional seconds are permitted
such as .5.
Expand Down Expand Up @@ -120,8 +141,8 @@ class ClientDevice(device.Device):
first time.
"""

def __init__(self, device_type, slave_id=None, name=None, pathlist = None, baudrate=None, parity=None, ipaddr=None, ipport=None,
timeout=None, trace=False):
def __init__(self, device_type, slave_id=None, name=None, pathlist=None, baudrate=None, parity=None, ipaddr=None, ipport=None,
tls=False, cafile=None, certfile=None, keyfile=None, insecure_skip_tls_verify=False, timeout=None, trace=False):
device.Device.__init__(self, addr=None)

self.type = device_type
Expand All @@ -136,7 +157,7 @@ def __init__(self, device_type, slave_id=None, name=None, pathlist = None, baudr
if device_type == RTU:
self.modbus_device = modbus.ModbusClientDeviceRTU(slave_id, name, baudrate, parity, timeout, self, trace)
elif device_type == TCP:
self.modbus_device = modbus.ModbusClientDeviceTCP(slave_id, ipaddr, ipport, timeout, self, trace)
self.modbus_device = modbus.ModbusClientDeviceTCP(slave_id, ipaddr, ipport, timeout, self, trace, tls, cafile, certfile, keyfile, insecure_skip_tls_verify)
elif device_type == MAPPED:
if name is not None:
self.modbus_device = modbus.ModbusClientDeviceMapped(slave_id, name, pathlist, self)
Expand Down Expand Up @@ -735,6 +756,27 @@ class SunSpecClientDevice(object):
For :const:`TCP` devices, device IP port. Defaulted by modbus module
to 502.
tls :
For :const:`TCP` devices, use TLS (Modbus/TCP Security). Defaults
to `tls=False`.
cafile :
For :const:`TCP` devices, path to certificate authority (CA)
certificate to use for validating server certificates. Only used
if `tls=True`.
certfile :
For :const:`TCP` devices, path to client TLS certificate to use
for client authentication. Only used if `tls=True`.
keyfile :
For :const:`TCP` devices, path to client TLS key to use for
client authentication. Only used if `tls=True`.
insecure_skip_tls_verify :
Skip verification of server TLS certificate. Only used if
`tls=True`.
timeout :
Modbus request timeout in seconds. Fractional seconds are permitted
such as .5.
Expand Down Expand Up @@ -764,10 +806,10 @@ class SunSpecClientDevice(object):
"""

def __init__(self, device_type, slave_id=None, name=None, pathlist = None, baudrate=None, parity=None, ipaddr=None, ipport=None,
timeout=None, trace=False, scan_progress=None, scan_delay=None):
tls=False, cafile=None, certfile=None, keyfile=None, insecure_skip_tls_verify=False, timeout=None, trace=False, scan_progress=None, scan_delay=None):

# super(self.__class__, self).__init__(device_type, slave_id, name, pathlist, baudrate, parity, ipaddr, ipport)
self.device = ClientDevice(device_type, slave_id, name, pathlist, baudrate, parity, ipaddr, ipport, timeout, trace)
self.device = ClientDevice(device_type, slave_id, name, pathlist, baudrate, parity, ipaddr, ipport, tls, cafile, certfile, keyfile, insecure_skip_tls_verify, timeout, trace)
self.models = []

try:
Expand Down
54 changes: 53 additions & 1 deletion sunspec/core/modbus/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
"""

import os
import ssl
import socket
import struct
import serial
Expand Down Expand Up @@ -585,6 +586,25 @@ class ModbusClientDeviceTCP(object):
Trace function to use for detailed logging. No detailed logging is
perform is a trace function is not supplied.
tls :
Use TLS (Modbus/TCP Security). Defaults to `tls=False`.
cafile :
Path to certificate authority (CA) certificate to use for
validating server certificates. Only used if `tls=True`.
certfile :
Path to client TLS certificate to use for client authentication.
Only used if `tls=True`.
keyfile :
Path to client TLS key to use for client authentication. Only
used if `tls=True`.
insecure_skip_tls_verify :
Skip verification of server TLS certificate. Only used if
`tls=True`.
max_count :
Maximum register count for a single Modbus request.
Expand Down Expand Up @@ -628,18 +648,42 @@ class ModbusClientDeviceTCP(object):
trace_func
Trace function to use for detailed logging.
tls
Use TLS (Modbus/TCP Security). Defaults to `tls=False`.
cafile
Path to certificate authority (CA) certificate to use for
validating server certificates. Only used if `tls=True`.
certfile
Path to client TLS certificate to use for client authentication.
Only used if `tls=True`.
keyfile
Path to client TLS key to use for client authentication. Only
used if `tls=True`.
insecure_skip_tls_verify :
Skip verification of server TLS certificate. Only used if
`tls=True`.
max_count
Maximum register count for a single Modbus request.
"""

def __init__(self, slave_id, ipaddr, ipport=502, timeout=None, ctx=None, trace_func=None, max_count=REQ_COUNT_MAX, test=False):
def __init__(self, slave_id, ipaddr, ipport=502, timeout=None, ctx=None, trace_func=None, tls=False, cafile=None, certfile=None, keyfile=None, insecure_skip_tls_verify=False, max_count=REQ_COUNT_MAX, test=False):
self.slave_id = slave_id
self.ipaddr = ipaddr
self.ipport = ipport
self.timeout = timeout
self.ctx = ctx
self.socket = None
self.trace_func = trace_func
self.tls = tls
self.cafile = cafile
self.certfile = certfile
self.keyfile = keyfile
self.tls_verify = not insecure_skip_tls_verify
self.max_count = max_count

if ipport is None:
Expand Down Expand Up @@ -673,6 +717,14 @@ def connect(self, timeout=None):
try:
self.socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
self.socket.settimeout(timeout)

if self.tls:
context = ssl.create_default_context(ssl.Purpose.SERVER_AUTH, cafile=self.cafile)
context.load_cert_chain(certfile=self.certfile, keyfile=self.keyfile)
context.check_hostname = self.tls_verify

self.socket = context.wrap_socket(self.socket, server_side=False, server_hostname=self.ipaddr)

self.socket.connect((self.ipaddr, self.ipport))
except Exception as e:
raise ModbusClientError('Connection error: %s' % str(e))
Expand Down

0 comments on commit ab80e90

Please sign in to comment.