Skip to content

Commit a9c3a3a

Browse files
author
kasemir
committed
PVA: Handle CMD_ORIGIN_TAG
When forwarding UDP unicast search requests, prefix them with origin tag. When server receives search request, check for optional preceding origin tag to identify forwarded message. On forward, 'reply_to_src_port' flag is copied, but server ignores it in forwarded messages.
1 parent 1a6d155 commit a9c3a3a

8 files changed

Lines changed: 187 additions & 47 deletions

File tree

core/pva/src/main/java/org/epics/pva/client/ClientUDPHandler.java

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
import org.epics.pva.PVASettings;
2323
import org.epics.pva.common.AddressInfo;
2424
import org.epics.pva.common.Network;
25+
import org.epics.pva.common.OriginTag;
2526
import org.epics.pva.common.PVAHeader;
2627
import org.epics.pva.common.SearchRequest;
2728
import org.epics.pva.common.SearchResponse;
@@ -197,6 +198,9 @@ protected boolean handleMessage(final InetSocketAddress from, final byte version
197198
{
198199
case PVAHeader.CMD_BEACON:
199200
return handleBeacon(from, version, payload, buffer);
201+
case PVAHeader.CMD_ORIGIN_TAG:
202+
// Will be decoded with CMD_SEARCH
203+
break;
200204
case PVAHeader.CMD_SEARCH:
201205
return handleSearchRequest(from, version, payload, buffer);
202206
case PVAHeader.CMD_SEARCH_RESPONSE:
@@ -290,7 +294,8 @@ private boolean handleBeacon(final InetSocketAddress from, final byte version,
290294
private boolean handleSearchRequest(final InetSocketAddress from, final byte version,
291295
final int payload, final ByteBuffer buffer)
292296
{
293-
final SearchRequest search = SearchRequest.decode(from, version, payload, buffer);
297+
final OriginTag origin = OriginTag.testForOriginOfSearch(from, buffer);
298+
final SearchRequest search = SearchRequest.decode(origin, from, version, payload, buffer);
294299
try
295300
{
296301
if (local_multicast != null && search != null && search.unicast)
@@ -300,7 +305,8 @@ private boolean handleSearchRequest(final InetSocketAddress from, final byte ver
300305
if (search.reply_required)
301306
{
302307
forward_buffer.clear();
303-
SearchRequest.encode(false, false, 0, null, search.client, search.tls, forward_buffer);
308+
OriginTag.encode(udp_search4, forward_buffer);
309+
SearchRequest.encode(false, search.reply_to_src_port, 0, null, search.client, search.tls, forward_buffer);
304310
forward_buffer.flip();
305311
logger.log(Level.FINER, () -> "Forward search to list servers to " + local_multicast + "\n" + Hexdump.toHexdump(forward_buffer));
306312
send(forward_buffer, local_multicast);
@@ -309,7 +315,8 @@ private boolean handleSearchRequest(final InetSocketAddress from, final byte ver
309315
else
310316
{
311317
forward_buffer.clear();
312-
SearchRequest.encode(false, false, search.seq, search.channels, search.client, search.tls, forward_buffer);
318+
OriginTag.encode(udp_search4, forward_buffer);
319+
SearchRequest.encode(false, search.reply_to_src_port, search.seq, search.channels, search.client, search.tls, forward_buffer);
313320
forward_buffer.flip();
314321
logger.log(Level.FINER, () -> "Forward search to " + local_multicast + "\n" + Hexdump.toHexdump(forward_buffer));
315322
send(forward_buffer, local_multicast);
Lines changed: 128 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,128 @@
1+
/*******************************************************************************
2+
* Copyright (c) 2025 Oak Ridge National Laboratory.
3+
* All rights reserved. This program and the accompanying materials
4+
* are made available under the terms of the Eclipse Public License v1.0
5+
* which accompanies this distribution, and is available at
6+
* http://www.eclipse.org/legal/epl-v10.html
7+
******************************************************************************/
8+
package org.epics.pva.common;
9+
10+
import static org.epics.pva.PVASettings.logger;
11+
12+
import java.net.InetAddress;
13+
import java.net.InetSocketAddress;
14+
import java.nio.ByteBuffer;
15+
import java.nio.channels.DatagramChannel;
16+
import java.util.logging.Level;
17+
18+
import org.epics.pva.data.PVAAddress;
19+
20+
/** Helper for CMD_ORIGIN_TAG
21+
*
22+
* <p>UDP search messages that are forwarded via
23+
* the local 224.0.0.128 multicast are prefixed
24+
* with CMD_ORIGIN_TAG.
25+
*
26+
* @author Kay Kasemir
27+
*/
28+
@SuppressWarnings("nls")
29+
public class OriginTag
30+
{
31+
/** Size of CMD_ORIGIN_TAG payload (address) */
32+
public static final byte PAYLOAD_SIZE = 16;
33+
34+
/** Size of CMD_ORIGIN_TAG message */
35+
public static final byte TOTAL_SIZE = PVAHeader.HEADER_SIZE + PAYLOAD_SIZE;
36+
37+
/** "address to which the receiving socket was bound ..may be 0.0.0.0" */
38+
public InetAddress address;
39+
40+
/** Check for optional CMD_ORIGIN_TAG before CMD_SEARCH
41+
* @param from Peer address
42+
* @param buffer UDP message buffer ready to decode CMD_SEARCH
43+
* @return {@link OriginTag} or <code>null</code>
44+
*/
45+
public static OriginTag testForOriginOfSearch(final InetSocketAddress from, final ByteBuffer buffer)
46+
{
47+
// Check for optional origin tag.
48+
// For a normal search packet, the buffer is positioned at PVAHeader.HEADER_SIZE (8).
49+
// For a forwarded packet, it's at CMD_ORIGIN_TAG message (8+16) + HEADER_SIZE (8), i.e., 32
50+
// For now assuming no or exactly one CMD_ORIGIN_TAG before CMD_SEARCH.
51+
// Not supporting multiple CMD_ORIGIN_TAGs nor anything else before CMD_SEARCH.
52+
53+
// pos is just after the CMD_SEARCH message header
54+
final int pos = buffer.position();
55+
if (// Is there room for CMD_ORIGIN_TAG before this message header?
56+
pos == OriginTag.TOTAL_SIZE + PVAHeader.HEADER_SIZE &&
57+
// Is command of previous message indeed CMD_ORIGIN_TAG?
58+
buffer.get(PVAHeader.HEADER_OFFSET_COMMAND) == PVAHeader.CMD_ORIGIN_TAG)
59+
{
60+
// Move back to origin message, decode it, restore buffer position
61+
buffer.position(PVAHeader.HEADER_SIZE);
62+
OriginTag origin = OriginTag.decode(from, OriginTag.PAYLOAD_SIZE, buffer);
63+
buffer.position(pos);
64+
return origin;
65+
}
66+
67+
return null;
68+
}
69+
70+
/** Decode origin tag
71+
*
72+
* @param from Peer address
73+
* @param payload Payload size
74+
* @param buffer Buffer positioned on payload
75+
* @return Decoded origin tag or <code>null</code> if not a valid
76+
*/
77+
public static OriginTag decode(final InetSocketAddress from,
78+
final int payload, final ByteBuffer buffer)
79+
{
80+
if (payload < PAYLOAD_SIZE)
81+
{
82+
logger.log(Level.WARNING, "PVA client " + from + " sent only " + payload + " bytes for origin tag");
83+
return null;
84+
}
85+
final OriginTag origin = new OriginTag();
86+
87+
// responseAddress, IPv6 address in case of IP based transport, UDP
88+
try
89+
{
90+
origin.address = PVAAddress.decode(buffer);
91+
}
92+
catch (Exception ex)
93+
{
94+
logger.log(Level.WARNING, "PVA Client " + from + " sent origin tag with invalid address");
95+
return null;
96+
}
97+
logger.log(Level.FINER, () -> "PVA client " + from + " sent " + origin);
98+
return origin;
99+
}
100+
101+
/** Encode origin tag
102+
* @param udp UDP socket from which to pick the forwarder's address
103+
* @param buffer Buffer into which to encode
104+
* @return Encoded forwarder's address
105+
*/
106+
public static InetAddress encode(final DatagramChannel udp, final ByteBuffer buffer)
107+
{
108+
PVAHeader.encodeMessageHeader(buffer, PVAHeader.FLAG_NONE, PVAHeader.CMD_ORIGIN_TAG, PAYLOAD_SIZE);
109+
InetAddress this_end;
110+
try
111+
{
112+
this_end = ((InetSocketAddress) udp.getLocalAddress()).getAddress();
113+
}
114+
catch (Exception ex)
115+
{
116+
logger.log(Level.WARNING, "Invalid address for CMD_ORIGIN_TAG", ex);
117+
this_end = new InetSocketAddress(0).getAddress();
118+
}
119+
PVAAddress.encode(this_end, buffer);
120+
return this_end;
121+
}
122+
123+
@Override
124+
public String toString()
125+
{
126+
return "Origin tag with address " + address;
127+
}
128+
}

core/pva/src/main/java/org/epics/pva/common/PVAHeader.java

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ public class PVAHeader
2727
public static final byte PVA_MAGIC = (byte)0xCA;
2828

2929
/** PVA protocol revision (implemented by this library)
30-
*
30+
*
3131
* <br>v2: Server's Echo reply includes the request payload
3232
* <br>v3: SearchRequest FLAG_REPLY_SRC_PORT
3333
*/
@@ -137,12 +137,18 @@ public class PVAHeader
137137
/** Offset from start of common PVA message header to byte version */
138138
public static final int HEADER_OFFSET_VERSION = 1;
139139

140+
/** Offset from start of common PVA message header to byte flags */
141+
public static final int HEADER_OFFSET_FLAGS = 2;
142+
143+
/** Offset from start of common PVA message header to byte command */
144+
public static final int HEADER_OFFSET_COMMAND = 3;
145+
140146
/** Offset from start of common PVA message header to int payload_size */
141147
public static final int HEADER_OFFSET_PAYLOAD_SIZE = 4;
142148

143149

144150
/** Encode common PVA message header
145-
* @param buffer Buffer into which to encode
151+
* @param buffer Buffer into which to encode, must be positioned at desired address (usually 'clear()'ed)
146152
* @param flags Combination of FLAG_
147153
* @param command Command
148154
* @param payload_size Size of payload that follows
@@ -154,7 +160,6 @@ public static void encodeMessageHeader(final ByteBuffer buffer, byte flags, fina
154160
flags |= FLAG_BIG_ENDIAN;
155161
else
156162
flags &= ~FLAG_BIG_ENDIAN;
157-
buffer.clear();
158163
buffer.put(PVA_MAGIC);
159164
buffer.put(PVA_PROTOCOL_REVISION);
160165
buffer.put(flags);

core/pva/src/main/java/org/epics/pva/common/SearchRequest.java

Lines changed: 19 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -70,7 +70,17 @@ public String toString()
7070

7171
/** Client should ignore the 'port' in the reply and
7272
* simply use the port of the 'source', that is the peer port
73-
* of the UDP message or TCP connection
73+
* of the UDP message or TCP connection.
74+
*
75+
* <p>In a forwarded message, the 'client' (reply-to)
76+
* address has been updated to reflect the original client.
77+
* The 'reply_to_src_port' flag is copied, but server
78+
* must NOT reply to the source port because that would
79+
* be the port of the forwarder, not the real client.
80+
* The presence of an {@link OriginTag} will indicate
81+
* that the 'reply_to_src_port' flag is copied and needs
82+
* to be ignored.
83+
*
7484
* @since Version 3
7585
*/
7686
public static final byte FLAG_REPLY_SRC_PORT = 0x02;
@@ -94,14 +104,14 @@ public String toString()
94104
public List<Channel> channels;
95105

96106
/** Check search request
97-
*
107+
* @param origin Optional CMD_ORIGIN_TAG that preceded the search message
98108
* @param from Peer address
99109
* @param version Message version
100110
* @param payload Payload size
101111
* @param buffer Buffer positioned on payload
102112
* @return Decoded search request or <code>null</code> if not a valid search request
103113
*/
104-
public static SearchRequest decode(final InetSocketAddress from, final byte version,
114+
public static SearchRequest decode(final OriginTag origin, final InetSocketAddress from, final byte version,
105115
final int payload, final ByteBuffer buffer)
106116
{
107117
// pvinfo sends 0x1D=29 bytes:
@@ -150,12 +160,12 @@ public static SearchRequest decode(final InetSocketAddress from, final byte vers
150160
// and instead use the peer's port.
151161
// This should help with NAT where we get the message from an intermediate
152162
// and need to reply via that same intermediate
153-
if (version >= 3 && search.reply_to_src_port)
163+
if (version >= 3 && search.reply_to_src_port && origin == null)
154164
port = from.getPort();
155165

156166
// Use address from message unless it's a generic local address
157167
if (addr.isAnyLocalAddress() || port <= 0)
158-
search.client = from;
168+
search.client = new InetSocketAddress(from.getAddress(), port);
159169
else
160170
search.client = new InetSocketAddress(addr, port);
161171

@@ -200,7 +210,7 @@ else if ("tcp".equals(protocol))
200210
+ (search.tls ? " (TLS)" : "")
201211
+ (search.unicast ? " (unicast)" : "")
202212
+ (search.reply_required ? " (reply required)" : "")
203-
+ (search.reply_to_src_port ? " (reply to source port)" : ""));
213+
+ (search.reply_to_src_port ? (origin == null ? " (reply to source port)" : " (reply to source port ignored because of origin tag)") : ""));
204214
search.channels.add(new Channel(cid, name));
205215
}
206216
}
@@ -221,10 +231,11 @@ public static void encode(final boolean unicast, final boolean use_src_port,
221231
final InetSocketAddress address, final boolean tls,
222232
final ByteBuffer buffer)
223233
{
234+
final int start = buffer.position();
224235
// Create with zero payload size, to be patched later
225236
PVAHeader.encodeMessageHeader(buffer, PVAHeader.FLAG_NONE, PVAHeader.CMD_SEARCH, 0);
226237

227-
final int payload_start = buffer.position();
238+
final int payload_start = start + PVAHeader.HEADER_SIZE;
228239

229240
// SEARCH message sequence
230241
// PVXS sends "find".getBytes() instead
@@ -282,6 +293,6 @@ public static void encode(final boolean unicast, final boolean use_src_port,
282293
}
283294

284295
// Update payload size
285-
buffer.putInt(PVAHeader.HEADER_OFFSET_PAYLOAD_SIZE, buffer.position() - payload_start);
296+
buffer.putInt(start + PVAHeader.HEADER_OFFSET_PAYLOAD_SIZE, buffer.position() - payload_start);
286297
}
287298
}

core/pva/src/main/java/org/epics/pva/common/TCPHandler.java

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -394,14 +394,14 @@ private ByteBuffer assertBufferSize(final ByteBuffer buffer, final int size)
394394
*/
395395
private void handleMessage(final ByteBuffer buffer) throws Exception
396396
{
397-
final byte flags = buffer.get(2);
397+
final byte flags = buffer.get(PVAHeader.HEADER_OFFSET_FLAGS);
398398
final byte segemented = (byte) (flags & PVAHeader.FLAG_SEGMENT_MASK);
399399
if (segemented != 0)
400400
handleSegmentedMessage(segemented, buffer);
401401
else
402402
{
403403
final boolean control = (flags & PVAHeader.FLAG_CONTROL) != 0;
404-
final byte command = buffer.get(3);
404+
final byte command = buffer.get(PVAHeader.HEADER_OFFSET_COMMAND);
405405
// Move to start of potential payload
406406
if (buffer.limit() >= 8)
407407
buffer.position(8);
@@ -462,11 +462,11 @@ else if (segments.position() > 0)
462462
if (segments == null || segments.position() <= 0)
463463
throw new Exception("Received " + (last ? "last" : "middle") + " message segment without first segment");
464464
// Check if command matches the one in first segment
465-
final byte seg_command = segments.get(3);
466-
if (seg_command != buffer.get(3))
465+
final byte seg_command = segments.get(PVAHeader.HEADER_OFFSET_COMMAND);
466+
if (seg_command != buffer.get(PVAHeader.HEADER_OFFSET_COMMAND))
467467
throw new Exception(String.format("Received " + (last ? "last" : "middle") +
468468
" message segment for command 0x%02X after first segment for command 0x%02X",
469-
buffer.get(3), seg_command));
469+
buffer.get(PVAHeader.HEADER_OFFSET_COMMAND), seg_command));
470470

471471
// Size of segments accumulated so far..
472472
final int seg_size = segments.getInt(PVAHeader.HEADER_OFFSET_PAYLOAD_SIZE);

core/pva/src/main/java/org/epics/pva/server/SearchCommandHandler.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ public void handleCommand(final ServerTCPHandler tcp, final ByteBuffer buffer) t
3030
final byte version = buffer.get(PVAHeader.HEADER_OFFSET_VERSION);
3131
final int payload_size = buffer.getInt(PVAHeader.HEADER_OFFSET_PAYLOAD_SIZE);
3232

33-
final SearchRequest search = SearchRequest.decode(tcp.getRemoteAddress(), version, payload_size, buffer);
33+
final SearchRequest search = SearchRequest.decode(null, tcp.getRemoteAddress(), version, payload_size, buffer);
3434

3535
if (search.channels != null)
3636
for (SearchRequest.Channel channel : search.channels)

0 commit comments

Comments
 (0)