Skip to content

Instantly share code, notes, and snippets.

@kousu
Created April 24, 2023 22:14
Show Gist options
  • Save kousu/fcc61e963348784a780f0ba8c9d9da90 to your computer and use it in GitHub Desktop.
Save kousu/fcc61e963348784a780f0ba8c9d9da90 to your computer and use it in GitHub Desktop.
Prototype a workaround for unix domain socket length limit of 107.
#!/usr/bin/env python
# this is a tiny little clone of `nc -U -l path`
# but it creates the socket *after chdir()ing in a subprocess* to avoid ENAMETOOLONG:
#
# To test the difference, get openbsd-netcat and run:
#
# mkdir -p /tmp/abcdefhijklmnopqrstuvwxyz/abcdefhijklmnopqrstuvwxyz/abcdefhijklmnopqrstuvwxyz/abcdefhijklmnopqrstuvwxyz
# nc -v -U -l /tmp/abcdefhijklmnopqrstuvwxyz/abcdefhijklmnopqrstuvwxyz/abcdefhijklmnopqrstuvwxyz/abcdefhijklmnopqrstuvwxyz/ipc.sock
#
# This will fail with "nc: File name too long", but
#
# ./nc-chdir /tmp/abcdefhijklmnopqrstuvwxyz/abcdefhijklmnopqrstuvwxyz/abcdefhijklmnopqrstuvwxyz/abcdefhijklmnopqrstuvwxyz/ipc.sock
#
# starts, and can be communicated with by
#
# (cd /tmp/abcdefhijklmnopqrstuvwxyz/abcdefhijklmnopqrstuvwxyz/abcdefhijklmnopqrstuvwxyz/abcdefhijklmnopqrstuvwxyz/ && nc -v -U ipc.sock)
import sys, os
import socket, select
import traceback
def reliable_bind(socket, path):
"""
This is a wrapper for socket.socket.bind(), with the same API.
But it works for paths of (almost) any length rather than being
limited to about 100: https://unix.stackexchange.com/questions/367008/why-is-socket-path-length-limited-to-a-hundred-chars
(this will still fail if len(basename(path)) > 100, or thereabouts; just don't do that)
"""
o = socket.get_inheritable()
try:
socket.set_inheritable(True) # this is a pythonism, to ensure the parent and child can share file descriptors
child = os.fork()
if child == 0:
# child process
try:
# in the subprocess, chdir() to the desired path
# so that we only need to bind() on basename(path);
# so long as the actual filename is not too long,
# this should succeed.
#
# The reason for using a child process is so that we can call chdir() -- which sets a global variable -- without having to worry about concurrency,
# or undoing the chdir() afterwards -- when the child process dies the chdir() is forgotten, but the socket is left bound correctly.
os.chdir(os.path.dirname(path))
# On Linux at least(?) this bind() is picked up correctly on the matching file descriptor in the parent.
socket.bind(os.path.basename(path))
except OSError as exc:
# assume that most of the errors are OSErrors
# and pass them back via the process's exit code
raise SystemExit(exc.errno) # error
except Exception as exc:
# but in really exceptional circumstances (low memory? corrupt process code?)
# encode that as code 255, which should be greater than any defined errno
traceback.print_exc()
raise SystemExit(255)
raise SystemExit(0) # success
else:
# parent process
_, status = os.waitpid(child, 0)
errno = os.waitstatus_to_exitcode(status)
if errno != 0:
if errno == 255:
raise Exception("Unknown error occurred in child")
raise OSError(errno, os.strerror(errno))
finally:
socket.set_inheritable(o)
#reliable_bind = socket.socket.bind # no subdir
def main():
addr = sys.argv[1]
s = socket.socket(socket.AF_UNIX)
reliable_bind(s, addr)
print("Bound on", s.getsockname())
try:
s.listen()
print("Listening on", s.getsockname())
S, remote = s.accept()
print("Connection received on ", S.getsockname())
# XXX 'remote' is empty; on linux, to find out who is talking to you, you have to use recvmsg() and look at the "acnillary data": https://stackoverflow.com/questions/8104904/identify-program-that-connects-to-a-unix-domain-socket
while True:
readable, _, _ = select.select([S, sys.stdin], [], [])
for fd in readable:
if fd == S:
buf = S.recv(512)
if not buf:
# socket disconnected
return
sys.stdout.buffer.raw.write(buf) # TODO: .buffer.raw?? should I just use os.write(sys.stdout.fileno())??
elif fd == sys.stdin:
buf = sys.stdin.buffer.raw.read(512) # TODO: ditto
if not buf:
return
S.send(buf)
except KeyboardInterrupt:
pass
finally:
os.unlink(addr) # TODO: is there a 'with:' for unix domain sockets?
if __name__ == '__main__':
main()
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment