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));