New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Bind client-side UDP sockets. #551
Conversation
This fixes the critical Windows bug introduces in v0.4.0. Closes caproto#540 Closes caproto#514 This takes the approach first proposed by @ke-zhang-rd in caproto#543 commit a3fb3d5 and applies it uniformly to all clients. Also, we bind immediately after creating the socket.
A little more explanation: My understanding is that I don't think there is any harm in explicitly binding earlier so that the code that follows can assume that the socket is definitely already bound and safe to call |
I'm not entirely sure this is the best or most correct solution (consider the rationale presented in https://laforge.gnumonks.org/blog/20171020-local_ip_unbound_udp/), as specifically it may not give you the address you expect. Try it yourself with I'd also consider the best place for this |
My first thought was to put it in I'd like to settle this as correctly as we can. I don't think I fully understand the issue yet. Isn't true that In [21]: sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
In [22]: sock.getsockname()
Out[22]: ('0.0.0.0', 0)
In [23]: sock.sendto(b'asdf', ('127.0.0.1', 5)) # implicitly binds?
Out[23]: 4
In [24]: sock.getsockname()
Out[24]: ('0.0.0.0', 35984)
In [25]: sock.sendto(b'asdf', ('172.17.0.1', 5))
Out[25]: 4
In [26]: sock.getsockname()
Out[26]: ('0.0.0.0', 35984) |
It seems that In [32]: sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
In [33]: sock.connect(('172.17.0.1', 5000))
In [34]: sock.getsockname()
Out[34]: ('172.17.0.1', 60155)
In [35]: sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
In [36]: sock.connect(('127.0.0.1', 5000))
In [37]: sock.getsockname()
Out[37]: ('127.0.0.1', 47564)
In [38]: sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
In [39]: sock.connect(('', 5000))
In [40]: sock.getsockname()
Out[40]: ('127.0.0.1', 47173) but In [41]: sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
In [42]: sock.bind(('', 5000))
In [43]: sock.getsockname()
Out[43]: ('0.0.0.0', 5000) |
Let's imagine we have differing
What should happen for case (3)? How about a separate |
That sounds good to me. Would we then change |
I guess this also tells us that there is no single "client address". Thus the |
(P.S. Thanks for taking the trouble to walk me through this, @klauer.) |
(If I'm being honest, I was walking myself through it as well ;) ) UDP socket per interface (multiple addresses might map to the same interface, after all) seems the most correct. That would likely be a rather large refactor... |
Socket per interface sounds correct. I'm not clear on how to get that from the target though. If I want to |
I was thinking something along the lines of this, if not using the auto address list: test_socket = socket.socket(...)
sockets_by_interface = {}
sockets_by_ca_addr = {}
for ca_addr in CA_ADDR_LIST:
test_socket.connect((ca_addr, arbitrary_port))
interface_addr = test_socket.getsockname()[0]
if interface_addr not in sockets_by_interface:
sockets_by_interface[interface_addr] = s = socket.socket(...)
# s.connect? s.bind?
sockets_by_ca_addr[ca_addr] = sockets_by_interface[interface_addr] |
Right...I was thinking that I think this refactor isn't prohibitively hard to execute. We'll see.... |
Just a thought, what if instead of the additional sockets there was just a mapping such as |
One socket would be better if it were possible. But it looks like after the first connection, the name sticks. Am I missing a way to do this with just one socket? In [2]: sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
In [3]: sock.connect(('172.17.0.1', 5000))
In [4]: sock.getsockname()
Out[4]: ('172.17.0.1', 53756)
In [5]: sock.connect(('127.0.0.1', 5000))
In [6]: sock.getsockname()
Out[6]: ('172.17.0.1', 53756)
In [7]: sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
In [8]: sock.connect(('127.0.0.1', 5000))
In [9]: sock.getsockname()
Out[9]: ('127.0.0.1', 53302)
In [10]: sock.connect(('172.17.0.1', 5000))
In [11]: sock.getsockname()
Out[11]: ('127.0.0.1', 53302) |
Oh, perhaps you mean we should map out the network topology using several short-lived sockets (connect/getsockname/close) at the start and then use just one socket for all further communication during the lifecycle of the |
We could get the IPs this way but I don't think we would know the ports. |
Hmm, yeah... just a half-baked thought... |
Another half-baked thought: If I understand correctly, In [38]: sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
In [39]: sock.sendto(b'', ('127.0.0.1', 5000))
Out[39]: 0
In [40]: sock.getsockname()
Out[40]: ('0.0.0.0', 42078) But if move to an explicit In [41]: sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
In [42]: sock.connect(('127.0.0.1', 5000))
In [43]: sock.getsockname()
Out[43]: ('127.0.0.1', 39819) Then it seems we have two options:
If this is right, then (2) is a lazier approach that lets the system take care of more things for us but leaves us ignorant about some details that it could hypothetically be useful to know and expose through logs. But it might not be a wrong approach. |
I agree with this summary. |
Thanks, @ke-zhang-rd. I haven't started writing any code for this yet. I'd like to feel a little more confident that we understand our options and their trade-offs before we proceed. I'm soliciting more opinions... |
The first post in epics-modules/asyn#11 seems to reinforce our current understanding. If we add a |
Just a thought: I wonder if tracing/comparing system calls for caproto vs epics-base would yield something worthwhile: https://gist.github.com/klauer/b5ff0a892126501d38efa39a7872b52c |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Based on the behavior of the following:
- https://gist.github.com/klauer/b5ff0a892126501d38efa39a7872b52c#file-caget-with-custom-addr-list-sh-L18
- https://gist.github.com/klauer/b5ff0a892126501d38efa39a7872b52c#file-caget-sh-L18
I agree with these changes now. I've dealt a considerable amount with sockets and yet there's still so much I just don't quite understand.
Nice. It's good to see this verified at the lowest level. Thanks for handling the merge as well. |
This fixes the critical Windows bug introduced in v0.4.0. Tested
interactively on a Windows VM.
Closes #540
Closes #514
This takes the approach first proposed by @ke-zhang-rd in #543 commit a3fb3d5 and applies it uniformly to all clients. Everywhere that we use
caproto._utils.bcast_socket()
in a client, we then immediately bind to('', 0)
.I intend to backport this to v0.4.x.