使用ssh隧道,让公网服务器访问本地服务

本文介绍如何通过SSH隧道实现公网服务器访问本地端口。步骤包括:创建仅用于端口转发的tunnel_user用户,限制其权限(禁止执行命令、分配TTY等);生成密钥对并配置authorized_keys添加限制选项;可选在sshd_config中增加安全配置;提供完整的bash脚本,支持自定义端口映射、启动/停止/状态查询,并可注册为systemd系统服务,实现开机自启。

作者:zhuge···预计阅读 33 分钟·250 阅读·0 评论
使用ssh隧道,让公网服务器访问本地服务

你想要实现的是通过 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

相关文章

评论

加载中...