From c1d0d7ec69f121ed0ced8e7af994e526f5004d59 Mon Sep 17 00:00:00 2001 From: kasemir Date: Wed, 9 Aug 2023 10:35:00 -0400 Subject: [PATCH] PVA server replies 'tls' or 'tcp'... --- core/pva/TLS.md | 20 +++++++------- .../main/java/org/epics/pva/PVASettings.java | 6 ++++- .../org/epics/pva/common/SearchRequest.java | 23 +++++++++------- .../org/epics/pva/common/SearchResponse.java | 4 ++- .../org/epics/pva/common/SecureSockets.java | 3 ++- .../java/org/epics/pva/common/TCPHandler.java | 8 +++--- .../java/org/epics/pva/server/PVAServer.java | 14 +++++----- .../pva/server/SearchCommandHandler.java | 4 +-- .../java/org/epics/pva/server/ServerDemo.java | 0 .../epics/pva/server/ServerTCPHandler.java | 13 ++++++--- .../epics/pva/server/ServerTCPListener.java | 27 ++++++++++++------- .../epics/pva/server/ServerUDPHandler.java | 13 ++++----- 12 files changed, 81 insertions(+), 54 deletions(-) rename core/pva/src/{test => main}/java/org/epics/pva/server/ServerDemo.java (100%) diff --git a/core/pva/TLS.md b/core/pva/TLS.md index ee4f6a3c0c..c207345b5c 100644 --- a/core/pva/TLS.md +++ b/core/pva/TLS.md @@ -1,8 +1,6 @@ Secure Socket Support ===================== -The server and client provide a simple preview of TLS-enabled "secure socket" communication. - By default, the server and client will use plain TCP sockets to communicate. By configuring a keystore for the server and a truststore for the client, the communication can be switched to secure (TLS) sockets. @@ -18,8 +16,6 @@ to the server which only the server can decode with its private key. keytool -genkey -alias mykey -dname "CN=myself" -keystore KEYSTORE -storepass changeit -keyalg RSA ``` - - To check, note "Entry type: PrivateKeyEntry" because the certificate holds both a public and private key: ``` @@ -33,9 +29,10 @@ An operational setup might prefer to sign them by a publicly trusted certificate Step 2: Create a client TRUSTSTORE to register the public server key ------- -Clients check this list of public keys to identify trusted servers. +Clients check a list of public keys to identify trusted servers. Clients can technically use the keystore we just created, but -they should really only have access to the server's public key. +they should really only have access to the server's public key, +not the server's private key. In addition, you may want to add public keys from more than one server into the client truststore. @@ -85,7 +82,7 @@ java -cp target/classes org/epics/pva/server/ServerDemo Step 4: Configure and run the demo client ------- -Set environment variables to inform the server about its keystore: +Set environment variables to inform the client about its truststore: ``` export EPICS_PVA_TLS_KEYCHAIN=/path/to/TRUSTSTORE @@ -95,7 +92,7 @@ export EPICS_PVA_STOREPASS=changeit Then run a demo client ``` -java -cp target/classes org/epics/pva/client/PVAClientMain monitor demo +java -cp target/classes org/epics/pva/client/PVAClientMain get demo ``` @@ -134,10 +131,10 @@ persist a reboot: # Default UDP search port sudo firewall-cmd --zone=public --add-port=5076/udp sudo firewall-cmd --zone=public --add-port=5076/udp --permanent -# Default TCP (plain) port +# Default plain TCP port sudo firewall-cmd --zone=public --add-port=5075/tcp sudo firewall-cmd --zone=public --add-port=5075/tcp --permanent -# Default TCP (TLS) port +# Default secure (TLS) TCP port sudo firewall-cmd --zone=public --add-port=5076/tcp sudo firewall-cmd --zone=public --add-port=5076/tcp --permanent @@ -179,7 +176,7 @@ keytool -printcert -file myioc.cer ``` Import the signed certificate into the ioc keystore. Since `ioc.cer` is signed by 'myca', which -is not a generally known CA, we will get an error like "Failed to establish chain" +is not a generally known CA, we will get an error "Failed to establish chain" unless we first import `myca.cer` to trust out local CA. ``` @@ -190,6 +187,7 @@ keytool -list -v -keystore ioc.p12 -storepass changeit A client will trust any IOC certificate signed by 'myca' once it's aware of the 'myca' certificate, which needs to be imported into the PKCS12 file format: + ``` keytool -importcert -alias myca -keystore trust_ca.p12 -storepass changeit -file myca.cer -noprompt ``` 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 5f0b648b66..aa7173cb30 100644 --- a/core/pva/src/main/java/org/epics/pva/PVASettings.java +++ b/core/pva/src/main/java/org/epics/pva/PVASettings.java @@ -89,9 +89,12 @@ public class PVASettings /** PVA client port for sending name searches and receiving beacons */ public static int EPICS_PVA_BROADCAST_PORT = 5076; - /** First PVA port used by server */ + /** First PVA port used by plain TCP server */ public static int EPICS_PVA_SERVER_PORT = 5075; + /** First PVA port used by TLS server */ + public static int EPICS_PVAS_TLS_PORT = 5076; + /** Local addresses to which server will listen. * *

First must be an IPv4 and/or IPv6 address that enables @@ -226,6 +229,7 @@ public class PVASettings EPICS_PVA_AUTO_ADDR_LIST = get("EPICS_PVA_AUTO_ADDR_LIST", EPICS_PVA_AUTO_ADDR_LIST); EPICS_PVA_NAME_SERVERS = get("EPICS_PVA_NAME_SERVERS", EPICS_PVA_NAME_SERVERS); EPICS_PVA_SERVER_PORT = get("EPICS_PVA_SERVER_PORT", EPICS_PVA_SERVER_PORT); + EPICS_PVAS_TLS_PORT = get("EPICS_PVAS_TLS_PORT", EPICS_PVAS_TLS_PORT); EPICS_PVAS_INTF_ADDR_LIST = get("EPICS_PVAS_INTF_ADDR_LIST", EPICS_PVAS_INTF_ADDR_LIST).trim(); EPICS_PVA_BROADCAST_PORT = get("EPICS_PVA_BROADCAST_PORT", EPICS_PVA_BROADCAST_PORT); EPICS_PVAS_BROADCAST_PORT = get("EPICS_PVAS_BROADCAST_PORT", EPICS_PVAS_BROADCAST_PORT); diff --git a/core/pva/src/main/java/org/epics/pva/common/SearchRequest.java b/core/pva/src/main/java/org/epics/pva/common/SearchRequest.java index 1a5cdf5c32..6f45a85453 100644 --- a/core/pva/src/main/java/org/epics/pva/common/SearchRequest.java +++ b/core/pva/src/main/java/org/epics/pva/common/SearchRequest.java @@ -72,6 +72,8 @@ public String toString() public boolean reply_required; /** Address of client */ public InetSocketAddress client; + /** Use TLS, or plain TCP? */ + public boolean tls; /** Names requested in search, null for 'list' */ public List channels; @@ -135,17 +137,18 @@ public static SearchRequest decode(final InetSocketAddress from, final byte vers search.client = new InetSocketAddress(addr, port); // Assert that client supports "tcp", ignore rest - boolean tcp = false; + boolean tcp = search.tls = false; int count = Byte.toUnsignedInt(buffer.get()); - String protocol = ""; + String unknown_protocol = ""; for (int i=0; i(count); @@ -191,6 +194,7 @@ public static void encode(final boolean unicast, final int seq, final Collection // SEARCH message sequence // PVXS sends "find".getBytes() instead + // For TCP search via EPICS_PVA_NAME_SERVERS, we send "look" ("kool" for little endian) buffer.putInt(seq); // If a host has multiple listeners on the UDP search port, @@ -222,6 +226,7 @@ public static void encode(final boolean unicast, final int seq, final Collection else { // Only support tcp, or both tls and tcp? + // TODO pass 'use_tls' in because encode might also be called by ServerUDPHandler.forwardSearchRequest if (PVASettings.EPICS_PVA_TLS_KEYCHAIN.isBlank()) buffer.put((byte)1); else diff --git a/core/pva/src/main/java/org/epics/pva/common/SearchResponse.java b/core/pva/src/main/java/org/epics/pva/common/SearchResponse.java index 9cfc74031f..1367d34626 100644 --- a/core/pva/src/main/java/org/epics/pva/common/SearchResponse.java +++ b/core/pva/src/main/java/org/epics/pva/common/SearchResponse.java @@ -110,10 +110,12 @@ public static SearchResponse decode(final int payload, final ByteBuffer buffer) * @param cid Client's channel ID or -1 * @param address Address where client can connect to access the channel * @param port Associated TCP port + * @param tls Use TLS? Otherwise plain TCP * @param buffer Buffer into which search response will be encoded */ public static void encode(final Guid guid, final int seq, final int cid, final InetAddress address, final int port, + final boolean tls, final ByteBuffer buffer) { PVAHeader.encodeMessageHeader(buffer, PVAHeader.FLAG_SERVER, PVAHeader.CMD_SEARCH_RESPONSE, 12+4+16+2+4+1+2+ (cid < 0 ? 0 : 4)); @@ -129,7 +131,7 @@ public static void encode(final Guid guid, final int seq, final int cid, buffer.putShort((short)port); // Protocol - PVAString.encodeString("tcp", buffer); + PVAString.encodeString(tls ? "tls" : "tcp", buffer); // Found PVABool.encodeBoolean(cid >= 0, buffer); 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 67e775a3f4..ed62f2e30c 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 @@ -48,7 +48,8 @@ private static synchronized void initialize() throws Exception if (initialized) return; - final char[] password = PVASettings.EPICS_PVA_STOREPASS.isBlank() ? null : PVASettings.EPICS_PVA_STOREPASS.toCharArray(); + // We support the default "" empty as well as actual passwords, but not null for no password + final char[] password = PVASettings.EPICS_PVA_STOREPASS.toCharArray(); if (! PVASettings.EPICS_PVAS_TLS_KEYCHAIN.isBlank()) { diff --git a/core/pva/src/main/java/org/epics/pva/common/TCPHandler.java b/core/pva/src/main/java/org/epics/pva/common/TCPHandler.java index bae7f5912e..266afb8842 100644 --- a/core/pva/src/main/java/org/epics/pva/common/TCPHandler.java +++ b/core/pva/src/main/java/org/epics/pva/common/TCPHandler.java @@ -175,8 +175,8 @@ private Void sender() { try { - Thread.currentThread().setName("TCP sender from " + socket.getLocalAddress() + " to " + socket.getInetAddress()); - logger.log(Level.FINER, Thread.currentThread().getName() + " started"); + Thread.currentThread().setName("TCP sender from " + socket.getLocalSocketAddress() + " to " + socket.getRemoteSocketAddress()); + logger.log(Level.FINER, () -> Thread.currentThread().getName() + " started"); while (true) { send_buffer.clear(); @@ -249,8 +249,8 @@ private Void receiver() { try { - Thread.currentThread().setName("TCP receiver " + socket.getInetAddress()); - logger.log(Level.FINER, Thread.currentThread().getName() + " started"); + Thread.currentThread().setName("TCP receiver " + socket.getLocalSocketAddress()); + logger.log(Level.FINER, () -> Thread.currentThread().getName() + " started for " + socket.getRemoteSocketAddress()); logger.log(Level.FINER, "Native byte order " + receive_buffer.order()); receive_buffer.clear(); final InputStream in = socket.getInputStream(); diff --git a/core/pva/src/main/java/org/epics/pva/server/PVAServer.java b/core/pva/src/main/java/org/epics/pva/server/PVAServer.java index 883c7b1864..072f037fbc 100644 --- a/core/pva/src/main/java/org/epics/pva/server/PVAServer.java +++ b/core/pva/src/main/java/org/epics/pva/server/PVAServer.java @@ -1,5 +1,5 @@ /******************************************************************************* - * Copyright (c) 2019-2022 Oak Ridge National Laboratory. + * Copyright (c) 2019-2023 Oak Ridge National Laboratory. * All rights reserved. This program and the accompanying materials * are made available under the terms of the Eclipse Public License v1.0 * which accompanies this distribution, and is available at @@ -54,7 +54,7 @@ public class PVAServer implements AutoCloseable /** TCP connection listener, creates {@link ServerTCPHandler} for each connecting client */ private final ServerTCPListener tcp; - /** Optional handler for seatches that's checked first */ + /** Optional searche handler 'hook' */ private final SearchHandler custom_search_handler; /** Handlers for the TCP connections clients established to this server */ @@ -167,21 +167,23 @@ ServerPV getPV(final int sid) * @param cid Client's channel ID * @param name PV Name * @param client Client's UDP reply address + * @param tls Does client support tls? * @param tcp_connection Optional TCP connection for search received via TCP, else null * @return */ boolean handleSearchRequest(final int seq, final int cid, final String name, final InetSocketAddress client, + final boolean tls, final ServerTCPHandler tcp_connection) { final Consumer send_search_reply = server_address -> { // If received via TCP, reply via same connection. if (tcp_connection != null) - tcp_connection.submitSearchReply(guid, seq, cid, server_address); + tcp_connection.submitSearchReply(guid, seq, cid, server_address, tls); else // Otherwise reply via UDP to the given address. - POOL.execute(() -> udp.sendSearchReply(guid, seq, cid, server_address, client)); + POOL.execute(() -> udp.sendSearchReply(guid, seq, cid, server_address, tls, client)); }; // Does custom handler consume the search request? @@ -192,9 +194,9 @@ boolean handleSearchRequest(final int seq, final int cid, final String name, if (cid < 0) { // 'List servers' search, no specific name if (tcp_connection != null) - tcp_connection.submitSearchReply(guid, seq, -1, USE_THIS_TCP_CONNECTION); + tcp_connection.submitSearchReply(guid, seq, -1, USE_THIS_TCP_CONNECTION, tls); else - POOL.execute(() -> udp.sendSearchReply(guid, 0, -1, getTCPAddress(), client)); + POOL.execute(() -> udp.sendSearchReply(guid, 0, -1, getTCPAddress(), tls, client)); return true; } else diff --git a/core/pva/src/main/java/org/epics/pva/server/SearchCommandHandler.java b/core/pva/src/main/java/org/epics/pva/server/SearchCommandHandler.java index 9ed1694e61..070c095c8d 100644 --- a/core/pva/src/main/java/org/epics/pva/server/SearchCommandHandler.java +++ b/core/pva/src/main/java/org/epics/pva/server/SearchCommandHandler.java @@ -1,5 +1,5 @@ /******************************************************************************* - * Copyright (c) 2021-2022 Oak Ridge National Laboratory. + * Copyright (c) 2021-2023 Oak Ridge National Laboratory. * All rights reserved. This program and the accompanying materials * are made available under the terms of the Eclipse Public License v1.0 * which accompanies this distribution, and is available at @@ -35,6 +35,6 @@ public void handleCommand(final ServerTCPHandler tcp, final ByteBuffer buffer) t if (search.channels != null) for (SearchRequest.Channel channel : search.channels) tcp.getServer().handleSearchRequest(search.seq, channel.getCID(), channel.getName(), - search.client, tcp); + search.client, search.tls, tcp); } } diff --git a/core/pva/src/test/java/org/epics/pva/server/ServerDemo.java b/core/pva/src/main/java/org/epics/pva/server/ServerDemo.java similarity index 100% rename from core/pva/src/test/java/org/epics/pva/server/ServerDemo.java rename to core/pva/src/main/java/org/epics/pva/server/ServerDemo.java diff --git a/core/pva/src/main/java/org/epics/pva/server/ServerTCPHandler.java b/core/pva/src/main/java/org/epics/pva/server/ServerTCPHandler.java index b3b869b879..bd7315289b 100644 --- a/core/pva/src/main/java/org/epics/pva/server/ServerTCPHandler.java +++ b/core/pva/src/main/java/org/epics/pva/server/ServerTCPHandler.java @@ -141,12 +141,19 @@ protected void handleApplicationMessage(final byte command, final ByteBuffer buf super.handleApplicationMessage(command, buffer); } - void submitSearchReply(final Guid guid, final int seq, final int cid, final InetSocketAddress server_address) + /** Send a "channel found" reply to a client's search + * @param guid This server's GUID + * @param seq Client search request sequence number + * @param cid Client's channel ID or -1 + * @param server_address TCP address where client can connect to server + * @param tls Should client use tls? + */ + void submitSearchReply(final Guid guid, final int seq, final int cid, final InetSocketAddress server_address, final boolean tls) { final RequestEncoder encoder = (version, buffer) -> { - logger.log(Level.FINER, "Sending TCP search reply"); - SearchResponse.encode(guid, seq, cid, server_address.getAddress(), server_address.getPort(), buffer); + logger.log(Level.FINER, () -> "Sending " + (tls ? "TLS" : "TCP") + " search reply"); + SearchResponse.encode(guid, seq, cid, server_address.getAddress(), server_address.getPort(), tls, buffer); }; submit(encoder); } 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 bf99872b47..79a61b2cf2 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 @@ -54,7 +54,14 @@ public ServerTCPListener(final PVAServer server) throws Exception { this.server = server; - server_socket = createSocket(); + // Is TLS configured? + final boolean tls = !PVASettings.EPICS_PVAS_TLS_KEYCHAIN.isBlank(); + + // TODO Support both plain and tls, not either/or + if (! tls) + server_socket = createSocket(PVASettings.EPICS_PVA_SERVER_PORT, false); + else + server_socket = createSocket(PVASettings.EPICS_PVAS_TLS_PORT, true); local_address = (InetSocketAddress) server_socket.getLocalSocketAddress(); logger.log(Level.CONFIG, "Listening on TCP " + local_address); @@ -120,25 +127,24 @@ private static boolean checkForIPv4Server(final int desired_port) /** Create server's TCP socket * - * @return Socket bound to EPICS_PVA_SERVER_PORT or unused port + * @param port Preferred TCP port + * @param tls Use TLS? + * @return Socket bound to preferred port or unused port * @throws Exception on error */ - private static ServerSocket createSocket() throws Exception + private static ServerSocket createSocket(final int port, final boolean tls) 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); + if (checkForIPv4Server(port)) + logger.log(Level.FINE, "Found existing IPv4 server on port " + port); else { // Try to bind to desired port try { - return SecureSockets.createServerSocket(new InetSocketAddress(PVASettings.EPICS_PVA_SERVER_PORT), tls); + return SecureSockets.createServerSocket(new InetSocketAddress(port), tls); } catch (BindException ex) { - logger.log(Level.INFO, "TCP port " + PVASettings.EPICS_PVA_SERVER_PORT + " already in use, switching to automatically assigned port"); + logger.log(Level.INFO, (tls ? "TLS" : "TCP") + " port " + port + " already in use, switching to automatically assigned port"); } } @@ -162,6 +168,7 @@ private void listen() while (running) { final Socket client = server_socket.accept(); + logger.log(Level.FINE, () -> Thread.currentThread().getName() + " accepted client " + client.getRemoteSocketAddress()); new ServerTCPHandler(server, client); } } diff --git a/core/pva/src/main/java/org/epics/pva/server/ServerUDPHandler.java b/core/pva/src/main/java/org/epics/pva/server/ServerUDPHandler.java index 1c39cec919..9f2724eeca 100644 --- a/core/pva/src/main/java/org/epics/pva/server/ServerUDPHandler.java +++ b/core/pva/src/main/java/org/epics/pva/server/ServerUDPHandler.java @@ -1,5 +1,5 @@ /******************************************************************************* - * Copyright (c) 2019-2022 Oak Ridge National Laboratory. + * Copyright (c) 2019-2023 Oak Ridge National Laboratory. * All rights reserved. This program and the accompanying materials * are made available under the terms of the Eclipse Public License v1.0 * which accompanies this distribution, and is available at @@ -183,7 +183,7 @@ private boolean handleSearch(final InetSocketAddress from, final byte version, { if (search.reply_required) { // pvlist request - final boolean handled = server.handleSearchRequest(0, -1, null, search.client, null); + final boolean handled = server.handleSearchRequest(0, -1, null, search.client, search.tls, null); if (! handled && search.unicast) PVAServer.POOL.submit(() -> forwardSearchRequest(0, null, search.client)); } @@ -193,7 +193,7 @@ private boolean handleSearch(final InetSocketAddress from, final byte version, List forward = null; for (SearchRequest.Channel channel : search.channels) { - final boolean handled = server.handleSearchRequest(search.seq, channel.getCID(), channel.getName(), search.client, null); + final boolean handled = server.handleSearchRequest(search.seq, channel.getCID(), channel.getName(), search.client, search.tls, null); if (! handled && search.unicast) { if (forward == null) @@ -250,15 +250,16 @@ private void forwardSearchRequest(final int seq, final Collection "Sending UDP search reply to " + client + "\n" + Hexdump.toHexdump(send_buffer));