
没有公网 IP?没关系,一根 SSH 隧道就能把本地服务暴露到公网。更疯狂的是——这个工具是我用 AI 对话「聊」出来的,全程没查文档、没翻 Stack Overflow。
事情是这样的:我在家里的一台树莓派上跑了一个小 Web 服务,想在公司访问它。家里是移动宽带,没有公网 IP,传统方案无非两种:
而我的云服务器上本来就有 SSH 服务在跑。脑子里突然闪过一个念头:
「SSH 不是能做反向隧道吗?用 autossh 保活,不就是一个最轻量的内网穿透方案?」
想法有了,剩下就是把它写出来。但这次我不想手动码代码——我想试试全程用 AI 把这个工具写出来。
我的开发流程大概是这样的:
我:我想写一个 Python 脚本,自动配置 SSH 免密登录 + autossh 反向隧道。
要求:跨平台,支持 Windows/Linux/macOS。
自动检测并安装 autossh,然后用 paramiko 连接服务器,上传公钥,
最后用 autossh -R 把本地 80 端口穿透出去AI 直接给我生成了 setup_ssh.py 的骨架——生成 RSA 密钥对、用 paramiko 连接服务器、把公钥追加到 authorized_keys。逻辑清晰,异常处理也考虑到了认证失败、网络超时等边界情况。
我:再写个 main.py,把 setup_ssh.py 串起来。
启动时先检查 paramiko 装没装,没装自动 pip install。
autossh 也是,Linux 下自动安装,Windows 跳过。
最后交互式询问要不要开反向隧道于是 main.py 出来了。最让我惊艳的是,AI 自己处理了跨平台的差异:
where 检测命令,Linux/macOS 用 whichkill_existing_autossh() 防止端口冲突我:pgrep 找不到进程是因为 pgrep 可能没装,加个降级方案
我:serverAliveInterval 设成多少合适?帮我想想
我:日志写到哪?要不要加个 /var/log/autossh.log?AI 每次都能精准定位到要改的代码位置,我也就越来越信任它,直接从「写代码」变成了「提需求」。
简单来说,就三步路:
你(内网机器) 云服务器(有公网 IP)
────────────── ────────────────────
1. 免密 SSH ──────────────────→ 公钥存好,以后不用输密码
2. autossh -R 8081:localhost:80 → 服务器监听 8081 端口
3. 别人访问 服务器IP:8081 ─────→ 流量转发到你本地的 80 端口这就是 SSH 反向隧道(Reverse SSH Tunnel) 的本质:
-R 8081:localhost:80 的意思是:把远程服务器的 8081 端口,映射到我这台机器的 80 端口对比传统方案:
方案 | 额外服务 | 配置难度 | 安全性 | 自由度 |
|---|---|---|---|---|
frp/ngrok | 需要服务端 | 中 | 一般 | 高 |
花生壳 | 依赖第三方 | 低 | 低 | 受限 |
SSH 反向隧道 | 无(SSH 本来就有) | 低 | 高 | 高 |
# setup_ssh.py 核心逻辑
def setup_ssh_key(hostname, username, password, public_key):
ssh = paramiko.SSHClient()
ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy())
ssh.connect(hostname, username=username, password=password)
commands = [
'mkdir -p ~/.ssh',
'chmod 700 ~/.ssh',
f'echo "{public_key}" >> ~/.ssh/authorized_keys',
'chmod 600 ~/.ssh/authorized_keys',
]
for cmd in commands:
ssh.exec_command(cmd)之前每次都要手动 ssh-copy-id,现在脚本一条龙搞定。paramiko 在这里充当了「自动化 SSH 客户端」的角色,对于需要批量配置多台机器的场景尤其好用。
# main.py 核心——autossh 反向隧道
cmd = (
f'autossh -fN '
f'-o "ServerAliveInterval 60" '
f'-o "ServerAliveCountMax 3" '
f'-R 8081:localhost:80 {USERNAME}@{SERVER_IP}'
)几个关键参数:
-f:后台运行,不占终端-N:不执行远程命令,纯做端口转发-R 8081:localhost:80:这是灵魂,反向端口转发ServerAliveInterval 60:每 60 秒发心跳包,防止被中间路由器掐断def find_command(cmd):
finder = "where" if os.name == "nt" else "which"
...
def install_autossh():
if os.name == "nt":
return True # Windows 下跳过
...在 AI 的帮助下,这些跨平台的边角细节处理得很干净。Windows 用户虽然不能用 autossh(可以用 plink 替代,这是后续优化的方向),但至少脚本不会崩溃。
整个工具从想法到可用,前后花了不到 30 分钟。说几个有意思的观察:
ssh 命令PASSWORD = "..."),AI 不会主动提醒你这是安全隐患。正确的做法是用环境变量或密钥文件GatewayPorts 没开?这类需要「排查系统环境」的问题,AI 只能给通用的排查清单基于这个基础,用 AI 继续迭代的话可以搞这些:
config.yaml,支持多台服务器的配置管理上述每一项,大概率也都是「跟 AI 聊几句」就能出 MVP 的程度。
main.py
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
简化的SSH免密登录配置脚本
直接使用预设的服务器信息
"""
import subprocess
import sys
import os
import platform
# 服务器配置信息
SERVER_IP = "xxxxxxxxxx"
USERNAME = "root"
PASSWORD = "Ai-TEst2@135246"
def find_command(cmd):
"""查找命令是否存在"""
try:
# Windows 用 where,类 Unix 用 which
finder = "where" if os.name == "nt" else "which"
result = subprocess.run(
[finder, cmd],
capture_output=True, text=True
)
return result.returncode == 0
except FileNotFoundError:
return False
def install_autossh():
"""检测并安装autossh(Windows跳过)"""
# Windows 下不需要管理 autossh,直接跳过
if os.name == "nt":
print("检测到Windows环境,跳过autossh检测")
return True
# 已安装则直接返回
if find_command("autossh"):
print("autossh已安装")
return True
print("未检测到autossh,开始自动安装...")
# 各发行版的安装命令
install_cmds = {
"apt-get": ["sudo", "apt-get", "install", "-y", "autossh"],
"yum": ["sudo", "yum", "install", "-y", "autossh"],
"dnf": ["sudo", "dnf", "install", "-y", "autossh"],
"brew": ["brew", "install", "autossh"],
"pacman": ["sudo", "pacman", "-S", "--noconfirm", "autossh"],
}
# 找到第一个可用的包管理器并执行安装
for pm, cmd in install_cmds.items():
if not find_command(pm):
continue
try:
print(f"使用 {pm} 安装autossh...")
subprocess.run(cmd, check=True)
print("autossh安装成功!")
return True
except subprocess.CalledProcessError as e:
print(f"通过 {pm} 安装失败,退出码: {e.returncode}")
return False
except FileNotFoundError:
continue
print("未找到支持的包管理器,请手动安装autossh:")
print(" Debian/Ubuntu: sudo apt-get install autossh")
print(" CentOS/RHEL: sudo yum install autossh")
print(" Fedora: sudo dnf install autossh")
print(" macOS: brew install autossh")
print(" Arch: sudo pacman -S autossh")
return False
def install_paramiko():
"""安装paramiko库"""
try:
import paramiko
return True
except ImportError:
print("正在安装paramiko库...")
try:
subprocess.check_call([sys.executable, "-m", "pip", "install", "paramiko"])
print("paramiko安装成功!")
return True
except subprocess.CalledProcessError:
print("paramiko安装失败,请手动安装:pip install paramiko")
return False
def test_ssh_connection():
"""测试SSH连接"""
try:
import paramiko
print("正在测试SSH连接...")
ssh = paramiko.SSHClient()
ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy())
# 尝试连接
ssh.connect(SERVER_IP, username=USERNAME, password=PASSWORD, timeout=10)
print("SSH连接测试成功!")
# 执行简单命令测试
stdin, stdout, stderr = ssh.exec_command('whoami')
result = stdout.read().decode('utf-8').strip()
print(f"当前用户: {result}")
ssh.close()
return True
except Exception as e:
print(f"SSH连接测试失败: {e}")
print("请检查:")
print("1. 服务器IP地址是否正确")
print("2. 用户名和密码是否正确")
print("3. 服务器SSH服务是否运行")
print("4. 网络连接是否正常")
return False
def kill_existing_autossh():
"""杀掉已有的针对目标服务器的autossh隧道进程"""
if os.name == "nt":
return # Windows跳过
# 查找包含目标IP的autossh进程
try:
result = subprocess.run(
["pgrep", "-af", f"autossh.*{SERVER_IP}"],
capture_output=True, text=True
)
if result.returncode != 0 or not result.stdout.strip():
print("未发现已有的autossh隧道进程")
return
# 提取进程ID并逐个杀掉
pids = []
for line in result.stdout.strip().splitlines():
parts = line.split(None, 1)
if parts:
pids.append(parts[0])
print(f"发现已有autossh隧道进程: {', '.join(pids)}")
for pid in pids:
subprocess.run(["kill", pid], capture_output=True)
print("已杀掉旧的autossh隧道进程")
except FileNotFoundError:
print("未找到pgrep命令,跳过清理旧进程")
def setup_autossh_tunnel():
"""配置autossh反向隧道(Windows跳过)"""
if os.name == "nt":
print("检测到Windows环境,跳过autossh反向隧道配置")
return True
if not find_command("autossh"):
print("未检测到autossh,跳过反向隧道配置")
return False
# 询问用户是否需要配置反向隧道
print("\n=== 配置autossh反向隧道 ===")
print(f"将把本地 80 端口反向转发到服务器 {SERVER_IP}:8081")
try:
answer = input("是否需要配置autossh反向隧道?[y/N]: ").strip().lower()
except (EOFError, KeyboardInterrupt):
print("\n已取消")
return False
if answer not in ("y", "yes"):
print("已跳过autossh反向隧道配置")
return True
# 先清理已有的隧道进程,避免重复
kill_existing_autossh()
# 启动反向隧道
# -f 后台运行 / -N 不执行远程命令 / -R 反向端口转发
cmd = (
f'autossh -fN -o "ServerAliveInterval 60" -o "ServerAliveCountMax 3" '
f'-R 8081:localhost:80 {USERNAME}@{SERVER_IP} '
f'> /var/log/autossh.log 2>&1'
)
print(f"执行命令: {cmd}")
try:
subprocess.run(cmd, shell=True, check=True)
except subprocess.CalledProcessError as e:
print(f"autossh隧道启动失败,退出码: {e.returncode}")
print("可查看日志: /var/log/autossh.log")
return False
# 验证是否启动成功
try:
result = subprocess.run(
["pgrep", "-af", f"autossh.*{SERVER_IP}"],
capture_output=True, text=True
)
if result.returncode == 0 and result.stdout.strip():
print("autossh反向隧道启动成功!")
print(f" 本地 80 端口 -> 服务器 {SERVER_IP}:8081")
return True
else:
print("autossh隧道进程未检测到,可能启动失败")
print("可查看日志: /var/log/autossh.log")
return False
except FileNotFoundError:
print("无法验证(缺少pgrep),请手动检查进程")
return True
def main():
print("=== SSH免密登录快速配置 ===")
print(f"目标服务器: {SERVER_IP}")
print(f"用户名: {USERNAME}")
print(f"密码: {PASSWORD[:8]}...") # 只显示前8位
print()
# 第一步:检测并安装autossh(Windows跳过)
if not install_autossh():
print("\nautossh未就绪,请先安装后重新运行脚本")
return
# 检查并安装paramiko
if not install_paramiko():
return
# 先测试SSH连接
if not test_ssh_connection():
print("\n请先解决SSH连接问题,然后重新运行脚本")
return
# 运行主配置脚本
script_path = os.path.join(os.path.dirname(__file__), "setup_ssh.py")
cmd = [
sys.executable,
script_path,
"--host", SERVER_IP,
"--user", USERNAME,
"--password", PASSWORD
]
try:
subprocess.run(cmd, check=True)
except subprocess.CalledProcessError as e:
print(f"配置失败,退出码: {e.returncode}")
return
except KeyboardInterrupt:
print("\n用户取消操作")
return
# 最后一步:配置autossh反向隧道(Windows跳过)
setup_autossh_tunnel()
if __name__ == '__main__':
main()setup_ssh.py
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
SSH免密登录自动配置脚本
使用paramiko库实现自动化SSH连接和密钥配置
"""
import os
import sys
import subprocess
import argparse
from pathlib import Path
try:
import paramiko
except ImportError:
print("错误:需要安装paramiko库")
print("请运行:pip install paramiko")
sys.exit(1)
def generate_ssh_key(key_path):
"""生成SSH密钥对"""
if os.path.exists(key_path):
print(f"SSH密钥已存在: {key_path}")
return True
try:
print("正在生成SSH密钥对...")
cmd = [
'ssh-keygen',
'-t', 'rsa',
'-b', '4096',
'-f', key_path,
'-N', ''
]
result = subprocess.run(cmd, capture_output=True, text=True)
if result.returncode == 0:
print("SSH密钥对生成成功!")
return True
else:
print(f"SSH密钥生成失败: {result.stderr}")
return False
except FileNotFoundError:
print("错误:未找到ssh-keygen命令,请确保已安装OpenSSH")
return False
def read_public_key(pub_key_path):
"""读取公钥内容"""
try:
with open(pub_key_path, 'r', encoding='utf-8') as f:
return f.read().strip()
except FileNotFoundError:
print(f"错误:公钥文件不存在: {pub_key_path}")
return None
def setup_ssh_key(hostname, username, password, public_key):
"""通过SSH连接设置公钥"""
try:
print(f"正在连接到 {hostname}...")
# 创建SSH客户端
ssh = paramiko.SSHClient()
ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy())
# 连接到服务器
ssh.connect(hostname, username=username, password=password, timeout=10)
print("SSH连接成功!")
# 执行命令设置SSH密钥
commands = [
'mkdir -p ~/.ssh',
'chmod 700 ~/.ssh',
f'echo "{public_key}" >> ~/.ssh/authorized_keys',
'chmod 600 ~/.ssh/authorized_keys',
'sort ~/.ssh/authorized_keys | uniq > ~/.ssh/authorized_keys.tmp',
'mv ~/.ssh/authorized_keys.tmp ~/.ssh/authorized_keys'
]
for cmd in commands:
print(f"执行命令: {cmd}")
stdin, stdout, stderr = ssh.exec_command(cmd)
# 等待命令执行完成
exit_status = stdout.channel.recv_exit_status()
if exit_status != 0:
error_msg = stderr.read().decode('utf-8')
print(f"命令执行失败: {error_msg}")
return False
print("SSH密钥配置完成!")
ssh.close()
return True
except paramiko.AuthenticationException:
print("错误:认证失败,请检查用户名和密码")
return False
except paramiko.SSHException as e:
print(f"SSH连接错误: {e}")
return False
except Exception as e:
print(f"未知错误: {e}")
return False
def test_passwordless_login(hostname, username, private_key_path):
"""测试免密登录"""
try:
print("正在测试免密登录...")
ssh = paramiko.SSHClient()
ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy())
# 使用私钥连接
private_key = paramiko.RSAKey.from_private_key_file(private_key_path)
ssh.connect(hostname, username=username, pkey=private_key, timeout=10)
# 执行测试命令
stdin, stdout, stderr = ssh.exec_command('echo "SSH免密登录测试成功!"')
result = stdout.read().decode('utf-8').strip()
print(result)
ssh.close()
return True
except Exception as e:
print(f"免密登录测试失败: {e}")
return False
def main():
parser = argparse.ArgumentParser(description='SSH免密登录自动配置脚本')
parser.add_argument('--host', required=True, help='服务器IP地址')
parser.add_argument('--user', required=True, help='用户名')
parser.add_argument('--password', required=True, help='密码')
parser.add_argument('--key-path', help='SSH密钥路径(可选)')
args = parser.parse_args()
# 设置SSH密钥路径
if args.key_path:
ssh_dir = Path(args.key_path).parent
private_key_path = args.key_path
public_key_path = f"{args.key_path}.pub"
else:
ssh_dir = Path.home() / '.ssh'
private_key_path = ssh_dir / 'id_rsa'
public_key_path = ssh_dir / 'id_rsa.pub'
# 创建SSH目录
ssh_dir.mkdir(mode=0o700, exist_ok=True)
print("=== SSH免密登录自动配置脚本 ===")
print(f"服务器: {args.host}")
print(f"用户名: {args.user}")
print(f"SSH密钥路径: {private_key_path}")
print()
# 步骤1:生成SSH密钥对
if not generate_ssh_key(str(private_key_path)):
sys.exit(1)
# 步骤2:读取公钥
public_key = read_public_key(str(public_key_path))
if not public_key:
sys.exit(1)
print(f"公钥内容: {public_key[:50]}...")
# 步骤3:配置SSH密钥
if not setup_ssh_key(args.host, args.user, args.password, public_key):
sys.exit(1)
# 步骤4:测试免密登录
if test_passwordless_login(args.host, args.user, str(private_key_path)):
print()
print("✅ SSH免密登录配置成功!")
print(f"现在可以使用以下命令免密登录:")
print(f"ssh {args.user}@{args.host}")
else:
print()
print("❌ 免密登录测试失败,请检查配置")
print("您可以尝试手动测试:")
print(f"ssh {args.user}@{args.host}")
if __name__ == '__main__':
main()这个项目让我意识到一个变化:「写代码」这件事本身正在加速贬值,但「知道要写什么」的能力反而更值钱了。
AI 可以帮你写出几乎没有任何语法错误的代码,但它不会告诉你「SSH 反向隧道是解决你问题的最优解」。你仍然需要理解网络原理、了解各种方案的 trade-off、知道什么时候该放弃优雅方案直接用 bash 脚本硬上——这些才是工程师真正的护城河。
工具永远只是工具。能驾驭工具的,永远是人。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。