From c9f9b70b3f1c8fda73de6c35f57c293813889695 Mon Sep 17 00:00:00 2001 From: kasemir Date: Mon, 7 Aug 2023 11:21:36 -0400 Subject: [PATCH] PVA client: Handle both "tcp" and "tls" in search reply --- .../main/java/org/epics/pva/PVASettings.java | 2 + .../org/epics/pva/client/ChannelSearch.java | 20 ++++-- .../epics/pva/client/ClientTCPHandler.java | 8 +-- .../java/org/epics/pva/client/PVAClient.java | 8 +-- .../org/epics/pva/common/SecureSockets.java | 71 ++++++++++++++----- .../epics/pva/server/ServerTCPListener.java | 29 ++------ 6 files changed, 86 insertions(+), 52 deletions(-) diff --git a/core/pva/src/main/java/org/epics/pva/PVASettings.java b/core/pva/src/main/java/org/epics/pva/PVASettings.java index 2ff9087117..5f0b648b66 100644 --- a/core/pva/src/main/java/org/epics/pva/PVASettings.java +++ b/core/pva/src/main/java/org/epics/pva/PVASettings.java @@ -138,6 +138,8 @@ public class PVASettings /** Path to trust store, a PKCS12 file that contains the certificates or root CA * that the client will trust. * When empty, PVA client does not support secure (TLS) communication. + * When configured, PVA client can reply to PVA servers that offer "tls" in a search reply, + * and searches via EPICS_PVA_NAME_SERVERS will also use TLS. */ public static String EPICS_PVA_TLS_KEYCHAIN = ""; diff --git a/core/pva/src/main/java/org/epics/pva/client/ChannelSearch.java b/core/pva/src/main/java/org/epics/pva/client/ChannelSearch.java index 61d926b84d..d9b6cf58ad 100644 --- a/core/pva/src/main/java/org/epics/pva/client/ChannelSearch.java +++ b/core/pva/src/main/java/org/epics/pva/client/ChannelSearch.java @@ -21,7 +21,7 @@ import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicInteger; -import java.util.function.Function; +import java.util.function.BiFunction; import java.util.logging.Level; import org.epics.pva.PVASettings; @@ -156,7 +156,8 @@ private class SearchedChannel private final ClientUDPHandler udp; - private final Function tcp_provider; + /** Create ClientTCPHandler from IP address and 'tls' flag */ + private final BiFunction tcp_provider; /** Buffer for assembling search messages */ private final ByteBuffer send_buffer = ByteBuffer.allocate(PVASettings.MAX_UDP_UNFRAGMENTED_SEND); @@ -169,9 +170,16 @@ private class SearchedChannel b_or_mcast_search_addresses = new ArrayList<>(), name_server_addresses = new ArrayList<>(); + /** Create channel searcher + * @param udp UDP handler + * @param udp_addresses UDP addresses to search + * @param tcp_provider Function that creates ClientTCPHandler for IP address and 'tls' flag + * @param name_server_addresses TCP addresses to search + * @throws Exception on error + */ public ChannelSearch(final ClientUDPHandler udp, final List udp_addresses, - final Function tcp_provider, + final BiFunction tcp_provider, final List name_server_addresses) throws Exception { this.udp = udp; @@ -413,7 +421,11 @@ private void search(final Collection channels) // Search via TCP for (AddressInfo name_server : name_server_addresses) { - final ClientTCPHandler tcp = tcp_provider.apply(name_server.getAddress()); + // TODO How to decide if TCP search should use TLS? + // Configure via EPICS_PVA_NAME_SERVERS? + // For now configuring EPICS_PVA_TLS_KEYCHAIN enables TLS for all name server lookups + final boolean tls = !PVASettings.EPICS_PVA_TLS_KEYCHAIN.isBlank(); + final ClientTCPHandler tcp = tcp_provider.apply(name_server.getAddress(), tls); // In case of connection errors (TCP connection blocked by firewall), // tcp will be null diff --git a/core/pva/src/main/java/org/epics/pva/client/ClientTCPHandler.java b/core/pva/src/main/java/org/epics/pva/client/ClientTCPHandler.java index b845bb3211..2001df9732 100644 --- a/core/pva/src/main/java/org/epics/pva/client/ClientTCPHandler.java +++ b/core/pva/src/main/java/org/epics/pva/client/ClientTCPHandler.java @@ -100,9 +100,9 @@ class ClientTCPHandler extends TCPHandler */ private final AtomicBoolean connection_validated = new AtomicBoolean(false); - public ClientTCPHandler(final PVAClient client, final InetSocketAddress address, final Guid guid) throws Exception + public ClientTCPHandler(final PVAClient client, final InetSocketAddress address, final Guid guid, final boolean tls) throws Exception { - super(createSocket(address), true); + super(createSocket(address, tls), true); logger.log(Level.FINE, () -> "TCPHandler " + guid + " for " + address + " created ============================"); this.client = client; this.guid = guid; @@ -117,9 +117,9 @@ public ClientTCPHandler(final PVAClient client, final InetSocketAddress address, // it's started when server confirms the connection. } - private static Socket createSocket(InetSocketAddress address) throws Exception + private static Socket createSocket(InetSocketAddress address, final boolean tls) throws Exception { - final Socket socket = SecureSockets.getClientFactory().createSocket(address.getAddress(), address.getPort()); + final Socket socket = SecureSockets.createClientSocket(address, tls); socket.setTcpNoDelay(true); socket.setKeepAlive(true); return socket; diff --git a/core/pva/src/main/java/org/epics/pva/client/PVAClient.java b/core/pva/src/main/java/org/epics/pva/client/PVAClient.java index c1b2a0d2a1..4f22ad140a 100644 --- a/core/pva/src/main/java/org/epics/pva/client/PVAClient.java +++ b/core/pva/src/main/java/org/epics/pva/client/PVAClient.java @@ -15,7 +15,7 @@ import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicInteger; -import java.util.function.Function; +import java.util.function.BiFunction; import java.util.logging.Level; import org.epics.pva.PVASettings; @@ -89,13 +89,13 @@ public PVAClient() throws Exception // TCP traffic is handled by one ClientTCPHandler per address (IP, socket). // Pass helper to channel search for getting such a handler. - final Function tcp_provider = the_addr -> + final BiFunction tcp_provider = (the_addr, use_tls) -> tcp_handlers.computeIfAbsent(the_addr, addr -> { try { // If absent, create with initial empty GUID - return new ClientTCPHandler(this, addr, Guid.EMPTY); + return new ClientTCPHandler(this, addr, Guid.EMPTY, use_tls); } catch (Exception ex) { @@ -254,7 +254,7 @@ void handleSearchResponse(final int channel_id, final InetSocketAddress server, { try { - return new ClientTCPHandler(this, addr, guid); + return new ClientTCPHandler(this, addr, guid, tls); } catch (Exception ex) { diff --git a/core/pva/src/main/java/org/epics/pva/common/SecureSockets.java b/core/pva/src/main/java/org/epics/pva/common/SecureSockets.java index 870638a5de..67e775a3f4 100644 --- a/core/pva/src/main/java/org/epics/pva/common/SecureSockets.java +++ b/core/pva/src/main/java/org/epics/pva/common/SecureSockets.java @@ -10,6 +10,9 @@ import static org.epics.pva.PVASettings.logger; import java.io.FileInputStream; +import java.net.InetSocketAddress; +import java.net.ServerSocket; +import java.net.Socket; import java.security.KeyStore; import java.util.logging.Level; @@ -17,6 +20,7 @@ import javax.net.SocketFactory; import javax.net.ssl.KeyManagerFactory; import javax.net.ssl.SSLContext; +import javax.net.ssl.SSLSocket; import javax.net.ssl.TrustManagerFactory; import org.epics.pva.PVASettings; @@ -35,13 +39,15 @@ @SuppressWarnings("nls") public class SecureSockets { - private static ServerSocketFactory server_sockets; - private static SocketFactory client_sockets; + private static boolean initialized = false; + private static ServerSocketFactory tls_server_sockets; + private static SocketFactory tls_client_sockets; private static synchronized void initialize() throws Exception { - // TODO For now always creating TLS sockets based on preference settings. - // Need to create them based on search response requesting TLS + if (initialized) + return; + final char[] password = PVASettings.EPICS_PVA_STOREPASS.isBlank() ? null : PVASettings.EPICS_PVA_STOREPASS.toCharArray(); if (! PVASettings.EPICS_PVAS_TLS_KEYCHAIN.isBlank()) @@ -56,10 +62,8 @@ private static synchronized void initialize() throws Exception final SSLContext context = SSLContext.getInstance("TLS"); context.init(key_manager.getKeyManagers(), null, null); - server_sockets = context.getServerSocketFactory(); + tls_server_sockets = context.getServerSocketFactory(); } - else - server_sockets = ServerSocketFactory.getDefault(); if (! PVASettings.EPICS_PVA_TLS_KEYCHAIN.isBlank()) { @@ -73,27 +77,62 @@ private static synchronized void initialize() throws Exception SSLContext context = SSLContext.getInstance("TLS"); context.init(null, trust_manager.getTrustManagers(), null); - client_sockets = context.getSocketFactory(); + tls_client_sockets = context.getSocketFactory(); } - else - client_sockets = SocketFactory.getDefault(); + initialized = true; } - /** @return Factory for plain or secure server sockets + /** Create server socket + * @param address IP address and port to which the socket will be bound + * @param tls Use TLS socket? Otherwise plain TCP + * @return Plain or secure server socket * @throws Exception on error */ - public static ServerSocketFactory getServerFactory() throws Exception + public static ServerSocket createServerSocket(final InetSocketAddress address, final boolean tls) throws Exception { initialize(); - return server_sockets; + final ServerSocket socket; + if (tls) + { + if (tls_server_sockets == null) + throw new Exception("TLS is not supported. Configure EPICS_PVAS_TLS_KEYCHAIN"); + socket = tls_server_sockets.createServerSocket(); + } + else + socket = new ServerSocket(); + + try + { + socket.setReuseAddress(true); + socket.bind(address); + } + catch (Exception ex) + { + socket.close(); + throw ex; + } + return socket; } - /** @return Factory for plain or secure client sockets + /** Create client socket + * @param address IP address and port to which the socket will be bound + * @param tls Use TLS socket? Otherwise plain TCP + * @return Plain or secure client socket * @throws Exception on error */ - public static SocketFactory getClientFactory() throws Exception + public static Socket createClientSocket(final InetSocketAddress address, final boolean tls) throws Exception { initialize(); - return client_sockets; + if (! tls) + return new Socket(address.getAddress(), address.getPort()); + + if (tls_client_sockets == null) + throw new Exception("TLS is not supported. Configure EPICS_PVA_TLS_KEYCHAIN"); + final SSLSocket socket = (SSLSocket) tls_client_sockets.createSocket(address.getAddress(), address.getPort()); + // PVXS prefers 1.3 + socket.setEnabledProtocols(new String[] { "TLSv1.3"}); + // Handshake starts when first writing, but that might delay SSL errors, so force handshake before we use the socket + socket.startHandshake(); + return socket; } } diff --git a/core/pva/src/main/java/org/epics/pva/server/ServerTCPListener.java b/core/pva/src/main/java/org/epics/pva/server/ServerTCPListener.java index 326a92ce80..bf99872b47 100644 --- a/core/pva/src/main/java/org/epics/pva/server/ServerTCPListener.java +++ b/core/pva/src/main/java/org/epics/pva/server/ServerTCPListener.java @@ -14,7 +14,6 @@ import java.net.InetSocketAddress; import java.net.ServerSocket; import java.net.Socket; -import java.nio.channels.ServerSocketChannel; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.TimeUnit; @@ -126,13 +125,16 @@ private static boolean checkForIPv4Server(final int desired_port) */ private static ServerSocket createSocket() throws Exception { + // If a PVA Server keychain has been configured, use TLS + final boolean tls = !PVASettings.EPICS_PVAS_TLS_KEYCHAIN.isBlank(); + if (checkForIPv4Server(PVASettings.EPICS_PVA_SERVER_PORT)) logger.log(Level.FINE, "Found existing IPv4 server on port " + PVASettings.EPICS_PVA_SERVER_PORT); else { // Try to bind to desired port try { - return createBoundSocket(new InetSocketAddress(PVASettings.EPICS_PVA_SERVER_PORT)); + return SecureSockets.createServerSocket(new InetSocketAddress(PVASettings.EPICS_PVA_SERVER_PORT), tls); } catch (BindException ex) { @@ -144,7 +146,7 @@ private static ServerSocket createSocket() throws Exception final InetSocketAddress any = new InetSocketAddress(0); try { - return createBoundSocket(any); + return SecureSockets.createServerSocket(any, tls); } catch (Exception e) { @@ -152,27 +154,6 @@ private static ServerSocket createSocket() throws Exception } } - /** Try to create socket that's bound to an address - * @param addr Desired address - * @return {@link ServerSocketChannel} - * @throws Exception on error - */ - private static ServerSocket createBoundSocket(final InetSocketAddress addr) throws Exception - { - final ServerSocket socket = SecureSockets.getServerFactory().createServerSocket(); - try - { - socket.setReuseAddress(true); - socket.bind(addr); - } - catch (Exception ex) - { - socket.close(); - throw ex; - } - return socket; - } - private void listen() { try