From 1a2e0ae9a949836943f92713463117cbacea94f9 Mon Sep 17 00:00:00 2001
From: Roberto Pastor Muela <37798125+RobPasMue@users.noreply.github.com>
Date: Mon, 24 Nov 2025 15:54:29 +0100
Subject: [PATCH 01/12] feat: cyberchannel
---
doc/source/user_guide/index.rst | 10 +-
doc/source/user_guide/secure_grpc.rst | 162 +++++++++
src/ansys/tools/common/cyberchannel.py | 479 +++++++++++++++++++++++++
3 files changed, 650 insertions(+), 1 deletion(-)
create mode 100644 doc/source/user_guide/secure_grpc.rst
create mode 100644 src/ansys/tools/common/cyberchannel.py
diff --git a/doc/source/user_guide/index.rst b/doc/source/user_guide/index.rst
index e8eda3c7..4bd7a076 100644
--- a/doc/source/user_guide/index.rst
+++ b/doc/source/user_guide/index.rst
@@ -55,6 +55,13 @@ This section explains key concepts for implementing the tools in Ansys Common To
Learn how to use the local launcher tool.
+ .. grid-item-card:: Securing gRPC connections
+ :padding: 2 2 2 2
+ :link: secure_grpc
+ :link-type: doc
+
+ Learn how to secure gRPC connections in PyAnsys.
+
.. toctree::
:hidden:
:maxdepth: 3
@@ -64,4 +71,5 @@ This section explains key concepts for implementing the tools in Ansys Common To
ansys_exceptions
versioning
report
- launcher/index
\ No newline at end of file
+ launcher/index
+ secure_grpc
diff --git a/doc/source/user_guide/secure_grpc.rst b/doc/source/user_guide/secure_grpc.rst
new file mode 100644
index 00000000..e723abbe
--- /dev/null
+++ b/doc/source/user_guide/secure_grpc.rst
@@ -0,0 +1,162 @@
+Securing gRPC connections
+#########################
+
+The PyAnsys ecosystem enables now various transport modes for securing gRPC connections.
+This page reviews the available transport modes, and how to use them.
+
+Supported transport modes
+=========================
+
+PyAnsys supports the following transport modes for gRPC connections:
+
+- **Mutual TLS (mTLS).** This mode allows secure connections using TLS
+ encryption and client/server certificates. It is recommended for production
+ use, especially when transmitting sensitive data. Works both locally and over
+ the network.
+
+- **Unix Domain Sockets (UDS).** This mode allows connections over a local
+ socket file. UDS is only supported for local inter-process communication
+ (IPC) on a machine running Linux.
+
+- **Windows Named User Authentication (WNUA).** This mode allows secure local
+ connections on Windows machines through user authentication. It is only
+ supported in Windows.
+
+- **Insecure.** This mode allows connections without any encryption or
+ authentication. It is NOT recommended for production use, but can be useful
+ for testing or development purposes.
+
+
+The ``cyberchannel`` module
+============================
+
+The ``cyberchannel`` module eases the transition to secure gRPC. It is meant to
+be used by client applications to create gRPC channels with the server.
+
+This module implements all transport modes described above. It also abstracts
+away the details of connection setup and certificate handling, making it easier
+to connect clients to gRPC servers in different environments.
+
+Example usage
+-------------
+
+.. code-block:: python
+
+ from cyberchannel import create_channel
+ import hello_pb2_grpc
+
+ channel = create_channel(
+ host="localhost", port=50051, # Channel details
+ transport_mode="mtls", certs_dir="path/to/certs", # Security details
+ grpc_options=[ # Extra details
+ ('grpc.max_receive_message_length', 50 * 1024 * 1024)
+ ],
+ )
+ stub = hello_pb2_grpc.GreeterStub(channel)
+
+API reference
+-------------
+
+.. list-table::
+ :header-rows: 1
+ :widths: 20 80
+
+ * - Function
+ - Description
+ * - ``create_channel(...)``
+ - Main entry point for users.
+ * - ``verify_transport_mode(...)``
+ - Check if selected transport mode is valid. If not, it raises an error.
+ * - ``verify_uds_socket(...)``
+ - Check if UDS socket file exists.
+
+Environment variables
+---------------------
+
+.. list-table::
+ :header-rows: 1
+ :widths: 20 60 20
+
+ * - Variable
+ - Description
+ - Default
+ * - ``ANSYS_GRPC_CERTIFICATES``
+ - Path to folder containing ``ca.crt``, ``client.crt``, and ``client.key``
+ for mTLS connections. If not set, defaults to a local ``./certs``
+ directory.
+ - ``./certs``
+
+Generating certificates for mTLS
+================================
+
+`OpenSSL `__ can be used to generate the necessary
+certificates for mTLS. The following files are required for both the server and
+the client:
+
+===== ======================= =========================================
+Side Required Files Purpose
+===== ======================= =========================================
+Server server.crt Server identity
+ server.key Server private key
+ ca.crt To verify client certificates
+Client client.crt Client identity
+ client.key Client private key
+ ca.crt To verify server certificates
+===== ======================= =========================================
+
+These files can be generated using `OpenSSL `__.
+
+Generate a certificate authority
+--------------------------------
+
+.. code-block:: bash
+
+ # Generate private key for CA
+ openssl genrsa -out ca.key 4096
+
+ # Generate self-signed CA certificate
+ openssl req -x509 -new -nodes -key ca.key -sha256 -days 3650 -out ca.crt \
+ -subj "/CN=MyRootCA"
+
+Generate the server certificate
+-------------------------------
+
+.. code-block:: bash
+
+ # Generate server private key
+ openssl genrsa -out server.key 4096
+
+ # Generate a certificate signing request (CSR) for the server
+ openssl req -new -key server.key -out server.csr \
+ -subj "/CN=localhost"
+
+ # Generate server certificate signed by the CA
+ openssl x509 -req -in server.csr -CA ca.crt -CAkey ca.key -CAcreateserial \
+ -out server.crt -days 365 -sha256
+
+Generate the client certificate
+-------------------------------
+
+.. code-block:: bash
+
+ # Generate client private key
+ openssl genrsa -out client.key 4096
+
+ # Generate a certificate signing request (CSR) for the client
+ openssl req -new -key client.key -out client.csr \
+ -subj "/CN=grpc-client"
+
+ # Generate client certificate signed by the CA
+ openssl x509 -req -in client.csr -CA ca.crt -CAkey ca.key -CAcreateserial \
+ -out client.crt -days 365 -sha256
+
+Verify certificates
+-------------------
+
+.. code-block:: bash
+
+ # Verify server certificate
+ openssl verify -CAfile ca.crt server.crt
+
+ # Verify client certificate
+ openssl verify -CAfile ca.crt client.crt
diff --git a/src/ansys/tools/common/cyberchannel.py b/src/ansys/tools/common/cyberchannel.py
new file mode 100644
index 00000000..94ee8647
--- /dev/null
+++ b/src/ansys/tools/common/cyberchannel.py
@@ -0,0 +1,479 @@
+"""Module to create gRPC channels with different transport modes.
+
+This module provides functions to create gRPC channels based on the specified
+transport mode, including insecure, Unix Domain Sockets (UDS), Windows Named User
+Authentication (WNUA), and Mutual TLS (mTLS).
+
+Example
+-------
+ channel = create_channel(
+ host="localhost",
+ port=50051,
+ transport_mode="mtls",
+ certs_dir="path/to/certs",
+ grpc_options=[('grpc.max_receive_message_length', 50 * 1024 * 1024)],
+ )
+ stub = hello_pb2_grpc.GreeterStub(channel)
+
+"""
+
+# Only the create_channel function is exposed for external use
+__all__ = ["create_channel", "verify_transport_mode", "verify_uds_socket"]
+
+import logging
+import os
+from dataclasses import dataclass
+from pathlib import Path
+from typing import cast
+from warnings import warn
+from typing import TypeGuard
+
+import grpc
+
+_IS_WINDOWS = os.name == "nt"
+LOOPBACK_HOSTS = ("localhost", "127.0.0.1")
+
+logger = logging.getLogger(__name__)
+
+@dataclass
+class CertificateFiles:
+ cert_file: str | Path | None = None
+ key_file: str | Path | None = None
+ ca_file: str | Path | None = None
+
+def create_channel(
+ transport_mode: str,
+ host: str | None = None,
+ port: int | str | None = None,
+ uds_service: str | None = None,
+ uds_dir: str | Path | None = None,
+ uds_id: str | None = None,
+ certs_dir: str | Path | None = None,
+ cert_files: CertificateFiles | None = None,
+ grpc_options: list[tuple[str, object]] | None = None,
+) -> grpc.Channel:
+ """Create a gRPC channel based on the transport mode.
+
+ Parameters
+ ----------
+ transport_mode : str
+ Transport mode selected by the user.
+ Options are: "insecure", "uds", "wnua", "mtls"
+ host : str | None
+ Hostname or IP address of the server.
+ By default `None` - however, if not using UDS transport mode,
+ it will be requested.
+ port : int | str | None
+ Port in which the server is running.
+ By default `None` - however, if not using UDS transport mode,
+ it will be requested.
+ uds_service : str | None
+ Optional service name for the UDS socket.
+ By default `None` - however, if UDS is selected, it will
+ be requested.
+ uds_dir : str | Path | None
+ Directory to use for Unix Domain Sockets (UDS) transport mode.
+ By default `None` and thus it will use the "~/.conn" folder.
+ uds_id : str | None
+ Optional ID to use for the UDS socket filename.
+ By default `None` and thus it will use ".sock".
+ Otherwise, the socket filename will be "-.sock".
+ certs_dir : str | Path | None
+ Directory to use for TLS certificates.
+ By default `None` and thus search for the "ANSYS_GRPC_CERTIFICATES" environment variable.
+ If not found, it will use the "certs" folder assuming it is in the current working
+ directory.
+ cert_files: CertificateFiles | None = None
+ Path to the client certificate file, client key file, and issuing certificate authority.
+ By default `None`.
+ If all three file paths are not all provided, use the certs_dir parameter.
+ grpc_options: list[tuple[str, object]] | None
+ gRPC channel options to pass when creating the channel.
+ Each option is a tuple of the form ("option_name", value).
+ By default `None` and thus no extra options are added.
+
+ Returns
+ -------
+ grpc.Channel
+ The created gRPC channel
+
+ """
+ def check_host_port(transport_mode, host, port) -> tuple[str, str, str]:
+ if host is None:
+ raise ValueError(f"When using {transport_mode.lower()} transport mode, 'host' must be provided.")
+ if port is None:
+ raise ValueError(f"When using {transport_mode.lower()} transport mode, 'port' must be provided.")
+ return transport_mode, host, port
+
+ match transport_mode.lower():
+ case "insecure":
+ transport_mode, host, port = check_host_port(transport_mode, host, port)
+ return create_insecure_channel(host, port, grpc_options)
+ case "uds":
+ return create_uds_channel(uds_service, uds_dir, uds_id, grpc_options)
+ case "wnua":
+ transport_mode, host, port = check_host_port(transport_mode, host, port)
+ return create_wnua_channel(host, port, grpc_options)
+ case "mtls":
+ transport_mode, host, port = check_host_port(transport_mode, host, port)
+ return create_mtls_channel(host, port, certs_dir, cert_files, grpc_options)
+ case _:
+ raise ValueError(
+ f"Unknown transport mode: {transport_mode}. "
+ "Valid options are: 'insecure', 'uds', 'wnua', 'mtls'."
+ )
+
+
+##################################### TRANSPORT MODE CHANNELS #####################################
+
+
+def create_insecure_channel(
+ host: str, port: int | str, grpc_options: list[tuple[str, object]] | None = None
+) -> grpc.Channel:
+ """Create an insecure gRPC channel without TLS.
+
+ Parameters
+ ----------
+ host : str
+ Hostname or IP address of the server.
+ port : int | str
+ Port in which the server is running.
+ grpc_options: list[tuple[str, object]] | None
+ gRPC channel options to pass when creating the channel.
+ Each option is a tuple of the form ("option_name", value).
+ By default `None` and thus no extra options are added.
+
+ Returns
+ -------
+ grpc.Channel
+ The created gRPC channel
+
+ """
+ target = f"{host}:{port}"
+ warn(
+ f"Starting gRPC client without TLS on {target}. This is INSECURE. "
+ "Consider using a secure connection."
+ )
+ logger.info(f"Connecting using INSECURE -> {target}")
+ return grpc.insecure_channel(target, options=grpc_options)
+
+
+def create_uds_channel(
+ uds_service: str | None,
+ uds_dir: str | Path | None = None,
+ uds_id: str | None = None,
+ grpc_options: list[tuple[str, object]] | None = None,
+) -> grpc.Channel:
+ """Create a gRPC channel using Unix Domain Sockets (UDS).
+
+ Parameters
+ ----------
+ uds_service : str
+ Service name for the UDS socket.
+ uds_dir : str | Path | None
+ Directory to use for Unix Domain Sockets (UDS) transport mode.
+ By default `None` and thus it will use the "~/.conn" folder.
+ uds_id : str | None
+ Optional ID to use for the UDS socket filename.
+ By default `None` and thus it will use ".sock".
+ Otherwise, the socket filename will be "-.sock".
+ grpc_options: list[tuple[str, object]] | None
+ gRPC channel options to pass when creating the channel.
+ Each option is a tuple of the form ("option_name", value).
+ By default `None` and thus only the default authority option is added.
+
+ Returns
+ -------
+ grpc.Channel
+ The created gRPC channel
+
+ """
+ if not is_uds_supported():
+ raise RuntimeError(
+ "Unix Domain Sockets are not supported on this platform or gRPC version."
+ )
+
+ if not uds_service:
+ raise ValueError("When using UDS transport mode, 'uds_service' must be provided.")
+
+ # Determine UDS folder
+ uds_folder = determine_uds_folder(uds_dir)
+
+ # Make sure the folder exists
+ uds_folder.mkdir(parents=True, exist_ok=True)
+
+ # Generate socket filename with optional ID
+ socket_filename = f"{uds_service}-{uds_id}.sock" if uds_id else f"{uds_service}.sock"
+ target = f"unix:{uds_folder / socket_filename}"
+ # Set default authority to "localhost" for UDS connection
+ # This is needed to avoid issues with some gRPC implementations,
+ # see https://github.com/grpc/grpc/issues/34305
+ options: list[tuple[str, object]] = [
+ ("grpc.default_authority", "localhost"),
+ ]
+ if grpc_options:
+ options.extend(grpc_options)
+ logger.info(f"Connecting using UDS -> {target}")
+ return grpc.insecure_channel(target, options=options)
+
+
+def create_wnua_channel(
+ host: str,
+ port: int | str,
+ grpc_options: list[tuple[str, object]] | None = None,
+) -> grpc.Channel:
+ """Create a gRPC channel using Windows Named User Authentication (WNUA).
+
+ Parameters
+ ----------
+ host : str
+ Hostname or IP address of the server.
+ port : int | str
+ Port in which the server is running.
+ grpc_options: list[tuple[str, object]] | None
+ gRPC channel options to pass when creating the channel.
+ Each option is a tuple of the form ("option_name", value).
+ By default `None` and thus only the default authority option is added.
+
+ Returns
+ -------
+ grpc.Channel
+ The created gRPC channel
+
+ """
+ if not _IS_WINDOWS:
+ raise ValueError("Windows Named User Authentication (WNUA) is only supported on Windows.")
+ if host not in LOOPBACK_HOSTS:
+ raise ValueError("Remote host connections are not supported with WNUA.")
+
+ target = f"{host}:{port}"
+ # Set default authority to "localhost" for WNUA connection
+ # This is needed to avoid issues with some gRPC implementations,
+ # see https://github.com/grpc/grpc/issues/34305
+ options: list[tuple[str, object]] = [
+ ("grpc.default_authority", "localhost"),
+ ]
+ if grpc_options:
+ options.extend(grpc_options)
+ logger.info(f"Connecting using WNUA -> {target}")
+ return grpc.insecure_channel(target, options=options)
+
+
+def create_mtls_channel(
+ host: str,
+ port: int | str,
+ certs_dir: str | Path | None = None,
+ cert_files: CertificateFiles | None = None,
+ grpc_options: list[tuple[str, object]] | None = None,
+) -> grpc.Channel:
+ """Create a gRPC channel using Mutual TLS (mTLS).
+
+ Parameters
+ ----------
+ host : str
+ Hostname or IP address of the server.
+ port : int | str
+ Port in which the server is running.
+ certs_dir : str | Path | None
+ Directory to use for TLS certificates.
+ By default `None` and thus search for the "ANSYS_GRPC_CERTIFICATES" environment variable.
+ If not found, it will use the "certs" folder assuming it is in the current working
+ directory.
+ cert_files: CertificateFiles | None
+ Path to the client certificate file, client key file, and issuing certificate authority.
+ By default `None`.
+ If all three file paths are not all provided, use the certs_dir parameter.
+ grpc_options: list[tuple[str, object]] | None
+ gRPC channel options to pass when creating the channel.
+ Each option is a tuple of the form ("option_name", value).
+ By default `None` and thus no extra options are added.
+
+ Returns
+ -------
+ grpc.Channel
+ The created gRPC channel
+
+ """
+ certs_folder = None
+ if cert_files is not None and cert_files.cert_file is not None and cert_files.key_file is not None and cert_files.ca_file is not None:
+ cert_file = Path(cert_files.cert_file).resolve()
+ key_file = Path(cert_files.key_file).resolve()
+ ca_file = Path(cert_files.ca_file).resolve()
+ else:
+ # Determine certificates folder
+ if certs_dir:
+ certs_folder = Path(certs_dir)
+ elif os.environ.get("ANSYS_GRPC_CERTIFICATES"):
+ certs_folder = Path(cast(str, os.environ.get("ANSYS_GRPC_CERTIFICATES")))
+ else:
+ certs_folder = Path("certs")
+ ca_file = certs_folder / "ca.crt"
+ cert_file = certs_folder / "client.crt"
+ key_file = certs_folder / "client.key"
+
+ # Load certificates
+ try:
+ with (ca_file).open("rb") as f:
+ trusted_certs = f.read()
+ with (cert_file).open("rb") as f:
+ client_cert = f.read()
+ with (key_file).open("rb") as f:
+ client_key = f.read()
+ except FileNotFoundError as e:
+ error_message = f"Certificate file not found: {e.filename}. "
+ if certs_folder is not None:
+ error_message += f"Ensure that the certificates are present in the '{certs_folder}' folder or " \
+ "set the 'ANSYS_GRPC_CERTIFICATES' environment variable."
+ raise FileNotFoundError(error_message) from e
+
+ # Create SSL credentials
+ credentials = grpc.ssl_channel_credentials(
+ root_certificates=trusted_certs, private_key=client_key, certificate_chain=client_cert
+ )
+
+ target = f"{host}:{port}"
+ logger.info(f"Connecting using mTLS -> {target}")
+ return grpc.secure_channel(target, credentials, options=grpc_options)
+
+
+######################################## HELPER FUNCTIONS ########################################
+
+
+def version_tuple(version_str: str) -> tuple[int, ...]:
+ """Convert a version string into a tuple of integers for comparison.
+
+ Parameters
+ ----------
+ version_str : str
+ The version string to convert.
+
+ Returns
+ -------
+ tuple[int, ...]
+ A tuple of integers representing the version.
+
+ """
+ return tuple(int(x) for x in version_str.split("."))
+
+
+def check_grpc_version():
+ """Check if the installed gRPC version meets the minimum requirement.
+
+ Returns
+ -------
+ bool
+ True if the gRPC version is sufficient, False otherwise.
+
+ """
+ min_version = "1.63.0"
+ current_version = grpc.__version__
+
+ try:
+ return version_tuple(current_version) >= version_tuple(min_version)
+ except ValueError:
+ logger.warning("Unable to parse gRPC version.")
+ return False
+
+
+def is_uds_supported():
+ """Check if Unix Domain Sockets (UDS) are supported on the current platform.
+
+ Returns
+ -------
+ bool
+ True if UDS is supported, False otherwise.
+
+ """
+ is_grpc_version_ok = check_grpc_version()
+ return is_grpc_version_ok if _IS_WINDOWS else True
+
+
+def determine_uds_folder(uds_dir: str | Path | None = None) -> Path:
+ """Determine the directory to use for Unix Domain Sockets (UDS).
+
+ Parameters
+ ----------
+ uds_dir : str | Path | None
+ Directory to use for Unix Domain Sockets (UDS) transport mode.
+ By default `None` and thus it will use the "~/.conn" folder.
+
+ Returns
+ -------
+ Path
+ The path to the UDS directory.
+
+ """
+ # If no directory is provided, use default based on OS
+ if uds_dir:
+ return uds_dir if isinstance(uds_dir, Path) else Path(uds_dir)
+ else:
+ if _IS_WINDOWS:
+ return Path(os.environ["USERPROFILE"]) / ".conn"
+ else:
+ # Linux/POSIX
+ return Path(os.environ["HOME"], ".conn")
+
+
+def verify_transport_mode(transport_mode: str, mode: str | None = None) -> None:
+ """Verify that the provided transport mode is valid.
+
+ Parameters
+ ----------
+ transport_mode : str
+ The transport mode to verify.
+ mode : str | None
+ Can be one of "all", "local" or "remote" to restrict the valid transport modes.
+ By default `None` and thus all transport modes are accepted.
+
+ Raises
+ ------
+ ValueError
+ If the transport mode is not one of the accepted values.
+
+ """
+ if mode == "local":
+ valid_modes = {"insecure", "uds", "wnua"}
+ elif mode == "remote":
+ valid_modes = {"insecure", "mtls"}
+ elif mode == "all" or mode is None:
+ valid_modes = {"insecure", "uds", "wnua", "mtls"}
+ else:
+ raise ValueError(f"Invalid mode: {mode}. Valid options are: 'all', 'local', 'remote'.")
+
+ if transport_mode.lower() not in valid_modes:
+ raise ValueError(
+ f"Invalid transport mode: {transport_mode}. "
+ f"Valid options are: {', '.join(valid_modes)}."
+ )
+
+
+def verify_uds_socket(
+ uds_service: str, uds_dir: Path | None = None, uds_id: str | None = None
+) -> bool:
+ """Verify that the UDS socket file has been created.
+
+ Parameters
+ ----------
+ uds_service : str
+ Service name for the UDS socket.
+ uds_dir : Path | None
+ Directory where the UDS socket file is expected to be (optional).
+ By default `None` and thus it will use the "~/.conn" folder.
+ uds_id : str | None
+ Unique identifier for the UDS socket (optional).
+ By default `None` and thus it will use ".sock".
+ Otherwise, the socket filename will be "-.sock".
+
+ Returns
+ -------
+ bool
+ True if the UDS socket file exists, False otherwise.
+ """
+ # Generate socket filename with optional ID
+ uds_filename = f"{uds_service}-{uds_id}.sock" if uds_id else f"{uds_service}.sock"
+
+ # Full path to the UDS socket file
+ uds_socket_path = determine_uds_folder(uds_dir) / uds_filename
+
+ # Check if the UDS socket file exists
+ return uds_socket_path.exists()
From 64bf7c9a0f0a80c99f7928cafa7d5230136265cf Mon Sep 17 00:00:00 2001
From: Roberto Pastor Muela <37798125+RobPasMue@users.noreply.github.com>
Date: Mon, 24 Nov 2025 15:58:52 +0100
Subject: [PATCH 02/12] fix: pre-commit
---
doc/source/user_guide/secure_grpc.rst | 10 +++--
src/ansys/tools/common/cyberchannel.py | 62 +++++++++++++++++---------
2 files changed, 47 insertions(+), 25 deletions(-)
diff --git a/doc/source/user_guide/secure_grpc.rst b/doc/source/user_guide/secure_grpc.rst
index e723abbe..2623cd15 100644
--- a/doc/source/user_guide/secure_grpc.rst
+++ b/doc/source/user_guide/secure_grpc.rst
@@ -46,10 +46,12 @@ Example usage
import hello_pb2_grpc
channel = create_channel(
- host="localhost", port=50051, # Channel details
- transport_mode="mtls", certs_dir="path/to/certs", # Security details
- grpc_options=[ # Extra details
- ('grpc.max_receive_message_length', 50 * 1024 * 1024)
+ host="localhost",
+ port=50051, # Channel details
+ transport_mode="mtls",
+ certs_dir="path/to/certs", # Security details
+ grpc_options=[ # Extra details
+ ("grpc.max_receive_message_length", 50 * 1024 * 1024)
],
)
stub = hello_pb2_grpc.GreeterStub(channel)
diff --git a/src/ansys/tools/common/cyberchannel.py b/src/ansys/tools/common/cyberchannel.py
index 94ee8647..0e02a939 100644
--- a/src/ansys/tools/common/cyberchannel.py
+++ b/src/ansys/tools/common/cyberchannel.py
@@ -1,3 +1,25 @@
+# Copyright (C) 2025 ANSYS, Inc. and/or its affiliates.
+# SPDX-License-Identifier: MIT
+#
+#
+# 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.
+
"""Module to create gRPC channels with different transport modes.
This module provides functions to create gRPC channels based on the specified
@@ -20,13 +42,12 @@
# Only the create_channel function is exposed for external use
__all__ = ["create_channel", "verify_transport_mode", "verify_uds_socket"]
+from dataclasses import dataclass
import logging
import os
-from dataclasses import dataclass
from pathlib import Path
from typing import cast
from warnings import warn
-from typing import TypeGuard
import grpc
@@ -35,12 +56,14 @@
logger = logging.getLogger(__name__)
+
@dataclass
class CertificateFiles:
cert_file: str | Path | None = None
key_file: str | Path | None = None
ca_file: str | Path | None = None
+
def create_channel(
transport_mode: str,
host: str | None = None,
@@ -98,6 +121,7 @@ def create_channel(
The created gRPC channel
"""
+
def check_host_port(transport_mode, host, port) -> tuple[str, str, str]:
if host is None:
raise ValueError(f"When using {transport_mode.lower()} transport mode, 'host' must be provided.")
@@ -119,8 +143,7 @@ def check_host_port(transport_mode, host, port) -> tuple[str, str, str]:
return create_mtls_channel(host, port, certs_dir, cert_files, grpc_options)
case _:
raise ValueError(
- f"Unknown transport mode: {transport_mode}. "
- "Valid options are: 'insecure', 'uds', 'wnua', 'mtls'."
+ f"Unknown transport mode: {transport_mode}. Valid options are: 'insecure', 'uds', 'wnua', 'mtls'."
)
@@ -150,10 +173,7 @@ def create_insecure_channel(
"""
target = f"{host}:{port}"
- warn(
- f"Starting gRPC client without TLS on {target}. This is INSECURE. "
- "Consider using a secure connection."
- )
+ warn(f"Starting gRPC client without TLS on {target}. This is INSECURE. Consider using a secure connection.")
logger.info(f"Connecting using INSECURE -> {target}")
return grpc.insecure_channel(target, options=grpc_options)
@@ -189,9 +209,7 @@ def create_uds_channel(
"""
if not is_uds_supported():
- raise RuntimeError(
- "Unix Domain Sockets are not supported on this platform or gRPC version."
- )
+ raise RuntimeError("Unix Domain Sockets are not supported on this platform or gRPC version.")
if not uds_service:
raise ValueError("When using UDS transport mode, 'uds_service' must be provided.")
@@ -295,7 +313,12 @@ def create_mtls_channel(
"""
certs_folder = None
- if cert_files is not None and cert_files.cert_file is not None and cert_files.key_file is not None and cert_files.ca_file is not None:
+ if (
+ cert_files is not None
+ and cert_files.cert_file is not None
+ and cert_files.key_file is not None
+ and cert_files.ca_file is not None
+ ):
cert_file = Path(cert_files.cert_file).resolve()
key_file = Path(cert_files.key_file).resolve()
ca_file = Path(cert_files.ca_file).resolve()
@@ -322,8 +345,10 @@ def create_mtls_channel(
except FileNotFoundError as e:
error_message = f"Certificate file not found: {e.filename}. "
if certs_folder is not None:
- error_message += f"Ensure that the certificates are present in the '{certs_folder}' folder or " \
- "set the 'ANSYS_GRPC_CERTIFICATES' environment variable."
+ error_message += (
+ f"Ensure that the certificates are present in the '{certs_folder}' folder or "
+ "set the 'ANSYS_GRPC_CERTIFICATES' environment variable."
+ )
raise FileNotFoundError(error_message) from e
# Create SSL credentials
@@ -441,15 +466,10 @@ def verify_transport_mode(transport_mode: str, mode: str | None = None) -> None:
raise ValueError(f"Invalid mode: {mode}. Valid options are: 'all', 'local', 'remote'.")
if transport_mode.lower() not in valid_modes:
- raise ValueError(
- f"Invalid transport mode: {transport_mode}. "
- f"Valid options are: {', '.join(valid_modes)}."
- )
+ raise ValueError(f"Invalid transport mode: {transport_mode}. Valid options are: {', '.join(valid_modes)}.")
-def verify_uds_socket(
- uds_service: str, uds_dir: Path | None = None, uds_id: str | None = None
-) -> bool:
+def verify_uds_socket(uds_service: str, uds_dir: Path | None = None, uds_id: str | None = None) -> bool:
"""Verify that the UDS socket file has been created.
Parameters
From c42d8ce18fb8e1d2b85bf9bffcb13629aea0d6c7 Mon Sep 17 00:00:00 2001
From: Roberto Pastor Muela <37798125+RobPasMue@users.noreply.github.com>
Date: Mon, 24 Nov 2025 16:04:59 +0100
Subject: [PATCH 03/12] fix: changes
---
doc/source/user_guide/secure_grpc.rst | 26 ++++++++++++++------------
1 file changed, 14 insertions(+), 12 deletions(-)
diff --git a/doc/source/user_guide/secure_grpc.rst b/doc/source/user_guide/secure_grpc.rst
index 2623cd15..be326431 100644
--- a/doc/source/user_guide/secure_grpc.rst
+++ b/doc/source/user_guide/secure_grpc.rst
@@ -1,18 +1,20 @@
Securing gRPC connections
#########################
-The PyAnsys ecosystem enables now various transport modes for securing gRPC connections.
-This page reviews the available transport modes, and how to use them.
+With the release of Ansys product service packs adding enhanced security to
+gRPC communication, the PyAnsys ecosystem enables various transport modes for
+securing gRPC connections. This page reviews the available transport modes,
+and how to use them.
Supported transport modes
=========================
PyAnsys supports the following transport modes for gRPC connections:
-- **Mutual TLS (mTLS).** This mode allows secure connections using TLS
- encryption and client/server certificates. It is recommended for production
- use, especially when transmitting sensitive data. Works both locally and over
- the network.
+- **Mutual TLS (mTLS).** This mode, which works both locally and over the
+ network, allows secure connections using TLS encryption and client/server
+ certificates. It is recommended for production use, especially when
+ transmitting sensitive data.
- **Unix Domain Sockets (UDS).** This mode allows connections over a local
socket file. UDS is only supported for local inter-process communication
@@ -33,9 +35,9 @@ The ``cyberchannel`` module
The ``cyberchannel`` module eases the transition to secure gRPC. It is meant to
be used by client applications to create gRPC channels with the server.
-This module implements all transport modes described above. It also abstracts
-away the details of connection setup and certificate handling, making it easier
-to connect clients to gRPC servers in different environments.
+This module implements all transport modes described previously. It also
+abstracts away the details of connection setup and certificate handling, making
+it easier to connect clients to gRPC servers in different environments.
Example usage
-------------
@@ -117,7 +119,7 @@ Generate a certificate authority
openssl genrsa -out ca.key 4096
# Generate self-signed CA certificate
- openssl req -x509 -new -nodes -key ca.key -sha256 -days 3650 -out ca.crt \
+ openssl req -x509 -new -nodes -key ca.key -sha256 -days 200 -out ca.crt \
-subj "/CN=MyRootCA"
Generate the server certificate
@@ -134,7 +136,7 @@ Generate the server certificate
# Generate server certificate signed by the CA
openssl x509 -req -in server.csr -CA ca.crt -CAkey ca.key -CAcreateserial \
- -out server.crt -days 365 -sha256
+ -out server.crt -days 200 -sha256
Generate the client certificate
-------------------------------
@@ -150,7 +152,7 @@ Generate the client certificate
# Generate client certificate signed by the CA
openssl x509 -req -in client.csr -CA ca.crt -CAkey ca.key -CAcreateserial \
- -out client.crt -days 365 -sha256
+ -out client.crt -days 200 -sha256
Verify certificates
-------------------
From b1ff5168c065f0db9685d53068ff8c68fe3855cf Mon Sep 17 00:00:00 2001
From: pyansys-ci-bot <92810346+pyansys-ci-bot@users.noreply.github.com>
Date: Mon, 24 Nov 2025 16:56:31 +0000
Subject: [PATCH 04/12] chore: adding changelog file 107.miscellaneous.md
[dependabot-skip]
---
doc/source/changelog/107.miscellaneous.md | 1 +
1 file changed, 1 insertion(+)
create mode 100644 doc/source/changelog/107.miscellaneous.md
diff --git a/doc/source/changelog/107.miscellaneous.md b/doc/source/changelog/107.miscellaneous.md
new file mode 100644
index 00000000..40d31e6d
--- /dev/null
+++ b/doc/source/changelog/107.miscellaneous.md
@@ -0,0 +1 @@
+Feat: adding cyberchannel module
From 174ccf274b3e1ec356a57e32fe5386f7fd8e1a00 Mon Sep 17 00:00:00 2001
From: Roberto Pastor Muela <37798125+RobPasMue@users.noreply.github.com>
Date: Mon, 24 Nov 2025 17:58:52 +0100
Subject: [PATCH 05/12] fix: safe wrap grpc import
---
src/ansys/tools/common/cyberchannel.py | 6 +++++-
1 file changed, 5 insertions(+), 1 deletion(-)
diff --git a/src/ansys/tools/common/cyberchannel.py b/src/ansys/tools/common/cyberchannel.py
index 0e02a939..a5ce1b35 100644
--- a/src/ansys/tools/common/cyberchannel.py
+++ b/src/ansys/tools/common/cyberchannel.py
@@ -49,7 +49,11 @@
from typing import cast
from warnings import warn
-import grpc
+try:
+ import grpc
+except ImportError: # pragma: no cover
+ warn("grpc module is not available - reach out to the library maintainers to include it into their dependencies")
+
_IS_WINDOWS = os.name == "nt"
LOOPBACK_HOSTS = ("localhost", "127.0.0.1")
From e27ad122387ea87978dc65d982769f6f8f2675ed Mon Sep 17 00:00:00 2001
From: Jorge Martinez
Date: Mon, 24 Nov 2025 18:05:26 +0100
Subject: [PATCH 06/12] fix(doc): talbe formatting
---
doc/source/user_guide/secure_grpc.rst | 33 ++++++++++++++++-----------
1 file changed, 20 insertions(+), 13 deletions(-)
diff --git a/doc/source/user_guide/secure_grpc.rst b/doc/source/user_guide/secure_grpc.rst
index be326431..e9772751 100644
--- a/doc/source/user_guide/secure_grpc.rst
+++ b/doc/source/user_guide/secure_grpc.rst
@@ -94,19 +94,26 @@ Generating certificates for mTLS
================================
`OpenSSL `__ can be used to generate the necessary
-certificates for mTLS. The following files are required for both the server and
-the client:
-
-===== ======================= =========================================
-Side Required Files Purpose
-===== ======================= =========================================
-Server server.crt Server identity
- server.key Server private key
- ca.crt To verify client certificates
-Client client.crt Client identity
- client.key Client private key
- ca.crt To verify server certificates
-===== ======================= =========================================
+certificates for mTLS.
+
+Certificate files used by the server:
+
+======================= =========================================
+Required Files Purpose
+======================= =========================================
+server.crt Server identity
+server.key Server private key
+ca.crt To verify client certificates
+
+Certificate files used by the client:
+
+======================= =========================================
+Required Files Purpose
+======================= =========================================
+client.crt Client identity
+client.key Client private key
+ca.crt To verify server certificates
+======================= =========================================
These files can be generated using `OpenSSL `__.
From c66149ba6f79332a86624d7984e38b5fda87340c Mon Sep 17 00:00:00 2001
From: Jorge Martinez
Date: Mon, 24 Nov 2025 18:12:53 +0100
Subject: [PATCH 07/12] fix(doc): talbe formatting
---
doc/source/user_guide/secure_grpc.rst | 42 +++++++++++++++------------
1 file changed, 24 insertions(+), 18 deletions(-)
diff --git a/doc/source/user_guide/secure_grpc.rst b/doc/source/user_guide/secure_grpc.rst
index e9772751..0c567545 100644
--- a/doc/source/user_guide/secure_grpc.rst
+++ b/doc/source/user_guide/secure_grpc.rst
@@ -96,24 +96,30 @@ Generating certificates for mTLS
`OpenSSL `__ can be used to generate the necessary
certificates for mTLS.
-Certificate files used by the server:
-
-======================= =========================================
-Required Files Purpose
-======================= =========================================
-server.crt Server identity
-server.key Server private key
-ca.crt To verify client certificates
-
-Certificate files used by the client:
-
-======================= =========================================
-Required Files Purpose
-======================= =========================================
-client.crt Client identity
-client.key Client private key
-ca.crt To verify server certificates
-======================= =========================================
+.. list-table:: Server certificate files
+ :widths: auto
+ :header-rows: 1
+
+ * - Required Files
+ - Purpose
+ * - server.crt
+ - Server identity
+ * - server.key
+ - Server private key
+ * - ca.crt
+ - To verify client certificates
+
+.. list-table:: Client certificate files
+ :widths: auto
+ :header-rows: 1
+ * - Required Files
+ - Purpose
+ * - client.crt
+ - Client identity
+ * - client.key
+ - Client private key
+ * - ca.crt
+ - To verify server certificates
These files can be generated using `OpenSSL `__.
From 688bfa4cc649e88cdd46ad7f98ba6413f84801ff Mon Sep 17 00:00:00 2001
From: Jorge Martinez
Date: Mon, 24 Nov 2025 18:14:19 +0100
Subject: [PATCH 08/12] fix(doc): talbe formatting
---
doc/source/user_guide/secure_grpc.rst | 1 +
1 file changed, 1 insertion(+)
diff --git a/doc/source/user_guide/secure_grpc.rst b/doc/source/user_guide/secure_grpc.rst
index 0c567545..45f1a89a 100644
--- a/doc/source/user_guide/secure_grpc.rst
+++ b/doc/source/user_guide/secure_grpc.rst
@@ -112,6 +112,7 @@ certificates for mTLS.
.. list-table:: Client certificate files
:widths: auto
:header-rows: 1
+
* - Required Files
- Purpose
* - client.crt
From 7a96769d851366fee49ca86c620aa4fe1319c1a3 Mon Sep 17 00:00:00 2001
From: Roberto Pastor Muela <37798125+RobPasMue@users.noreply.github.com>
Date: Mon, 24 Nov 2025 23:55:28 +0100
Subject: [PATCH 09/12] docs: fix table rendering
---
doc/source/user_guide/secure_grpc.rst | 42 +++++++++++++--------------
1 file changed, 21 insertions(+), 21 deletions(-)
diff --git a/doc/source/user_guide/secure_grpc.rst b/doc/source/user_guide/secure_grpc.rst
index 45f1a89a..da507533 100644
--- a/doc/source/user_guide/secure_grpc.rst
+++ b/doc/source/user_guide/secure_grpc.rst
@@ -97,30 +97,30 @@ Generating certificates for mTLS
certificates for mTLS.
.. list-table:: Server certificate files
- :widths: auto
- :header-rows: 1
+ :header-rows: 1
+ :widths: auto
- * - Required Files
- - Purpose
- * - server.crt
- - Server identity
- * - server.key
- - Server private key
- * - ca.crt
- - To verify client certificates
+ * - Required Files
+ - Purpose
+ * - server.crt
+ - Server identity
+ * - server.key
+ - Server private key
+ * - ca.crt
+ - To verify client certificates
.. list-table:: Client certificate files
- :widths: auto
- :header-rows: 1
-
- * - Required Files
- - Purpose
- * - client.crt
- - Client identity
- * - client.key
- - Client private key
- * - ca.crt
- - To verify server certificates
+ :header-rows: 1
+ :widths: auto
+
+ * - Required Files
+ - Purpose
+ * - client.crt
+ - Client identity
+ * - client.key
+ - Client private key
+ * - ca.crt
+ - To verify server certificates
These files can be generated using `OpenSSL `__.
From fdc83fece65831f37bc8563425243be84885889e Mon Sep 17 00:00:00 2001
From: Roberto Pastor Muela <37798125+RobPasMue@users.noreply.github.com>
Date: Mon, 24 Nov 2025 23:56:13 +0100
Subject: [PATCH 10/12] docs: fix table rendering
---
doc/source/user_guide/secure_grpc.rst | 22 +++++++++++-----------
1 file changed, 11 insertions(+), 11 deletions(-)
diff --git a/doc/source/user_guide/secure_grpc.rst b/doc/source/user_guide/secure_grpc.rst
index da507533..2bc3a792 100644
--- a/doc/source/user_guide/secure_grpc.rst
+++ b/doc/source/user_guide/secure_grpc.rst
@@ -78,17 +78,17 @@ Environment variables
---------------------
.. list-table::
- :header-rows: 1
- :widths: 20 60 20
-
- * - Variable
- - Description
- - Default
- * - ``ANSYS_GRPC_CERTIFICATES``
- - Path to folder containing ``ca.crt``, ``client.crt``, and ``client.key``
- for mTLS connections. If not set, defaults to a local ``./certs``
- directory.
- - ``./certs``
+ :header-rows: 1
+ :widths: 20 60 20
+
+ * - Variable
+ - Description
+ - Default
+ * - ``ANSYS_GRPC_CERTIFICATES``
+ - Path to folder containing ``ca.crt``, ``client.crt``, and ``client.key``
+ for mTLS connections. If not set, defaults to a local ``./certs``
+ directory.
+ - ``./certs``
Generating certificates for mTLS
================================
From eed192b0a127b78692907f6a81e47bccbdc9c1bf Mon Sep 17 00:00:00 2001
From: Jorge Martinez
Date: Tue, 25 Nov 2025 08:51:01 +0100
Subject: [PATCH 11/12] fix(doc): code directive
---
src/ansys/tools/common/cyberchannel.py | 3 +++
1 file changed, 3 insertions(+)
diff --git a/src/ansys/tools/common/cyberchannel.py b/src/ansys/tools/common/cyberchannel.py
index a5ce1b35..71ad2f7f 100644
--- a/src/ansys/tools/common/cyberchannel.py
+++ b/src/ansys/tools/common/cyberchannel.py
@@ -28,6 +28,9 @@
Example
-------
+
+.. code-block:: python
+
channel = create_channel(
host="localhost",
port=50051,
From 6d586f1b318f29c9a13408b3f7e606396e3ef19e Mon Sep 17 00:00:00 2001
From: "pre-commit-ci[bot]"
<66853113+pre-commit-ci[bot]@users.noreply.github.com>
Date: Tue, 25 Nov 2025 07:52:29 +0000
Subject: [PATCH 12/12] [pre-commit.ci] auto fixes from pre-commit.com hooks
for more information, see https://pre-commit.ci
---
src/ansys/tools/common/cyberchannel.py | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/src/ansys/tools/common/cyberchannel.py b/src/ansys/tools/common/cyberchannel.py
index 71ad2f7f..06d6a27c 100644
--- a/src/ansys/tools/common/cyberchannel.py
+++ b/src/ansys/tools/common/cyberchannel.py
@@ -36,7 +36,7 @@
port=50051,
transport_mode="mtls",
certs_dir="path/to/certs",
- grpc_options=[('grpc.max_receive_message_length', 50 * 1024 * 1024)],
+ grpc_options=[("grpc.max_receive_message_length", 50 * 1024 * 1024)],
)
stub = hello_pb2_grpc.GreeterStub(channel)