переадресация портов ssh («ssh -fNL») не работает через ожидаемый спавн для автоматического предоставления пароля

#linux #ssh #sh #expect

#linux #ssh #sh #ожидать

Вопрос:

Я знаю, что для переадресации портов команда ssh -L . Я также использую другие параметры для его оформления. Так, например, окончательная полная команда может выглядеть следующим образом ssh -fCNL *:10000:127.0.0.1:10001 127.0.0.1 . И все работает только после ввода пароля.

Затем, поскольку необходимо перенаправить не только один порт, я решаю оставить работу сценарию оболочки и использовать expect (tcl) для предоставления паролей (все то же самое).

Хотя без глубокого понимания expect мне удалось написать код с помощью Интернета. Скрипт успешно создает ssh и предоставляет правильный пароль. Но в конечном итоге я обнаруживаю, что такого процесса нет, когда я пытаюсь проверить с помощью ps -ef | grep ssh и netstat -anp | grep 10000 .

Я даю -v опцию ssh, и результат, похоже, в порядке.

Итак, в чем проблема? Я искал в Интернете, но большинство вопросов не о переадресации портов. Я не уверен, правильно ли использовать expect, пока я просто хочу, чтобы скрипт автоматически предоставлял пароль.

Вот сценарий.

 #!/bin/sh

# Port Forwarding

# set -x

## function definition
connection ()
{
    ps -ef | grep -v grep | grep ssh | grep $1 | grep $2 > /dev/null
    if [ $? -eq 0 ] ; then
        echo "forward $1 -> $2 done"
        exit 0
    fi

    # ssh-keygen -f "$HOME/.ssh/known_hosts" -R "127.0.0.1"

    /usr/bin/expect << EOF
set timeout 30
spawn /usr/bin/ssh -v -fCNL *:$1:127.0.0.1:$2 127.0.0.1
expect {
"yes/no" {send "yesr" ; exp_continue}
"password:" {send "1234567r" ; exp_continue}
eof
}
catch wait result
exit [lindex $result 3]
EOF
    echo "expect ssh return $?"
    echo "forward $1 -> $2 done"
}

## check expect available
which expect > /dev/null
if [ $? -ne 0 ] ; then
    echo "command expect not available"
    exit 1
fi

login_port="10000"
forward_port="10001"

## check whether the number of elements is equal
login_port_num=$(echo ${login_port} | wc -w)
forward_port_num=$(echo ${forward_port} | wc -w)
if [ ${login_port_num} -ne ${forward_port_num} ] ; then
    echo "The numbers of login ports and forward ports are not equal"
    exit 1
fi
port_num=${login_port_num}

## provide pair of arguments to ssh main function
index=1
while [ ${index} -le ${port_num} ] ; do
    login_p=$(echo ${login_port} | awk '{print $'$index'}')
    forward_p=$(echo ${forward_port} | awk '{print $'$index'}')
    connection ${login_p} ${forward_p}
    index=$((index   1))
done
  

Вот вывод из скрипта

 spawn /usr/bin/ssh -v -fCNL *:10000:127.0.0.1:10001 127.0.0.1
OpenSSH_7.2p2 Ubuntu-4ubuntu2.10, OpenSSL 1.0.2g  1 Mar 2016
...
debug1: Next authentication method: password
wang@127.0.0.1's password: 
debug1: Enabling compression at level 6.
debug1: Authentication succeeded (password).
Authenticated to 127.0.0.1 ([127.0.0.1]:22).
debug1: Local connections to *:10000 forwarded to remote address 127.0.0.1:10001
debug1: Local forwarding listening on 0.0.0.0 port 10000.
debug1: channel 0: new [port listener]
debug1: Local forwarding listening on :: port 10000.
debug1: channel 1: new [port listener]
debug1: Requesting no-more-sessions@openssh.com
debug1: forking to background
expect ssh return 0
forward 10000 -> 10001 done
  

Комментарии:

1. Не используйте пароли. Используйте ключи.

Ответ №1:

Это должно сработать для вас:

 spawn -ignore SIGHUP ssh -f ...
  

Обновить:

Другой обходной путь:

 spawn bash -c "ssh -f ...; sleep 1"
  

ОБНОВЛЕНИЕ 2 (небольшое объяснение):

ssh -f вызывает daemon(), чтобы сделать себя демоном. Смотрите ssh.c в коде souce:

 /* Do fork() after authentication. Used by "ssh -f" */
static void
fork_postauth(void)
{
        if (need_controlpersist_detach)
                control_persist_detach();
        debug("forking to background");
        fork_after_authentication_flag = 0;
        if (daemon(1, 1) == -1)
                fatal("daemon() failed: %.200s", strerror(errno));
}
  

daemon() реализован следующим образом:

 int
daemon(int nochdir, int noclose)
{
        int fd;

        switch (fork()) {
        case -1:
                return (-1);
        case 0:
                break;
        default:
                _exit(0);
        }

        if (setsid() == -1)
                return (-1);

        if (!nochdir)
                (void)chdir("/");

        if (!noclose amp;amp; (fd = open(_PATH_DEVNULL, O_RDWR, 0)) != -1) {
                (void)dup2(fd, STDIN_FILENO);
                (void)dup2(fd, STDOUT_FILENO);
                (void)dup2(fd, STDERR_FILENO);
                if (fd > 2)
                        (void)close (fd);
        }
        return (0);
}
  

Существует условие гонки (не уверен, что это правильный термин для этого) между _exit() в родительском процессе и setsid() в дочернем процессе. Здесь _exit() всегда будет выполняться первой, поскольку «функция _exit() немедленно завершает вызывающий процесс», а setsid() имеет гораздо больший вес. Поэтому, когда родительский процесс завершает работу, setsid() еще не действует, и дочерний процесс все еще находится в том же сеансе, что и родительский процесс. Согласно книге apue (я имею в виду издание 2005 года, глава 10: Сигналы), SIGHUP «также генерируется , если руководитель сеанса завершается. В этом случае сигнал отправляется каждому процессу в группе процессов переднего плана

Вкратце:

  • Expect выделяет pty и выполняется ssh на pty. Здесь, ssh будет запущен в новом сеансе и будет лидером сеанса.
  • ssh -f вызовы daemon() . Родительский процесс (руководитель сеанса) вызывает _exit() . В это время дочерний процесс все еще находится в сеансе, поэтому он получит SIGHUP поведение по умолчанию для завершения процесса.

Как работают обходные пути:

  • Способ nohup ( spawn -ignore SIGHUP ) — явно попросить процесс игнорировать SIGHUP , чтобы он не был завершен.
  • Для bash -c 'sshh -f ...; sleep 1' , bash был бы лидером сеанса и sleep 1 в конце концов предотвращает преждевременный выход лидера сеанса. Таким образом, после того, как sleep 1 дочерний ssh процесс setsid() уже выполнен, и дочерний ssh процесс уже находится в новом сеансе процесса, в котором он находится, — в.

ОБНОВЛЕНИЕ 3:

Вы можете скомпилировать ssh со следующей модификацией (в ssh.c) и проверить:

 static int
my_daemon(int nochdir, int noclose)
{
    int fd;

    switch (fork()) {
    case -1:
        return (-1);
    case 0:
        break;
    default:
        // wait a while for child's setsid() to complete
        sleep(1);
//      ^^^^^^^^
        _exit(0);
    }

    if (setsid() == -1)
        return (-1);

    if (!nochdir)
        (void)chdir("/");

    if (!noclose amp;amp; (fd = open(_PATH_DEVNULL, O_RDWR, 0)) != -1) {
        (void)dup2(fd, STDIN_FILENO);
        (void)dup2(fd, STDOUT_FILENO);
        (void)dup2(fd, STDERR_FILENO);
        if (fd > 2)
            (void)close (fd);
    }
    return (0);
}

/* Do fork() after authentication. Used by "ssh -f" */
static void
fork_postauth(void)
{
    if (need_controlpersist_detach)
        control_persist_detach();
    debug("forking to background");
    fork_after_authentication_flag = 0;
    if (my_daemon(1, 1) == -1)
//      ^^^^^^^^^
        fatal("my_daemon() failed: %.200s", strerror(errno));
}
  

Комментарии:

1. вау, работает как шарм! Позвольте мне угадать. Процесс ssh принадлежит expect. Когда ожидание завершается, SIGHUP также отправляется ssh-процессу, и поэтому он закрывается. Если это правильно, откуда вы это знаете? Простите меня, если это упомянуто в руководстве по ожиданию, но я сделал быстрый поиск и никаких результатов.

2. Большое спасибо! По-видимому, мне нужно больше изучить, чтобы полностью понять эти вещи, лол. Но мне любопытно, как можно так глубоко погрузиться, чтобы изучить исходный код и понять механизм. В любом случае, это огромный стимул заставить меня узнать больше!