问题描述
我知道要进行端口转发,命令是ssh -L
。我还使用其他选项来装饰它。因此,例如,最终的完整命令可能看起来像这样ssh -fCNL *:10000:127.0.0.1:10001 127.0.0.1
。输入密码后,一切都将正常工作。
然后,因为不仅需要转发一个端口,所以我决定将作业留给shell脚本,并使用Expect(tcl)提供密码(全部相同)。
尽管对期望没有深入的了解,但我还是设法借助Internet编写了代码。该脚本会成功生成ssh并提供正确的密码。但是当我尝试使用ps -ef | grep ssh
和netstat -anp | grep 10000
进行检查时,最终发现没有这样的过程。
我给ssh -v
选项,输出似乎很好。
那么问题出在哪里呢?我已经通过Internet搜索过,但是大多数问题都与端口转发无关。我只是想让脚本自动提供密码,所以不确定使用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 "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 $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
解决方法
这应该对您有用:
spawn -ignore SIGHUP ssh -f ...
# OR
spawn nohup ssh -f ...
更新:
另一个解决方法是:
spawn bash -c "ssh -f ...; sleep 1"
更新2(有点解释):
ssh -f
调用daemon()使其成为守护程序。参见源代码中的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));
}
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()立即终止调用过程 ”,而setsid()的工作量更大。因此,当父进程退出时,setsid()
尚未生效,并且子进程仍与父进程处于同一会话中。根据{{3}}(我指的是2005年版本,第10章:信号),{strong如果会话负责人终止,也会生成SIGHUP
。在这种情况下,信号将发送到前台进程组中的每个进程。”
简而言之:
- 期望分配一个pty并在该pty上运行
ssh
。在这里,ssh
将在新会话中运行,并且成为会话负责人。 -
ssh -f
呼叫daemon()
。父进程(会话负责人)调用_exit()
。目前,子进程仍仍在会话中,因此它将获得SIGHUP
,其默认行为是终止该进程。
解决方法的工作原理:
- nohup 方法(使用
spawn -ignore SIGHUP
或nohup
命令)是明确要求该进程忽略SIGHUP
,以便它不会被终止。 - 对于
bash -c 'sshh -f ...; sleep 1'
,bash
将成为会话负责人,而sleep 1
最终将阻止会话负责人过早退出。因此,在sleep 1
之后,子ssh
进程的setsid()
已经完成,子ssh
已经在新的进程会话中。
更新3:
您可以对ssh进行以下修改(在apue book中)并进行验证:
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,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));
}
更新4:
基于以上分析,以下解决方法也适用。
# 'set -m' to enable job control which is by default disabled for
# non-interactive Bash
spawn bash -c "set -m; ssh -f ... & "
# ^^^^^^ ^^^
通过这种方式ssh
在后台pgrp 中启动,因此会话负责人终止时不会与SIGHUP
一起发送。 (如前所述,会话负责人终止时,只有前景pgrp 会与SIGHUP
一起发送。)