Skip to content

Commit

Permalink
PVA server replies 'tls' or 'tcp'...
Browse files Browse the repository at this point in the history
  • Loading branch information
kasemir committed Aug 9, 2023
1 parent c9f9b70 commit c1d0d7e
Show file tree
Hide file tree
Showing 12 changed files with 81 additions and 54 deletions.
20 changes: 9 additions & 11 deletions core/pva/TLS.md
Original file line number Diff line number Diff line change
@@ -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.
Expand All @@ -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:

```
Expand All @@ -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.

Expand Down Expand Up @@ -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
Expand All @@ -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
```


Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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.

```
Expand All @@ -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
```
Expand Down
6 changes: 5 additions & 1 deletion core/pva/src/main/java/org/epics/pva/PVASettings.java
Original file line number Diff line number Diff line change
Expand Up @@ -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.
*
* <p>First must be an IPv4 and/or IPv6 address that enables
Expand Down Expand Up @@ -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);
Expand Down
23 changes: 14 additions & 9 deletions core/pva/src/main/java/org/epics/pva/common/SearchRequest.java
Original file line number Diff line number Diff line change
Expand Up @@ -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, <code>null</code> for 'list' */
public List<Channel> channels;

Expand Down Expand Up @@ -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 = "<none>";
String unknown_protocol = "<none>";
for (int i=0; i<count; ++i)
{
protocol = PVAString.decodeString(buffer);
if ("tcp".equals(protocol))
{
final String protocol = PVAString.decodeString(buffer);
if ("tls".equals(protocol))
search.tls = true;
else if ("tcp".equals(protocol))
tcp = true;
break;
}
else
unknown_protocol = protocol;
}

// Loop over searched channels
Expand All @@ -158,9 +161,9 @@ public static SearchRequest decode(final InetSocketAddress from, final byte vers
}
else
{ // Channel search request
if (! tcp)
if (! (tcp || search.tls))
{
logger.log(Level.WARNING, "PVA Client " + from + " sent search #" + search.seq + " for protocol '" + protocol + "', need 'tcp'");
logger.log(Level.WARNING, "PVA Client " + from + " sent search #" + search.seq + " for protocol '" + unknown_protocol + "', need 'tcp' or 'tls'");
return null;
}
search.channels = new ArrayList<>(count);
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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));
Expand All @@ -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);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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())
{
Expand Down
8 changes: 4 additions & 4 deletions core/pva/src/main/java/org/epics/pva/common/TCPHandler.java
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down Expand Up @@ -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();
Expand Down
14 changes: 8 additions & 6 deletions core/pva/src/main/java/org/epics/pva/server/PVAServer.java
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -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 */
Expand Down Expand Up @@ -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 <code>null</code>
* @return
*/
boolean handleSearchRequest(final int seq, final int cid, final String name,
final InetSocketAddress client,
final boolean tls,
final ServerTCPHandler tcp_connection)
{
final Consumer<InetSocketAddress> 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?
Expand All @@ -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
Expand Down
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -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);
}
}
13 changes: 10 additions & 3 deletions core/pva/src/main/java/org/epics/pva/server/ServerTCPHandler.java
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
Expand Down
27 changes: 17 additions & 10 deletions core/pva/src/main/java/org/epics/pva/server/ServerTCPListener.java
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down Expand Up @@ -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");
}
}

Expand All @@ -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);
}
}
Expand Down
Loading

0 comments on commit c1d0d7e

Please sign in to comment.