Skip to content

server: follow TCP port fallback on all interfaces#195

Open
physwkim wants to merge 1 commit into
epics-base:masterfrom
physwkim:fix/multi-iface-tcp-fallback
Open

server: follow TCP port fallback on all interfaces#195
physwkim wants to merge 1 commit into
epics-base:masterfrom
physwkim:fix/multi-iface-tcp-fallback

Conversation

@physwkim

Copy link
Copy Markdown
Contributor

A server binding several interfaces aborts if the configured TCP port is in use. The first interface falls back to a dynamically assigned port, but the rest retry the original port, which is still busy, and are not allowed to fall back in turn. The Server ctor then throws, so an IOC loses its PVA server entirely where a single-interface server would have survived.

Reproduce with a second softIocPVX started under:

EPICS_PVAS_INTF_ADDR_LIST="127.0.0.1 192.168.2.127 100.71.48.68"
EPICS_PVAS_SERVER_PORT=5085

The second IOC reports

ERR pvxs.tcp.setup Bind to 127.0.0.1:5085 fails after fallback with 98
Error in pvxsBaseRegistrar : Address already in use

while iocInit() otherwise completes, leaving an IOC with no PVA server at all.

Interfaces which do not name a port already adopt effective.tcp_port, which is how a server with EPICS_PVAS_SERVER_PORT=0 puts all of its interfaces on one dynamically assigned port. This change extends that to the port found in use: an interface asking for the server TCP port follows the server onto the port actually bound.

Note the bug is only reachable through the environment: split_addr_into() fills in $EPICS_PVAS_SERVER_PORT for entries which omit a port, so by the time the interface loop runs, "use the server's port" is spelled either as port zero or as the configured port itself. Both now follow the fallback. An interface naming some other explicit port is a deliberate choice and keeps it.

Includes a regression test (testconflictifaces in testwild.cpp) using the existing wildcard-bind occupancy pattern; it fails deterministically before the fix and passes after (20/20 both ways). It is gated to the Linux IP stack alongside the adjacent testconflict case, as it needs a second loopback address.

@mdavidsaver

Copy link
Copy Markdown
Member

Note the bug is only reachable through the environment: split_addr_into() fills in $EPICS_PVAS_SERVER_PORT for entries which omit a port

Maybe this is the real issue? Having the default applied twice? The first time when there is insufficient information to resolve conflicting TCP port numbers.

$EPICS_PVAS_INTF_ADDR_LIST filled the configured TCP port into every
port-less interface entry (split_addr_into with defaultPort=tcp_port).
When the first interface's port was already in use, build() picked a
fallback TCP port, but the remaining interfaces were now pinned to the
original (unbindable) port and could not follow -- the multi-interface
conflict left the server unusable.

Pass defaultPort=0 here, matching the client EPICS_PVA_INTF_ADDR_LIST
path.  A port-less interface then stays at port 0, and the existing
acceptor loop (if(addr.port()==0) addr.setPort(effective.tcp_port))
stamps every such interface with the port actually bound, fallback
included.  This removes the double application of the default the
reviewer identified, and drops the server.cpp change from the earlier
revision of this branch.

testconfig: server INTF list now round-trips port-less, symmetric with
the client assertion.  testwild.testconflictifaces exercises the
multi-interface fallback through the environment as a user configures it.
@physwkim
physwkim force-pushed the fix/multi-iface-tcp-fallback branch from 4070775 to 9348ebc Compare July 17, 2026 22:36
@physwkim

Copy link
Copy Markdown
Contributor Author

Good catch — that's the real issue. The default port is applied twice: split_addr_into() stamps $EPICS_PVAS_SERVER_PORT onto every port-less EPICS_PVAS_INTF_ADDR_LIST entry (the server path in config.cpp uses defaultPort = self.tcp_port), and then the acceptor loop tries to reconcile the port again at bind time. When the first interface forces a fallback, the already-stamped interfaces are stranded on the original, now-unbindable port.

The client path already does the right thing: EPICS_PVA_INTF_ADDR_LIST is parsed with defaultPort = 0 (config.cpp:593), so a port-less interface stays at 0 and follows the bound port. The server path was the asymmetric one.

I've reworked the branch to fix it there instead: the server EPICS_PVAS_INTF_ADDR_LIST parse now also passes defaultPort = 0, and I've reverted the server.cpp change — the existing if(addr.port()==0) addr.setPort(effective.tcp_port) in the acceptor loop already stamps every port-less interface with the port actually bound, fallback included. No second application, no special-casing.

One visible consequence worth your call: the server INTF list now round-trips port-less (applyDefsinterfaces == {"1.1.1.1","1.2.3.4"} rather than {"1.1.1.1:5678","1.2.3.4:5678"}), which is exactly what the client side already does. I updated the testconfig assertion to match. If you'd rather keep the server list stamped for display and instead special-case the follow-me at bind time, say the word and I'll go the other way.

Verified: testconfig passes with the symmetric assertion, and testwild.testconflictifaces (multi-interface fallback configured through the environment as a user would) passes — both interfaces follow the fallback port.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants