你想要实现的是通过 SSH 隧道,让公网服务器 abc.com 能够访问你本机上的 1001-1005 端口,在服务器上创建仅能密钥认证、无法执行命令的 tunnel_user 用户,并编写可注册为系统服务的脚本完成隧道配置。
1. 首先在 abc.com 服务器上执行以下操作:
# 1. 创建普通用户 tunnel_user(无登录shell,无家目录)
sudo useradd -m -s /usr/sbin/nologin tunnel_user
#已经创建
#sudo usermod -s /usr/sbin/nologin tunnel_user
#改为正常登录
#sudo usermod -s /bin/bash tunnel_user
# 2. 创建 .ssh 目录并设置权限
sudo mkdir -p /home/tunnel_user/.ssh
sudo chown tunnel_user:tunnel_user /home/tunnel_user/.ssh
sudo chmod 700 /home/tunnel_user/.ssh
# 3. 生成密钥对(本地机器执行,生成后将公钥上传到服务器)
# 本地机器执行此命令(不要设置密码)
ssh-keygen -t ed25519 -f ~/.ssh/tunnel_key -N ""
# 4. 将本地公钥复制到服务器 tunnel_user 的 authorized_keys
# 本地执行(替换为你的服务器地址)
cat ~/.ssh/tunnel_key.pub | ssh root@abc.com "sudo tee -a /home/tunnel_user/.ssh/authorized_keys"
# 5. 设置 authorized_keys 权限(服务器执行)
sudo chown tunnel_user:tunnel_user /home/tunnel_user/.ssh/authorized_keys
sudo chmod 600 /home/tunnel_user/.ssh/authorized_keys
# 6. 限制 tunnel_user 仅能做端口转发,禁止执行命令
# 编辑服务器的 authorized_keys 文件,在公钥前添加限制
sudo vi /home/tunnel_user/.ssh/authorized_keys
# 在公钥开头添加:no-pty,no-agent-forwarding,no-X11-forwarding,command="echo 'This account is for port forwarding only'"
# 最终格式示例:
# no-pty,no-agent-forwarding,no-X11-forwarding,command="echo 'This account is for port forwarding only'" ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIxxxxxxx tunnel_key
# 7. 重启 sshd 服务
sudo systemctl restart sshd
# 重新加载
sudo systemctl reload ssh
2. 关键配置说明
no-pty:禁止分配伪终端,无法执行交互式命令 no-agent-forwarding:禁止代理转发,提升安全性 no-X11-forwarding:禁止 X11 转发 command="...":强制执行指定命令,覆盖用户的任何命令输入
3. 可选配置 编辑SSH配置
sudo tee -a /etc/ssh/sshd_config << 'EOF'
# 仅允许tunnel_user使用密钥登录,并进行端口转发
Match User tunnel_user
# 认证方式
AuthenticationMethods publickey
PubkeyAuthentication yes
PasswordAuthentication no
ChallengeResponseAuthentication no
KbdInteractiveAuthentication no
# 端口转发设置
AllowTcpForwarding yes
# 允许转发到任何地址
PermitOpen any
# 允许绑定到0.0.0.0(远程转发)
GatewayPorts yes
# 安全限制
# 禁止分配TTY(不能执行命令)
PermitTTY no
# 禁用X11转发
X11Forwarding no
# 禁用代理转发
AllowAgentForwarding no
# 禁用VPN式隧道
PermitTunnel no
# 禁止执行~/.ssh/rc文件
PermitUserRC no
# 会话限制(可选)
# 服务器每300秒向客户端发心跳
# ClientAliveInterval 300
# 10次心跳失败则断开
# ClientAliveCountMax 2
# 增加会话数限制
MaxSessions 10
EOF
# 3. 重新加载SSH(不重启)
sudo systemctl reload ssh
4. 客户端脚本
#!/bin/bash
# SSH隧道服务脚本(自定义端口映射版 + 测试命令)
# 配置项
TUNNEL_USER="tunnel_user"
SERVER="abc.com"
SERVER_SSH_PORT="22"
LOCAL_IP="127.0.0.1" # 本地回环地址
# 核心:自定义端口映射表(格式:本地端口:服务器端口),可自由增删修改
PORT_MAPPINGS=(
"1001:3301" # 本地1001 → 服务器3301
"1002:3302" # 本地1002 → 服务器3302
"1003:3303" # 本地1003 → 服务器3303
"1004:3304" # 本地1004 → 服务器3304
"1005:3305" # 本地1005 → 服务器3305
# 可添加更多映射,例如 "1006:3306"
)
KEY_FILE="$HOME/.ssh/tunnel_key"
LOG_FILE="/var/log/ssh_tunnel.log"
PID_FILE="/var/run/ssh_tunnel.pid"
# 日志函数
log() {
echo "[$(date '+%Y-%m-%d %H:%M:%S')] $1" >> $LOG_FILE
}
# 检查密钥文件
check_key() {
if [ ! -f "$KEY_FILE" ]; then
log "错误:密钥文件 $KEY_FILE 不存在"
echo "错误:密钥文件 $KEY_FILE 不存在"
exit 1
fi
}
# 验证端口映射格式并构建转发参数
build_forward_args() {
local args=""
for mapping in "${PORT_MAPPINGS[@]}"; do
# 拆分本地端口和服务器端口
IFS=':' read -r local_port remote_port <<< "$mapping"
# 验证端口格式(数字且1-65535)
if ! [[ "$local_port" =~ ^[0-9]+$ && "$remote_port" =~ ^[0-9]+$ &&
"$local_port" -ge 1 && "$local_port" -le 65535 &&
"$remote_port" -ge 1 && "$remote_port" -le 65535 ]]; then
log "错误:端口映射 $mapping 格式无效,必须是 数字:数字(1-65535)"
echo "错误:端口映射 $mapping 格式无效!"
exit 1
fi
# 构建-R参数(远程端口转发:服务器端口 → 本地端口)
args+=" -R $remote_port:$LOCAL_IP:$local_port"
done
echo "$args"
}
# 构建完整的测试命令(test功能核心)
build_full_command() {
check_key
local forward_args=$(build_forward_args)
# 拼接完整的SSH命令(去掉后台运行&,方便手动测试)
local full_cmd="ssh -i \"$KEY_FILE\" \
-o ServerAliveInterval=30 \
-o ServerAliveCountMax=3 \
-o StrictHostKeyChecking=no \
-o PasswordAuthentication=no \
-N $forward_args \
-p $SERVER_SSH_PORT \
$TUNNEL_USER@$SERVER"
echo "===== 完整的SSH隧道执行命令 ====="
echo $full_cmd
echo -e "\
===== 端口映射清单 ====="
for mapping in "${PORT_MAPPINGS[@]}"; do
IFS=':' read -r local_port remote_port <<< "$mapping"
echo " 服务器端口 $remote_port → 本地 $LOCAL_IP:$local_port"
done
echo -e "\
===== 服务器端端口检测命令 ====="
echo "# 登录服务器后执行,检查端口是否监听:"
for mapping in "${PORT_MAPPINGS[@]}"; do
IFS=':' read -r local_port remote_port <<< "$mapping"
echo "ss -tulpn | grep $remote_port"
done
echo -e "\
===== 本地端进程检测命令 ====="
echo "# 本地执行,检查隧道进程是否运行:"
echo "ps aux | grep ssh | grep $TUNNEL_USER"
}
# 启动隧道
start() {
if [ -f "$PID_FILE" ]; then
local pid=$(cat "$PID_FILE")
if ps -p $pid > /dev/null; then
log "隧道已在运行(PID: $pid)"
echo "隧道已在运行(PID: $pid)"
return 0
else
rm -f "$PID_FILE"
log "清理无效的PID文件"
fi
fi
check_key
local forward_args=$(build_forward_args)
log "启动SSH隧道,转发参数:$forward_args"
echo "启动SSH隧道..."
# 启动隧道(后台运行,禁用密码认证,设置心跳保持连接)
ssh -i "$KEY_FILE" \
-o ServerAliveInterval=30 \
-o ServerAliveCountMax=3 \
-o StrictHostKeyChecking=no \
-o PasswordAuthentication=no \
-N $forward_args \
-p $SERVER_SSH_PORT \
$TUNNEL_USER@$SERVER \
> $LOG_FILE 2>&1 &
local pid=$!
echo $pid > $PID_FILE
# 延迟1秒检测进程是否真的启动
sleep 1
if ps -p $pid > /dev/null; then
log "隧道启动成功,PID: $pid"
echo "隧道启动成功,PID: $pid"
else
rm -f $PID_FILE
log "隧道启动失败!请执行 $0 test 查看完整命令手动测试"
echo "错误:隧道启动失败!请执行 $0 test 查看完整命令手动测试"
fi
}
# 停止隧道
stop() {
if [ -f "$PID_FILE" ]; then
local pid=$(cat "$PID_FILE")
if ps -p $pid > /dev/null; then
log "停止隧道(PID: $pid)"
echo "停止隧道(PID: $pid)..."
kill $pid
sleep 2
# 确保进程终止
if ps -p $pid > /dev/null; then
kill -9 $pid
log "强制终止隧道进程(PID: $pid)"
fi
rm -f "$PID_FILE"
log "隧道已停止"
echo "隧道已停止"
else
rm -f "$PID_FILE"
log "PID文件存在,但进程未运行,清理PID文件"
echo "隧道未运行"
fi
else
log "PID文件不存在,隧道未运行"
echo "隧道未运行"
fi
}
# 重启隧道
restart() {
stop
sleep 2
start
}
# 查看状态(显示完整端口映射+端口检测)
status() {
echo "===== 隧道状态 ====="
if [ -f "$PID_FILE" ]; then
local pid=$(cat "$PID_FILE")
if ps -p $pid > /dev/null; then
log "查看状态:隧道运行中(PID: $pid)"
echo "✅ 隧道运行中(PID: $pid)"
echo -e "\
===== 端口映射 ====="
for mapping in "${PORT_MAPPINGS[@]}"; do
IFS=':' read -r local_port remote_port <<< "$mapping"
echo " 服务器端口 $remote_port → 本地 $LOCAL_IP:$local_port"
done
echo -e "\
===== 建议 ====="
echo "请在服务器执行以下命令检测端口是否监听:"
for mapping in "${PORT_MAPPINGS[@]}"; do
IFS=':' read -r local_port remote_port <<< "$mapping"
echo " ss -tulpn | grep $remote_port"
done
else
log "查看状态:PID文件存在,但进程未运行"
echo "❌ 隧道已停止(无效PID)"
fi
else
log "查看状态:隧道未运行"
echo "❌ 隧道未运行"
fi
}
# 注册为系统服务
install_service() {
# 获取脚本绝对路径(避免路径问题)
SCRIPT_ABS_PATH=$(realpath "$0")
# 创建systemd服务文件
local service_file="/etc/systemd/system/ssh-tunnel.service"
sudo cat > $service_file << EOF
[Unit]
Description=SSH Tunnel Service (Custom Port Mapping)
After=network.target
[Service]
Type=forking
User=$USER
Group=$USER
ExecStart=$SCRIPT_ABS_PATH start
ExecStop=$SCRIPT_ABS_PATH stop
ExecRestart=$SCRIPT_ABS_PATH restart
PIDFile=$PID_FILE
Restart=always
RestartSec=10
StandardOutput=append:$LOG_FILE
StandardError=append:$LOG_FILE
[Install]
WantedBy=multi-user.target
EOF
# 确保日志文件存在且权限正确
sudo touch $LOG_FILE
sudo chown $USER:$USER $LOG_FILE
# 重新加载systemd配置
sudo systemctl daemon-reload
# 设置开机自启
sudo systemctl enable ssh-tunnel
log "系统服务已注册,服务名:ssh-tunnel"
echo "✅ 系统服务已注册成功!"
echo "常用命令:"
echo " 启动服务:sudo systemctl start ssh-tunnel"
echo " 停止服务:sudo systemctl stop ssh-tunnel"
echo " 查看状态:sudo systemctl status ssh-tunnel"
echo " 查看日志:tail -f $LOG_FILE"
}
# 卸载系统服务
uninstall_service() {
sudo systemctl stop ssh-tunnel
sudo systemctl disable ssh-tunnel
sudo rm -f /etc/systemd/system/ssh-tunnel.service
sudo systemctl daemon-reload
log "系统服务已卸载"
echo "✅ 系统服务已卸载成功!"
}
# 主逻辑
case "$1" in
start)
start
;;
stop)
stop
;;
restart)
restart
;;
status)
status
;;
test)
build_full_command
;;
install)
install_service
;;
uninstall)
uninstall_service
;;
*)
echo "用法:$0 {start|stop|restart|status|test|install|uninstall}"
exit 1
;;
esac
exit 0
5. 用法
# 1. 将脚本保存为 ssh_tunnel_service.sh
# 2. 添加执行权限
chmod +x ssh_tunnel_service.sh
# 3. 测试启动隧道(先手动测试)
./ssh_tunnel_service.sh start
# 4. 查看隧道状态
./ssh_tunnel_service.sh status
# 5. 注册为系统服务(永久生效)
./ssh_tunnel_service.sh install
# 6. 启动系统服务
sudo systemctl start ssh-tunnel
# 7. 设置开机自启(install命令已包含,可验证)
sudo systemctl enable ssh-tunnel
# 8. 测试
./ssh_tunnel_service.sh test

评论