1

I am trying to duplicate the behavior of this socat command in Python:

socat PTY,link=/tmp/mypty tcp-listen:1234

A TCP connection to the server port 1234 will be connected to /tmp/mypty (a link to /dev/pts/N), and running screen /tmp/mypty will allow a user to interact with whatever is on the other end of that connection. In other words, if this is used to connect to that listening port:

socat exec:'bash -li',pty,stderr,setsid,sigint,sane tcp-connect:172.16.30.3:1234

Then running screen /tmp/mypty will connect the user to the instance of bash running on the other end of the TCP connection.

Getting the TCP socket is straightforward. And I can get a PTY using os.openpty(), which yields me a pair of file descriptors, which are just integers.

What I am unable to do is hook that socket and those file descriptors in a way that will seamlessly pass data back and forth. If they were both sockets, it would be trivial; use a loop with select() to pass new read data to the other socket's write.

What I have tried:

  1. Using socket.fromfd to build a socket using one of the PTY file descriptors. This results in OSError: [Errno 88] Socket operation on non-socket. Example of this code, where clisock is a valid socket:
    def sockToPty(clisock,addr):
        (master, slave) = os.openpty()
        print('PTY: Opening {}'.format(os.ttyname(slave)))
        ptymsock = socket.fromfd(master, socket.AF_UNIX, socket.SOCK_STREAM)
        ptyssock = socket.fromfd(slave,  socket.AF_UNIX, socket.SOCK_STREAM)
        print('CLISOCK: {}'.format(clisock))
        print('PTYMSOCK: {}'.format(ptymsock))
        print('PTYSSOCK: {}'.format(ptyssock))
        peer[clisock] = ptymsock
        peer[ptyssock] = clisock
        while True:
            iready, oready, eready = select.select([clisock, ptyssock], [], [])

            for sock in iready:
                data = sock.recv(4096)
                if not len(data):
                    return
                peer[sock].send(data)

which results in this error:

    PTY: Opening /dev/pts/2
    CLISOCK: 
    PTYMSOCK: 
    PTYSSOCK: 
    Exception in thread Thread-1:
    Traceback (most recent call last):
      File "/usr/lib/python3.6/threading.py", line 916, in _bootstrap_inner
        self.run()
      File "/usr/lib/python3.6/threading.py", line 864, in run
        self._target(*self._args, **self._kwargs)
      File "./tcpmux.py", line 62, in sockToPty
        peer[sock].send(data)
    OSError: [Errno 88] Socket operation on non-socket

and it makes no difference if I use AF_INET instead of AF_UNIX.

  1. Using os.dup2() to map the SSLsocket's fileno() to stdin, stdout, and stderr and then running os.openpty() (a la tcp_pty_bind.py). This does not generate any errors, but there's no apparent data flow when I open the PTY using screen.

Almost all of the Python PTY examples out there are focused on pty.spawn(), making it hard to find any example code of this (admittedly obscure) use of PTY.

Side note: I'm using os.openpty instead of pty.openpty simply because there doesn't seem to be a point. The pty module seems to be advertised for more portability, but not to have much more portability, and I'm working on Linux so portability isn't an issue. And since I'm getting valid file descriptors to a correct os.ttyname(), I've no reason to question that openpty() isn't working properly.

1 Answer 1

1

This can be done using select.poll() instead of select.select(), because select.poll() will work with both File Descriptors and Sockets.

def sockToPty(clisock,addr):
    (master, slave) = os.openpty()
    tty.setraw(master, termios.TCSANOW)
    print('PTY: Opened {} for {}'.format(os.ttyname(slave), addr))
    mypoll = select.poll()
    mypoll.register(clisock, select.POLLIN)
    mypoll.register(master, select.POLLIN)
    try:
        while True:
            fdlist = mypoll.poll(1000)
            for fd,event in fdlist:
                # We treat sockets and FDs differently
                if fd == master:
                    data = os.read(fd, 4096)
                    if len(data) == 0:
                        print('PTY: {}, {} exiting due to Zero read.'.format(addr, os.ttyname(slave)))
                        raise GetOut
                    clisock.send(data)
                else:
                    data = clisock.recv(4096)
                    if len(data) == 0:
                        print('PTY: {}, {} exiting due to Zero read.'.format(addr, os.ttyname(slave)))
                        raise GetOut
                    os.write(master, data)
    except GetOut:
        os.close(master)
        os.close(slave)
        clisock.shutdown(socket.SHUT_RDWR)
        clisock.close()

This code is more complex than it needs to be for simple sockets - with a TCP socket, you can simply use the fileno() for that socket as a File Descriptor and then you treat both sides the same way (os.read(), os.write()). However, if you're going to use this with an SSLSocket, you need the more complicated version above, because if you grab the fileno() from an SSLSocket it expects you're going to handle the TLS layer as well as the data.

Sign up to request clarification or add additional context in comments.

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.