nuta / nsh

A command-line shell like fish, but POSIX compatible.
901 stars 34 forks source link

Can't ssh (via Python/Paramiko) to Alpine Docker container, as user whose shell is nsh #50

Closed JamesParrott closed 6 months ago

JamesParrott commented 6 months ago

[edit] Apologies. The hang only occurs when sshing in with Paramiko, a Python library. This bug is an even smaller niche, and even lower priority for you guys therefore, but I'll leave it up for posterity.

Hi there,

I've been running some tests, ssh-ing into a local Docker container from Python, using multiple shells. When I try as a user whose shell is nsh, the test script hangs.

It's entirely possible I've misconfigured nsh, or encountered one of those strange Musl Alpine bugs. And it's even more likely, there's an issue in my Python code.

However I can successfully connect with Paramiko, as users whose shells are: bash , dash , fish , zsh , ion-shell , tcsh , oksh , loksh , yash, and from edge/testing: elvish , xonsh , mrsh and imrsh (and csh, ksh, rc, and a source build of heirloom-sh on Debian).

I first thought this was a general ssh issue, but it only occurs in my script. It would be above and beyond the call of duty for someone to debug my Python code, especially on a great repo you could all be working on anyway, that uses a different language. But nonetheless, I wondered why I've been unable to get the script to work for nsh, (I've had no joy with hilbish or nushell either).

To reproduce (some shells not installed for brevity):

Dockerfile:

ARG base_image=alpine
ARG base_tag=edge

FROM "${base_image}:${base_tag}" as runner

ENV LANG=C.UTF8

RUN apk add --no-cache \
    openssh \
    bash \
    yash

RUN apk add --no-cache --repository=https://dl-cdn.alpinelinux.org/alpine/edge/testing/ \
    elvish \
    mrsh \
    nsh

# Do not hardcode important passwords into Dockerfiles (and do not
# set trivially guessable passwords), as I have done below!   
#
# Use secrets, or at least environment variables.
#  
# This Dockerfile is intended to define a local testing server, 
# remote connections to which are prevented by other means.
#
RUN echo 'PasswordAuthentication yes' >> /etc/ssh/sshd_config && \
    adduser -h /home/sh -s /bin/sh -D sh && \
    echo -n 'sh:sh' | chpasswd && \
    adduser -h /home/ash -s /bin/ash -D ash && \
    echo -n 'ash:ash' | chpasswd && \
    adduser -h /home/bash -s /bin/bash  -D bash && \
    echo -n 'bash:bash' | chpasswd && \
    adduser -h /home/yash -s /usr/bin/yash -D yash && \
    echo -n 'yash:yash' | chpasswd && \
    adduser -h /home/elvish -s /usr/bin/elvish -D elvish && \
    echo -n 'elvish:elvish' | chpasswd && \
    adduser -h /home/mrsh -s /usr/bin/mrsh -D mrsh && \
    echo -n 'mrsh:mrsh' | chpasswd && \
    adduser -h /home/nsh -s /usr/bin/nsh -D nsh && \
    echo -n 'nsh:nsh' | chpasswd

RUN ssh-keygen -A

# Start SSH daemon to listen for log ins.
CMD ["/usr/sbin/sshd", "-D", "-e"]

EXPOSE 22

Build image:

docker build -t alpine_nsh

Run container (blocks process to show output from server):

docker run -p 22:22 --rm alpine_nsh

Install paramiko, preferably after first making and activating a venv (requires Python).

pip install paramiko

Run test script:

import sys

import paramiko

shells = sys.argv[1:] or ['sh', 'ash', 'bash', 'yash', 'elvish', 'nsh']

for shell in shells:

    input(f'Press Enter to test: {shell} ')

    con = paramiko.SSHClient()
    con.set_missing_host_key_policy(paramiko.AutoAddPolicy())
    con.connect('localhost', username=shell, password=shell)
    con_chann = con.invoke_shell()

    con_in = con_chann.makefile('wt')
    con_out = con_chann.makefile('rb')

    con_in.write('''\
echo "Hello World"
exit
''')
    print(con_out.read().decode())

    con_out.close()
    con_in.close()
    con.close()

I know I can just docker exec in instead. It's a long story, and besides the point, why I want to ssh to containers. But lets do that, to show nsh is otherwise working, and to check the path of the nsh binary:

docker ps

remember some characters from the start of the container_id, then the following commands:

docker exec -it container_id nsh
ls /usr/bin/nsh -l
nsh --version

respectively produce:

-rwxr-xr-x    1 root     root       1314456 May 23  2023 /usr/bin/nsh
0.4.2

To kill the container when done:

docker kill container_id
JamesParrott commented 6 months ago

Fixed it. The issue occurs due to throwing a multiline string at stdin. This fix throws nsh one command at a time and it works fine. There could be a tricky buffering / stream /stdin flushing issue, but perhaps it might be decided to support multi-line string inputs in future.

    try:
        for command in '''\
                ls -l
                echo !!
                echo $0
                exit
                '''.split('\n'):
            stdin, stdout, stderr = con.exec_command(command)
            print(stdout.read())  

    except paramiko.ssh_exception.AuthenticationException as e:
        print(e)
    finally:

        con.close()