Created
April 24, 2023 22:14
-
-
Save kousu/fcc61e963348784a780f0ba8c9d9da90 to your computer and use it in GitHub Desktop.
Prototype a workaround for unix domain socket length limit of 107.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/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