Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

MODBUS/TCP Security support (TLS) #133

Closed
gustavowd opened this issue Nov 10, 2022 · 6 comments · Fixed by #159
Closed

MODBUS/TCP Security support (TLS) #133

gustavowd opened this issue Nov 10, 2022 · 6 comments · Fixed by #159

Comments

@gustavowd
Copy link
Contributor

gustavowd commented Nov 10, 2022

Hi. Congrats for the project.
Is it possible to add support for the new specification, the Modbus TCP Security, based on TLS?

Some of new protocols based on modbus are migrating for this modbus version, as the sunspec modbus protocol. The specification is available at:
https://modbus.org/docs/MB-TCP-Security-v21_2018-07-24.pdf

I beleive it can be implemented based on tokio-rustls.

Thanks in advance.
Gustavo Denardin

@uklotzde
Copy link
Member

Thanks for the proposal.

This is a community project and contributions are always welcome. Don't hesitate to open a Draft PR for early discussion.

@flosse flosse added the tcp label Nov 11, 2022
@gustavowd
Copy link
Contributor Author

Hi uklotzde,

I'm new to the rust development, but I succeeded in implement a simple tls server using some crates from the tokio modbus project. However i do not complete understand how to integrate this tls server to the project code in order to do a pull request. I will post the code here to contribute with the project.

I have tested the server with a client written with C and libmodbus. It's working.

main.rs:
use argh::FromArgs;
use rustls_pemfile::{certs, ec_private_keys};
use std::fs::File;
use std::io::{self, BufReader};
use std::net::ToSocketAddrs;
use std::path::{Path, PathBuf};
use std::sync::Arc;
use std::convert::From;
use tokio::net::TcpListener;
use tokio_rustls::rustls::{self, Certificate, PrivateKey};
use tokio_rustls::TlsAcceptor;
use tokio_util::codec::{Framed};
use futures_util::{future, sink::SinkExt as _, stream::StreamExt as _};

mod slave;
mod codec;
mod frame;
mod service;

use crate::frame::{Response, ResponsePdu, ExceptionResponse, Request};

/// Tokio Rustls server example
#[derive(FromArgs)]
struct Options {
/// bind addr
#[argh(positional)]
addr: String,

/// cert file
#[argh(option, short = 'c')]
cert: PathBuf,

/// key file
#[argh(option, short = 'k')]
key: PathBuf,

}

fn load_certs(path: &Path) -> io::Result<Vec> {
certs(&mut BufReader::new(File::open(path)?))
.map_err(|_| io::Error::new(io::ErrorKind::InvalidInput, "invalid cert"))
.map(|mut certs| certs.drain(..).map(Certificate).collect())
}

fn load_keys(path: &Path) -> io::Result<Vec> {
ec_private_keys(&mut BufReader::new(File::open(path)?))
.map_err(|_| io::Error::new(io::ErrorKind::InvalidInput, "invalid key"))
.map(|mut keys| keys.drain(..).map(PrivateKey).collect())
}

#[tokio::main]
async fn main() -> io::Result<()> {
let options: Options = argh::from_env();

let addr = options
    .addr
    .to_socket_addrs()?
    .next()
    .ok_or_else(|| io::Error::from(io::ErrorKind::AddrNotAvailable))?;

let certs = load_certs(&options.cert)?;
let mut keys = load_keys(&options.key)?;
//println!("{:?}", keys);
let config = rustls::ServerConfig::builder()
    .with_safe_defaults()
    .with_no_client_auth()
    .with_single_cert(certs, keys.remove(0))
    .map_err(|err| io::Error::new(io::ErrorKind::InvalidInput, err))?;
let acceptor = TlsAcceptor::from(Arc::new(config));

let listener = TcpListener::bind(&addr).await?;

loop {
    let (stream, peer_addr) = listener.accept().await?;
    let acceptor = acceptor.clone();

    let fut = async move {
        let stream = acceptor.accept(stream).await?;
        let mut framed = Framed::new(stream, codec::tls::ServerCodec::default());

        let request = framed.next().await;
        println!("{:?}", request);
        let request = request.unwrap();
        let request = request.unwrap();

        let hdr = request.hdr;
        let req: Request = request.into();
        println!("{:?}", req);
        let registers = vec![0; 1];
        let mut future: future::Ready<Result<Response, io::Error>> = future::ready(Ok::<frame::Response, io::Error>(frame::Response::ReadHoldingRegisters(registers)));
        let mut future2: future::Ready<Result<ExceptionResponse, io::Error>> = future::ready(Ok::<frame::ExceptionResponse, io::Error>(frame::ExceptionResponse { function: 0x03, exception: crate::frame::Exception::IllegalDataAddress }));
        let mut error: bool = false;

        match req {
            Request::ReadHoldingRegisters(addr, cnt) => {
                if (addr > 50) || ((addr + cnt) > 50) {
                    error = true;
                    future2 = future::ready(Ok::<frame::ExceptionResponse, io::Error>(frame::ExceptionResponse { function: 0x03, exception: crate::frame::Exception::IllegalDataAddress }));
                }else{
                    let mut registers = vec![0; cnt.into()];
                    registers[0] = 77;
                    future = future::ready(Ok::<frame::Response, io::Error>(frame::Response::ReadHoldingRegisters(registers)));
                }
            },
            Request::ReadInputRegisters(addr, cnt) => {
                if (addr > 50) || ((addr + cnt) > 50) {
                    error = true;
                    future2 = future::ready(Ok::<frame::ExceptionResponse, io::Error>(frame::ExceptionResponse { function: 0x04, exception: crate::frame::Exception::IllegalDataAddress }));
                }else{
                    let mut registers = vec![0; cnt.into()];
                    registers[0] = 78;
                    future = future::ready(Ok::<frame::Response, io::Error>(frame::Response::ReadInputRegisters(registers)));
                }
            },
            _ => unimplemented!(),
        }

        if error == false {
            let response = future.await?;
            let pdu: ResponsePdu = ResponsePdu::from(response);
            let resp = frame::tls::ResponseAdu { hdr, pdu };
            let result = framed.send(resp).await;
            println!("Result {} - {:?}", peer_addr, result);
        }else{
            let response = future2.await?;
            let pdu: ResponsePdu = ResponsePdu::from(response);
            let resp = frame::tls::ResponseAdu { hdr, pdu };
            let result = framed.send(resp).await;
            println!("Result {} - {:?}", peer_addr, result);
        }

        Ok(()) as io::Result<()>
    };

    tokio::spawn(async move {
        if let Err(err) = fut.await {
            eprintln!("{:?}", err);
        }
    });
}

}

cargo.toml:
[package]
name = "tokio-tls-server"
version = "0.1.0"
edition = "2021"

See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html

[dependencies]
futures = "0.3.26"
futures-util = "0.3.26"
tokio = { version = "1.25.0", features = [ "full" ] }
argh = "0.1.10"
rustls-pemfile = "1.0.2"
tokio-rustls = "0.23.4"
byteorder = "1.4.3"
bytes = "1.4.0"
tokio-util = { version = "0.7.7", default-features = false, features = [
"codec",
] }

@gustavowd
Copy link
Contributor Author

gustavowd commented Feb 23, 2023

As I understand it, this code can help with the development of a TLS server for the project, as it demonstrates how to create a TLS connection, accept new connections, and receive and send packets over TLS.

Basically is the same of tcp server, to read a new modbus frame is just to execute this line with the TLS connection:
let request = framed.next().await;

@uklotzde
Copy link
Member

uklotzde commented Mar 6, 2023

The client-side support for TLS still needs to be resolved. I missed that aspect.

To keep thinks manageable I created a new, focused issue #161 and added a back link to this issue.

@uklotzde
Copy link
Member

uklotzde commented Mar 6, 2023

I noticed that we still need to abstract over both TlsStream and TcpStream to achieve an agnostic implementation for both client and server. My bad.

As a prerequisite #119 needs to be implemented first.

@uklotzde uklotzde reopened this Mar 6, 2023
@uklotzde uklotzde changed the title MODBUS/TCP Security support MODBUS/TCP Security support (TLS) Mar 6, 2023
@uklotzde
Copy link
Member

The addition of both TLS client and server examples demonstrate that TLS support could be added as an additional layer on top of the foundational Modbus code.

A fully configurable, batteries included approach enabled by a Cargo feature would be handy. But as long as no one is able to provide a solid and flexible implementation that is suitable for covering (almost) all practical use cases we should close this issue.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging a pull request may close this issue.

3 participants