From d04f48019496161ce4a8d47f18fa41d89056b636 Mon Sep 17 00:00:00 2001 From: goodboy Date: Fri, 14 Aug 2026 09:39:02 -0400 Subject: [PATCH] Add `Address.rebind_from_sockname` opt-out MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Gate `Endpoint.start_listener()`s `getsockname()`-vs-`.addr` reconciliation on a new per-addr-type `ClassVar[bool]`, set `True` on both `TCPAddress` and `UDSAddress` so existing behaviour is bit-for-bit unchanged. That reconciliation exists ONLY to learn a kernel-assigned port from a `port=0` tcp bind (its own comment says so). The incoming `tipc` backend (gh #378) has no late-binding analogue AND its `getsockname()` answers a `TIPC_ADDR_ID` port-id rather than the name-seq it published — rebinding from that would swap a dialable service name for an un-dialable, un-reconstructable port id. So opting out is semantically right rather than a hack. (this patch was generated in some part by `claude-code` using `claude-opus-5` (`anthropic`)) --- tractor/discovery/_addr.py | 45 +++++++++++++++++++++++++++++++++++--- tractor/ipc/_server.py | 9 ++++++++ tractor/ipc/_tcp.py | 4 ++++ tractor/ipc/_uds.py | 6 +++++ 4 files changed, 61 insertions(+), 3 deletions(-) diff --git a/tractor/discovery/_addr.py b/tractor/discovery/_addr.py index 2697c5c9..94b922fb 100644 --- a/tractor/discovery/_addr.py +++ b/tractor/discovery/_addr.py @@ -33,6 +33,7 @@ from ..runtime._state import ( ) from ..ipc._tcp import TCPAddress from ..ipc._uds import UDSAddress +from ..ipc._tipc import TIPCAddress if TYPE_CHECKING: from ..runtime._runtime import Actor @@ -65,9 +66,22 @@ log = get_logger() # UnwrappedAddress = ( # tcp/udp/uds + # ('127.0.0.1', 1616) + # ('/run/user/1000/tractor', 'registry@1616.sock') + # + # ..and the explicitly proto-keyed (`multiaddr`-spelled) + # form, which is where ALL backends should eventually land + # per the note below, + # ('tipc', 1953628160, 1616, 2) + # + # XXX VARIADIC bc `msgspec` refuses a union of >1 array-like + # type, so the two shapes can't be spelled as a union. Keep + # in sync with `.msg.types.UnwrappedAddress` which + # re-declares this to dodge a circular import AND is what + # actually validates the `SpawnSpec` wire msg! tuple[ - str, # host/domain(tcp), filesys-dir(uds) - int|str, # port/path(uds) + str|int, + ..., ] # ?TODO? should we also include another 2 fields from # our `Aid` msg such that we include the runtime `Actor.uid` @@ -83,6 +97,17 @@ class Address(Protocol): proto_key: ClassVar[str] unwrapped_type: ClassVar[UnwrappedAddress] + # whether `.ipc._server.Endpoint.start_listener()` should + # reconcile a bound `.addr` against its listener's + # `socket.getsockname()`. + # + # XXX NOTE, that reconciliation exists ONLY to learn the + # kernel-*assigned* port from a `port=0` tcp bind; a backend + # whose `getsockname()` reports a categorically different thing + # than what was `.bind()`ed must opt out with `False`, else the + # ep's addr gets clobbered by an un-dialable one. + rebind_from_sockname: ClassVar[bool] + # TODO, i feel like an `.is_bound()` is a better thing to # support? # Lke, what use does this have besides a noop and if it's not @@ -172,7 +197,8 @@ class Address(Protocol): _address_types: bidict[str, Type[Address]] = { 'tcp': TCPAddress, - 'uds': UDSAddress + 'uds': UDSAddress, + 'tipc': TIPCAddress, } @@ -184,6 +210,9 @@ _default_lo_addrs: dict[ ] = { 'tcp': TCPAddress.get_root().unwrap(), 'uds': UDSAddress.get_root().unwrap(), + # NOTE, pure/cheap: a service-name pair, no kernel module + # nor I/O required at import time. + 'tipc': TIPCAddress.get_root().unwrap(), } @@ -229,6 +258,16 @@ def wrap_address( # import pdbp; pdbp.set_trace() match addr: + # XXX, the explicitly proto-keyed form (spelled with the + # `multiaddr` proto name) which is where ALL backends + # should eventually land per the `UnwrappedAddress` + # migration note above. + # + # NOTE, a bare seq-pattern matches `list` too, which is + # what `msgpack` decodes our tuples back to. + case ('tipc', *_): + cls = TIPCAddress + # classic network socket-address as tuple/list case ( (str(), int()) diff --git a/tractor/ipc/_server.py b/tractor/ipc/_server.py index 31f4c6b0..9570b674 100644 --- a/tractor/ipc/_server.py +++ b/tractor/ipc/_server.py @@ -661,7 +661,16 @@ class Endpoint(Struct): # NOTE, for handling the resolved non-0 port for # TCP/UDP network sockets. + # + # XXX, gated on the addr-type's opt-in since for some + # backends `getsockname()` does NOT answer "the addr you + # bound"; `tipc` reports a `TIPC_ADDR_ID` port-id instead + # of the published name-seq, so rebinding from it would + # replace a dialable service-name with an un-dialable + # (and un-reconstructable) port-id. if ( + self.addr.rebind_from_sockname + and (unwrapped := lstnr.socket.getsockname()) != self.addr.unwrap() diff --git a/tractor/ipc/_tcp.py b/tractor/ipc/_tcp.py index 293ae4be..3180b411 100644 --- a/tractor/ipc/_tcp.py +++ b/tractor/ipc/_tcp.py @@ -65,6 +65,10 @@ class TCPAddress( unwrapped_type: ClassVar[type] = tuple[str, int] def_bindspace: ClassVar[str] = '127.0.0.1' + # XXX, REQUIRED here since a `port=0` bind means the kernel + # picks and `getsockname()` is the only way we learn it. + rebind_from_sockname: ClassVar[bool] = True + # ?TODO, actually validate ipv4/6 with stdlib's `ipaddress` @property def is_valid(self) -> bool: diff --git a/tractor/ipc/_uds.py b/tractor/ipc/_uds.py index d1e9d2f6..bd1a44dd 100644 --- a/tractor/ipc/_uds.py +++ b/tractor/ipc/_uds.py @@ -117,6 +117,12 @@ class UDSAddress( unwrapped_type: ClassVar[type] = tuple[str, int] def_bindspace: ClassVar[Path] = get_rt_dir() + # NOTE, `getsockname()` answers the sock-file path as a `str` + # which never `==` our 2-tuple `.unwrap()`, so the round-trip + # always fires; it's a no-op modulo `.maybe_pid` and is kept + # `True` to preserve pre-existing behaviour exactly. + rebind_from_sockname: ClassVar[bool] = True + @property def bindspace(self) -> Path: '''