Skip to content

fix(opcua): listen on every interface for a 0.0.0.0 endpoint - #226

Open
Neira12 wants to merge 4 commits into
Autonomy-Logic:developmentfrom
Neira12:fix/opcua-bind-all-interfaces
Open

Neira12 wants to merge 4 commits into
Autonomy-Logic:developmentfrom
Neira12:fix/opcua-bind-all-interfaces

Conversation

@Neira12

@Neira12 Neira12 commented Oct 23, 2026 •

Copy link
Copy Markdown

Summary

Fixes #210: with the OPC-UA server set to "All interfaces (0.0.0.0)", the runtime listens on a single interface only.

normalize_endpoint_url() replaces 0.0.0.0 with one reachable address (get_local_ip(), the default-route address) so clients get a usable endpoint URL. asyncua listens on the endpoint host, so the listener ended up bound to that one address: other LANs, loopback and Docker interfaces were not reachable (on the reporter's host, only the modem WAN address was).

The fix keeps the normalized URL as the advertised endpoint and sets asyncua's Server.socket_address to ("0.0.0.0", port) when the configured endpoint is 0.0.0.0, so the server listens on every interface. asyncua's discovery already rewrites the endpoint URL to the address each client connected to (match_discovery_source_ip, on by default), so clients on any interface are told an address they can reach. Endpoints with a specific host are unchanged.

Not in this PR: the issue comment about retrying when an interface has no address yet at startup, and :: (IPv6 any) endpoints.

Ticket

External contribution, no RTOP ticket. Closes #210.

How it was tested

  • New tests/pytest/plugins/opcua/test_endpoint_bind.py (10 tests): bind_address_for() cases; OpcuaServerManager._setup_server() plus a real asyncua start, checking the listening socket is 0.0.0.0 for an all-interfaces endpoint and the given host otherwise; and a TCP connection on loopback and on a non-loopback address while the endpoint advertises the non-loopback one.
  • Without the server.py change, the two all-interfaces tests fail: the socket is bound to the advertised address only and the loopback connection is refused. With it, all 10 pass.
  • Added the file to the "plugin suites known green" CI step; that step (test_string_types.py, test_read_status.py, test_endpoint_bind.py) passes locally: 36 passed.
  • Whole tests/pytest/plugins/opcua directory: 47 failed / 264 passed on development before the change, 47 failed / 274 passed after; the 47 failures are the same tests (pre-existing, e.g. the test project's opcua.json is format_version 0).
  • Manual end-to-end check with an asyncua Client: sessions opened on both 127.0.0.1 and the host's LAN address against an all-interfaces server; each client was given the endpoint URL of the address it used.
  • Python 3.13, asyncua 1.1.8 (the version pinned in the plugin's requirements.txt). Not tested on a Raspberry Pi or in the Docker install.

Checklist

  • bash scripts/run-pytest.sh passes: not run as a whole; the OPC-UA results are above. The opcua directory is red on development before this change.
  • pre-commit run clean: the new test file passes black, isort, ruff and ruff-format. server.py and opcua_endpoints_config.py are not black-formatted on development, so they were not reformatted here; the change adds no new ruff findings. pylint was not run.
  • Docs updated if behavior changed (README, CLAUDE.md, docs/): no docs describe this behaviour.
  • Follows the shared review process (summary in .claude/review-guidelines.md): Python plugin only, no IPC or scan-cycle change, new behaviour covered by pytest, type hints on the new function.

Contributor License Agreement

🤖 Generated with Claude Code

https://claude.ai/code/session_015ZahKj3kRaL9bQVSUQSHjK

Neira12 and others added 4 commits October 23, 2026 18:28
normalize_endpoint_url() swaps 0.0.0.0 for one reachable address so clients get a usable endpoint URL, but asyncua listens on the endpoint host, so the server ended up listening on that one address only. Keep the normalized URL as the advertised endpoint and set socket_address to 0.0.0.0 so the listener covers every interface.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015ZahKj3kRaL9bQVSUQSHjK
Returns ("0.0.0.0", port) for an opc.tcp://0.0.0.0 endpoint and None otherwise; server.py uses it as the asyncua socket address.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015ZahKj3kRaL9bQVSUQSHjK
Covers bind_address_for(), the listening socket after a real asyncua start, and TCP connections on loopback and a non-loopback address.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015ZahKj3kRaL9bQVSUQSHjK
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.

1 participant