ssh 端口转发 ("ssh -fNL") 无法通过 expect spawn 自动提供密码

ssh port forwarding ("ssh -fNL") doesn't work via expect spawn to automatically provide password

我知道要做端口转发,命令是ssh -L。我还使用其他选项来装饰它。因此,例如,最终的完整命令可能如下所示 ssh -fCNL *:10000:127.0.0.1:10001 127.0.0.1。输入密码后一切正常。

然后,因为不是只有一个端口需要转发,所以我决定把工作交给shell脚本,用expect(tcl)提供密码(都一样)。

虽然对expect没有很深的理解,但还是借助网络写出了代码。该脚本成功生成 ssh 并提供正确的密码。但是当我尝试使用 ps -ef | grep sshnetstat -anp | grep 10000.

进行检查时,我最终发现没有这样的过程

我给 ssh -v 选项,输出似乎没问题。

那么问题出在哪里呢?我通过 Internet 进行了搜索,但大多数问题都与端口转发无关。我不确定使用expect是否合适,而我只想让脚本自动提供密码。

这是脚本。

#!/bin/sh

# Port Forwarding

# set -x

## function definition
connection ()
{
    ps -ef | grep -v grep | grep ssh | grep  | grep  > /dev/null
    if [ $? -eq 0 ] ; then
        echo "forward  ->  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 *::127.0.0.1: 127.0.0.1
expect {
"yes/no" {send "yes\r" ; exp_continue}
"password:" {send "1234567\r" ; exp_continue}
eof
}
catch wait result
exit [lindex $result 3]
EOF
    echo "expect ssh return $?"
    echo "forward  ->  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

这应该适合你:

spawn -ignore SIGHUP ssh -f ...

更新:

另一种解决方法是:

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

更新 2(一点解释):

ssh -f在源代码中调用daemon() to make itself a daemon. See ssh.c

/* 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() is implemented like this:

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 && (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() terminates the calling process immediately" and setsid() is much more heavy weight. So when the parent process exits, setsid() is not effective yet and the child process is still in the same session as the parent process. According to the apue book(我指的是 2005 版,第 10 章:信号),SIGHUP”也会生成 if会话负责人终止。在这种情况下,信号被发送到前台进程组中的每个进程

简而言之:

  • Expect 分配一个 pty 并在 pty 上运行 ssh。在这里,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.c中)编译ssh并验证:

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 && (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));
}