This is an OPC UA server / client API implemented in Rust.
OPC UA is an industry standard for monitoring of data. It's used extensively for embedded devices, industrial control, IoT, etc. - just about anything that has data that something else wants to monitor, control or visualize.
Rust is a systems programming language and is therefore a natural choice for implementing OPC UA. This implementation will support the embedded profile.
The code is licenced under MPL-2.0. Like all open source code, you use this code at your own risk.
If you want to get stuck in, there are a number of samples in the samples/ folder. The simple-client
and the simple-server
projects are
minimal client and server programs respectively.
# In one bash
cd opcua/samples/simple-server
cargo run
# In another bash
cd opcua/samples/simple-client
cargo run
The full list of samples:
simple-server
- an OPC UA server that adds 4 variables v1, v2, v3 and v4 and updates them from a timer via push and pull mechanismssimple-client
- an OPC UA client that connects to a server and requests the values of v1, v2, v3 and v4. It may also subscribe to changes to these values.discovery-client
- an OPC UA client that connects to a discovery server and lists the servers registered on itchess-server
- an OPC UA server that connects to a chess engine as its back end and updates variables representing the state of the game.demo-server
- an OPC UA server that will implements more functionality than the simple server and may become a compliance server in time.
There are also a couple of node-opcua scripts under 3rd-party which behave in a similar fashion to simple-client
and
simple-server
.
This allows behaviour to be compared with an independently written OPC UA implementation.
node-opcua-client
- an OPC UA client that connects to a server and subscribes to v1, v2, v3, and v4.node-opcua-server
- an OPC UA server that exposes v1, v2, v3 and v4 and changes them from a timer.
This implementation will implement the opc.tcp:// binary format. It will not implement OPC UA over XML. XML hasn't see much adoption so this is no great impediment. Binary over https:// might happen at a later time.
The server shall implement the OPC UA capabilities:
- http://opcfoundation.org/UA-Profile/Server/Behaviour - base server profile
- http://opcfoundation.org/UA-Profile/Server/EmbeddedUA - embedded UA profile
The following services are supported:
-
Discovery service set
- GetEndpoints
-
Attribute service set
- Read
- Write
-
Session service set
- CreateSession
- ActivateSession
- CloseSession
-
View service set
- Browse
- BrowseNext
- TranslateBrowsePathsToNodeIds
-
MonitoredItem service set
- CreateMonitoredItems - Data change filter including dead band filtering.
- ModifyMonitoredItems
- DeleteMonitoredItems
- SetMonitoringMode
-
Subscription service set
- CreateSubscription
- ModifySubscription
- DeleteSubscriptions
- Publish
- Republish
- SetPublishingMode
Other service calls are unsupported. Calling an unsupported service will terminate the session.
The standard OPC UA address space is exposed. OPC UA for Rust uses a script to generate code to create and populate the standard address space.
Currently the following are not supported
- Diagnostic info. OPC UA allows for you to ask for diagnostics with any request. None is supplied at this time
- Session resumption. If your client disconnects, all information is discarded.
- Default nodeset is mostly static. Certain fields of server information will contain their default values unless explicitly set.
The client shall provide synchrononous and asynchronous calls corresponding to the functionality of the server. It will also support these additional calls.
- FindServers - to discover servers from a discovery server
- RegisterServer - for servers to register themselves with a discovery server
Server and client can be configured programmatically or by configuration file. See the samples/
folder for examples
of client and server side configuration. The config files are specified in YAML.
Server and client support endpoints with the standard message security modes:
- None - no encryption
- Sign - no encryption but messages are digitally signed to ensure integrity
- SignAndEncrypt - signed messages which are then encrypted
The following security policies are supported.
- None (no encryption)
- Basic128Rsa15
- Basic256
- Basic256Rsa256
The server and client support the following user identities
- Anonymous/None, i.e. no authentication
- User/password - plaintext password only
User/pass identities are defined by configuration
- Install latest stable rust, e.g. using rustup
- Install gcc and OpenSSL development libs & headers.
On Linux this should be straightforward. On Windows, read below.
You need OpenSSL to build OPC UA. The easiest way is to install the stable-x86_64-pc-windows-gnu Rust toolchain
and then install MSYS2 64-bit. Read the instructions on the site especially on updating to the
latest packages via pacman -Syuu
.
Once MSYS2 has installed & updated you must install the MingW 64-bit compiler toolchain and OpenSSL packages.
pacman -S gcc mingw-w64-x86_64-gcc mingw-w64-x86_64-gdb mingw-w64-x86_64-pkg-config openssl openssl-devel pkg-config
Now ensure that these ensure both Rust and MinGW64 binaries are on your PATH and you should be ready:
set PATH=C:\msys64\mingw64\bin;C:\Users\MyName\.cargo\bin;%PATH%
OPC UA for Rust follows the normal Rust conventions. There is a Cargo.toml per module that you may use to build the module and all dependencies. You may also build the entire workspace from the top like so:
cd opcua
cargo build
OPC UA for Rust uses cryptographic algorithms for signing, verifying, encrypting and decrypting data. In addition it creates, loads and saves certificates and keys.
OpenSSL is used for this purpose although it would be nicer to go to a more pure Rust implementation. To that end most of the crypto+OpenSSL code is abstracted to make it easier to remove in the future.
You are advised to read the OpenSSL documentation to set up your environment.
Note: It should be possible to build using MSVC and link to a OpenSSL binary lib but you should read the Rust OpenSSL docs for how to set up your paths properly.
The server / client uses the following directory structure to manage trusted/rejected certificates:
pki/
own/
cert.der - your server/client's public certificate
private/
key.pem - your server/client's private key
trusted/
... - contains certs from client/servers you've connected with and you trust
rejected/
... - contains certs from client/servers you've connected with and you don't trust
For encrypted connections the following applies:
- The server will reject the first connection from an unrecognized client. It will create a file representing
the cert in its the
pki/rejected/
folder and you, the administrator must move the cert to thetrusted/
folder to permit connections from that client in future. - Likewise, the client shall reject unrecognized servers in the same fashion, and the cert must be moved from the
rejected/
totrusted/
folder for connection to succeed.
The tools/certificate-creator
tool will create a demo public self-signed cert and private key.
It can be built from source, or the crate:
cargo install --force opcua-certificate-creator
A minimal usage might be something like this inside samples/simple-client and/or samples/simple-server:
opcua-certificate-creator --pkipath ./pki
A full list of arguments can be obtained by --help
and you are advised to set fields such
as expiration length, description, country code etc to your requirements.
The API will use convention and idiomatic rust minimize and make concise the amount of code that needs to be written.
Here is a minimal, functioning server.
extern crate opcua_types;
extern crate opcua_core;
extern crate opcua_server;
use std::sync::{Arc, RwLock};
use opcua_server::prelude::*;
fn main() {
Server::run(Arc::new(RwLock::new(Server::new_default())));
}
This server will accept connections, allow you to browse the address space and subscribe to variables.
Refer to the samples/simple-server/
and samples/simple-client/
examples for something that adds variables to the address space and changes their values.
Fundamental types are implemented by hand. Structures such as requests/responses are machine generated by script.
The tools/schema/
directory contains NodeJS scripts that will generate the following from OPC UA schemas.
- Status codes
- Node Ids (objects, variables, references etc.)
- Data structures including serialization.
- Request and Response messages including serialization
- Address space
All OPC UA enums, structs, fields, constants etc. will conform to Rust lint rules where it makes sense.
i.e. OPC UA uses pascal case for field names but the impl will use snake case, for example requestHeader
is defined
as request_header
.
struct OpenSecureChannelRequest {
pub request_header: RequestHeader
}
The OPC UA type SecurityPolicy value INVALID_0
will an enum SecurityPolicy
with a value Invalid
with a scalar value
of 0.
pub enum SecurityPolicy {
Invalid = 0,
None = 1
...
}
The enum will be turned in and out of a scalar value during serialization via a match.
Wherever possible Rust idioms will be used - enums, options and other conveniences of the language will be used to represent data in the most efficient and strict way possible. e.g. here is the ExtensionObject
#[derive(PartialEq, Debug, Clone)]
pub enum ExtensionObjectEncoding {
None,
ByteString(ByteString),
XmlElement(XmlElement),
}
/// A structure that contains an application specific data type that may not be recognized by the receiver.
/// Data type ID 22
#[derive(PartialEq, Debug, Clone)]
pub struct ExtensionObject {
pub node_id: NodeId,
pub body: ExtensionObjectEncoding,
}
Rust enables the body
payload to be None
, ByteString
or XmlElement
and this is handled during serialization.
Certain enums use Boxed types to avoiding being overly large. e.g. the Variant enum boxes complex values such as DataValue, arrays etc. to prevent being too bloated.
OPC UA has some some really long PascalCase ids, many of which are further broken up by underscores. I've tried converting the name to upper snake and they look terrible. I've tried removing underscores and they look terrible.
So the names and underscores are preserved as-in in generated code even though they generate lint errors. The lint rules are disabled for generated code.
For example:
#[allow(non_camel_case_types)]
#[derive(Debug, PartialEq, Copy, Clone)]
pub enum VariableId {
//... thousands of ids, many like this or worse
ExclusiveRateOfChangeAlarmType_LimitState_LastTransition_EffectiveTransitionTime = 11474,
}
All status codes will be values within the StatusCode
enum. Values such as Good
, BadUnexpectedError
etc.
The enum will also implement Copy
so that status codes are copy on assign. The enum provides helpers is_good()
,
is_bad()
, name()
and description()
for testing and debugging purposes.
All code (with the exceptions noted for OPC UA) should be follow the most current Rust RFC coding guidelines for naming conventions, layout etc.
Code should be formatted with the IntelliJ rust plugin, or with rustfmt.
Client and server will work their ways through OPC UA profiles to the point of usability. But presently they are working towards.
- Nano Embedded Device Server Profile, which has these main points
- UA-TCP binary
- SecurityPolicy of None (i.e. no encryption / signing)
- Username / Password support (plaintext)
- Address space
- Discovery Services
- Session Services (minimum, single session)
- View Services (basic)
- Micro Embedded Device Server Profile. This is a bump up from Nano.
- UA secure conversation
- 2 or more sessions
- Data change notifications via a subscription.
- Embedded UA Server Profile
- Standard data change notifications via a subscription
- Queueing
- Deadband filter
- ! No CallMethod service
- ! No GetMonitoredItems via call
- ! No ResendData via call
- Standard data change notifications via a subscription
This OPC UA link provides interactive and descriptive information about profiles and relevant test cases.
See the CHANGELOG.md for changes per version as well as short-term & aspirational wishlist.
- log - for logging / auditing
- openssl - cryptographic functions for signing, certifications and encryption/decryption
- serde, server_yaml - for processing config files
- byteorder - for serializing values with the proper endian-ness
- chrono - for high quality time functions
- time - for some types that chrono still uses, e.g. Duration
- random - for random number generation in some places
The plan is for unit tests for at least the following
- All data types, request and response types will be covered by a serialization
- Chunking messages together, handling errors, buffer limits, multiple chunks
- Limit validation on string, array fields which have size limits
- OpenSecureChannel, CloseSecureChannel request and response
- Service calls
- Sign, verify, encrypt and decrypt (when implemented)
- Data change filters
- Subscription state engine
- Encryption
Integration testing shall wait for client and server to be complete. At that point it shall be possible to write a unit test that initiates a connection from a client to a server and simulates scenarios such as.
- Discovery service
- Connect / disconnect
- Create session
- Subscribe to values
- Encryption (when implemented)
See this OPC UA link and click on the test case links associated with facets.
There are a lot of tests. Any that can be sanely automated or covered by unit / integration tests will be. The project will not be a slave to these tests, but it will try to ensure compatibility.
The best way to test is to build the sample-server and use a 3rd party client to connect to it.
If you have NodeJS then the easiest 3rd party client to get going with is node-opcua opc-commander client.
npm install -g opcua-commander
Then build and run the sample server:
cd sample-server
cargo run
And in another shell
opcua-commander -e opc.tcp://localhost:4855