利用反向ssh从外网访问内网主机

前言

内网一台主机仅能在内网访问,没有公网ip,外网无法直接访问。下面是个反向ssh连接方案,从而可以连接到校园内网主机。

工作原理

内网主机B可以访问到外网服务器A,于是让B与A创建通信隧道连接,随后再通过外网主机A连接到内网主机B

所谓反向ssh最通俗的理解,这就像寄信一样,虽然我不知道你的地址,但是你知道我的地址,那么你就先给我写封信,告诉我你的地址,然后我不就可以回信给你了么?

前提:使用密钥登录

为了不输入密码,这边选择用密钥登录,下面是密钥登录的方法

内网主机B创建密钥,使用命令ssh-keygen -t rsa,这个命令会在**~/.ssh/**目录下生成一个公钥,一个私钥。

将公钥的内容,复制到服务器A的**~/.ssh/authorized_keys**(这里的~是user的home路径)内

随后,在内网主机B尝试无密码登录到服务器A(假设A的ip为12.12.12.12)

1
ssh username@12.12.12.12

如果不需要输入密码,直接登录成功,再进行下面的步骤

基本操作步骤

下面是使用原始的ssh进行连接,但ssh本身容易自动断开,所以需要用autossh保持稳定(看实际方案)

由于我们自己使用的电脑未必有公网ip,因此我们需要一个有固定公网ip的服务器

1、准备好有固定ip的服务器A,以及待访问的内网机器B。两者都开着sshd服务,端口号默认都是22。顺便做好ssh免密码登陆

2、内网主机B上执行以下命令:

1
ssh -NfR 7777:localhost:22 username@servername -p 2222

这条命令的意思是

  • 在后台执行(-f)
  • 不实际连接而是做port forwarding(-N)
  • 做反向ssh(-R)
  • 将远程服务器的7777端口映射成连接本机(B)与该服务器的反向ssh的端口

执行完ss -ant |grep 7777netstat -ap | grep 7777命令

我们可以在服务器A上看到他的7777端口已经开始监听,这个7777端口就已经映射成了内网主机B的22端口了,

1
2
ss -ant |grep 7777
LISTEN 0 128 127.0.0.1:7777 *:*

3、在服务器A中执行:

1
ssh username@localhost -p 7777

这样就成功地在服务器A本地登陆内网的主机

4、使用下面的命令,就可以通过服务器A来ssh登录到内网主机B了

1
ssh username@servername -p 7777

[!Important]

一定要在云服务器上修改sshd_config,将里面的GateWayPort后面的东西为 yes,随后一定要重启ssh服务!

否则会出现Connection refuse

1
2
3
4
5
6
7
8
$ vim /etc/ssh/sshd_config
# 修改GateWayPort后面的东西为yes
# GateWayPort yes
# 注意去除前面的 # 号
# 再保存并退出
$ systemctl restart sshd
# 一些服务器没有systemctl,用下面这个
$ service sshd restart

附:这里有必要加强一下记忆,这个端口号一不小心就容易搞混,man文档中的参数命令是这样的:

1
-R [bind_address:]port:host:hostport
  • bind_address以及其后面的port是指远程主机的ip以及端口,由于ssh命令本身需要远程主机的ip(上上条命令中的servername),因此这个bind_address原则上是可以省略的,如果不省略就写0.0.0.0。
  • host以及其后的hostport是指本机的ip和端口。

功能优化

上面的做法其实有一个问题,就是反向ssh可能会不稳定,主机B对服务器A的端口映射可能会断掉,那么这时候就需要主机B重新链接,而显然远在外地的我无法登陆B,这其实有一个非常简单的解决方案,就是用autossh替代步骤2中的ssh:

1
autossh -M 2222 -NfR 7777:localhost:22 username@servername -p 2222

后面的参数跟ssh都一样,只是多了一个-M参数,这个参数的意思就是用本机的2222端口来监听ssh,每当他断了就重新把他连起来。。。不过man文档中也说了,这个端口又叫echo port,他其实是有一对端口的形式出现,第二个端口就是这个端口号加一。因此我们要保证这个**端口号(2222)和这个端口号加一(2223)**的端口号不被占用。


实际方案

在内网主机B上的操作

使用密钥登录

为了不输入密码,这边选择用密钥登录,下面是密钥登录的方法

在内网主机B创建密钥,使用命令ssh-keygen -t rsa,这个命令会在**~/.ssh/**目录下生成一个公钥,一个私钥。

将公钥的内容,复制到服务器A的**~/.ssh/authorized_keys**内

随后,在内网主机B尝试无密码登录到服务器A(假设A的ip为12.12.12.12)

1
ssh username@12.12.12.12

如果不需要输入密码,直接登录成功,再进行下面的步骤,否则下面的步骤也无法功能


[!Important]

再次提醒:一定要在云服务器上修改sshd_config,将里面的GateWayPort后面的东西为 yes,随后一定要重启ssh服务!

否则会出现Connection refuse

1
2
3
4
5
6
7
8
$ vim /etc/ssh/sshd_config
# 修改GateWayPort后面的东西为yes
# GateWayPort yes
# 注意去除前面的 # 号
# 再保存并退出
$ systemctl restart sshd
# 一些服务器没有systemctl,用下面这个
$ service sshd restart

安装autossh

1
apt install autossh

autossh软件可以自动断开重连

创建自动登录脚本

创建个shell脚本,作用是让内网主机B连接到服务器A,这边使用密钥登录

脚本名:autossh.sh

(此脚本调用了autossh功能,可以改名为其它)

功能:如果已经存在autossh进程,则显示这个进程pid;否则,强制执行登录

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
#!/bin/bash

# SSH连接参数
# 服务器用户
remote_user="remote_ssh"
# 服务器ip
remote_host="150.158.14.44"
# 连接到服务器用户remote_user的端口
remote_port="2222"
# 6666也是在服务器上开的端口,用来登陆到内网主机
local_port="6666"
# 连接到内网主机B的ssh端口
inner_ssh_port="22"

# 尝试ping baidu.com,检查网络连通性
if ping -c 1 baidu.com &> /dev/null
then
echo "Connect to the Internet successfully"
else
echo "Fail to connect to the Internet, sh will be executed"
/opt/anaconda3/bin/python /home/dongli911/autoNet.py
fi

# 启动autossh并使用sshpass自动输入密码
autossh_pid=($(pgrep autossh))
n=${#autossh_pid[@]}
if [ $n -ge 2 ]; then
echo "autossh is running PID: $autossh_pid "
else
echo "autossh is not running, it will run now"
autossh -M 0 -f -N -o "PubkeyAuthentication=yes" -o "PasswordAuthentication=no" -i /home/dongli911/.ssh/dongli911_id_rsa -o "ServerAliveInterval=30" -o "ServerAliveCountMax=3" -R $local_port:localhost:$inner_ssh_port $remote_user@$remote_host -p $remote_port
autossh_pid=($(pgrep autossh))
echo "autossh is running PID: $autossh_pid "
fi

检查autossh是否启用

可以定时执行下面的命令,来检测autossh是否启动

脚本名:check_autossh.sh

功能:如果已经存在autossh进程,则显示这个进程pid;否则,先询问是否需要登录,再判断并执行登录

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#!/bin/bash

autossh_pid=($(pgrep autossh))
n=${#autossh_pid[@]}
if [ $n -ge 2 ]; then
echo "autossh is running PID: $autossh_pid "
else
echo "autossh is not running"
echo "if you wanna run autossh.sh , input 'y' "
read input
if [ "$input" = "y" ]; then
/bin/bash /home/dongli911/autossh.sh
echo "This shell have exacted autossh.sh, autossh should be run now ,Please run this shell again to double check"
else
echo "autossh.sh is not exacted, please run it manully"
fi
fi

开机执行自动连接脚本(推荐)

使用crontab实现开机执行autossh脚本,每三小时检测一次autossh是否启动中

1
2
@reboot /home/autossh.sh
0 */3 * * * /home/autossh.sh 2>&1

ssh连接到内网主机

在任意外网主机使用下面命令即可登录到内网主机B

1
ssh username@12.12.12.12 -p 6688

如果是外网主机A需要登录到内网主机B,还可以使用

1
ssh username@localhost -p 6688

开机执行某个脚本(备用方案)

在 /etc/init.d/ 中创建自己的开机运行脚本

1
2
3
4
# 这里的文件名 mystart 可以修改为任何你喜欢的名称,但是必须放在/etc/init.d/目录中
cd /etc/init.d
sudo vim /etc/init.d/mystart.sh
123

写入需要执行的命令,

1
2
3
4
5
6
7
8
9
10
11
12
13
#!/bin/bash
### BEGIN INIT INFO
# Provides: 111
# Required-Start: $local_fs $network
# Required-Stop: $local_fs
# Default-Start: 2 3 4 5
# Default-Stop: 0 1 6
# Short-Description: self define auto start
# Description: self define auto start
### END INIT INFO
# 上面的部分也必须写上,后面放上你需要开机执行的命令,这里是挂载一个硬盘
sudo bash /home/autossh.sh
123456789101112

修改脚本文件权限(将命令中的mystart.sh替换成实际的脚本文件名称)

1
2
sudo chmod 755 /etc/init.d/mystart.sh
1

加入开机启动(将命令中的mystart.sh替换成实际的脚本文件名称)

1
sudo update-rc.d mystart.sh defaults 90