我最早碰到这个场景,是接手一个新项目:代码只能在 Linux 虚拟机上编译,可我日常工作机是 Windows,每次改完代码都得手动同步进虚拟机再编译,来回折腾。后来换了 VSCode 里用 SSH 连 VMware 虚拟机的方式,把所有编辑、编译、调试都统一到一个窗口里,整个开发节奏一下子就顺了。这篇指南把 VSCode、SSH、VMware、虚拟机这套组合从网络配置到免密登录再到故障排查的完整链路写一遍,照着做就能把本地编辑器和远程 Linux 环境无缝串起来,适合刚接触虚拟机开发、想在宿主机上直接写代码编译调试的朋友。
1. 开发环境选型:为什么把编辑器迁到宿主机、让代码留在虚拟机里
1.1 先对比几种常见的做法
很多人最开始会在虚拟机里直接装 VSCode,界面卡顿不说,复制粘贴、文件拖拽都很别扭,虚拟机分辨率也经常和宿主机桌面打架。还有一种做法是宿主机编辑代码,通过 VMware 共享文件夹同步到虚拟机,再在虚拟机里编译。这个方案听起来方便,但 Windows 的 NTFS 文件系统以共享方式挂载到 Linux 后,文件权限经常乱掉,chmod 甚至不生效,很多 Linux 工具链会遇到诡异问题,跨文件系统的文件同步也容易产生缓存不一致。
我最终的方案是:VSCode 跑在宿主机上,通过 SSH 协议连进虚拟机,VSCode 会在虚拟机的用户目录下启动一个 VS Code Server 服务,本地窗口展示的目录树、代码内容、终端、调试面板,实际读写操作都在虚拟机内部完成。这就是常说的 Remote-SSH。
三种做法对比下来,表格里看得很清楚:
| 方案 | 编辑器位置 | 代码存储 | 编译运行 | 主要问题 |
|---|---|---|---|---|
| 虚拟机内装 VSCode | 虚拟机 | 虚拟机磁盘 | 虚拟机 | 操作卡顿、跨系统复制不便 |
| 共享文件夹 | 宿主机 | 宿主磁盘 | 虚拟机 | 文件权限损坏、inotify 失效 |
| VSCode Remote-SSH | 宿主机 | 虚拟机磁盘 | 虚拟机 | 初次配置略繁琐,稳定后体验接近本地 |
1.2 Remote-SSH 的核心原理一句话讲清
很多人误以为 Remote-SSH 就是像 SFTP 一样把文件下载到本地再编辑、保存时上传,其实不是。VSCode 客户端只负责画界面和接收输入,所有文件索引、语言服务、插件执行、终端进程都发生在虚拟机里的 VS Code Server 进程中。你打开一个搜索,实际上是在远端跑搜索;你配置一个 C/C++ 插件的 IntelliSense,索引和编译分析也在远端。
这也是为什么它比 SFTP 方式流畅得多——不需要反复传输文件,网络只传输 UI 渲染指令和文本数据,带宽占用很低。理解了这一点,你就明白为什么远端插件要单独安装,为什么有时本地插件在远程窗口里"消失"了。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. VMware 网络模型选型:NAT 和桥接怎么选,哪些配置会导致连不上
2.1 三种网络模式一句话总结
VMware 虚拟机的网络配置是整个 SSH 通路的地基。VMware 提供三种常见模式:
- NAT 模式:虚拟机和宿主机共用一个私有网段,虚拟机通过宿主机共享 IP 上网,宿主机能主动访问虚拟机,但外部设备无法直接访问。
- 桥接模式:虚拟机变成局域网里的一台独立设备,有自己的局域网 IP,其他电脑也能直接访问,前提是你的 Wi-Fi 或交换机允许设备互访。
- Host-Only 模式:虚拟机只能和宿主机通信,无法访问外网,适合纯隔离测试。
日常开发场景,我建议直接用 NAT。原因很实际:NAT 模式下虚拟机的 IP 和宿主机之间天然连通,你不需要关心交换机、路由器、AP 隔离这些乱七八糟的因素,只要宿主机不开防火墙限制,SSH 基本一次通。桥接模式看着灵活,但如果你在公司或宿舍的公共网络环境下,对方开个 AP 隔离,桥接虚拟机连局域网都进不去,更别说被宿主机连上了。
2.2 打开虚拟网络编辑器确认网段
在 VMware Workstation 菜单栏点击"编辑 > 虚拟网络编辑器",会看到 VMnet8(NAT 模式)对应的子网地址,常见的是 192.168.x.0 这样的网段。后面写 VSCode 的 SSH 配置时,需要知道虚拟机的具体 IP,这张表就是用来对位查的。
另外,Ubuntu 虚拟机安装完如果右上角网络图标出现问号,大多数情况是网络适配器没有正确连接。先到"虚拟机设置 > 硬件 > 网络适配器"里确认勾选了"已连接"和"启动时连接",再到虚拟机里执行 ip a 看网卡有没有拿到地址。
2.3 用静态 IP 给虚拟机固定地址
NAT 模式下虚拟机默认走 DHCP,最头疼的问题就是重启后 IP 变了,VSCode 配置文件里写的地址失效。所以我在配好环境后第一件事就是改成静态 IP。Ubuntu 新版本用 netplan 管理网络,配置文件在 /etc/netplan/ 下,文件名可能不同,打开后改成类似这样:
yaml复制network:
version: 2
ethernets:
ens33:
dhcp4: false
addresses: [192.168.20.100/24]
routes:
- to: default
via: 192.168.20.2
nameservers:
addresses: [192.168.20.2, 223.5.5.5]
注意网卡名 ens33 只是 Ubuntu 常见的例子,你机器上可能是 ens160 或 eno1,以自己的 ip a 输出为准。网关地址 192.168.20.2 是 VMware NAT 的默认网关,在虚拟网络编辑器里能看到。改完后执行 sudo netplan apply 使配置生效。这一步做完,虚拟机的 IP 就固定了,后续 SSH 配置也不用反复改。
3. 虚拟机内 OpenSSH 服务端准备:从安装到锁定登录用户
3.1 安装并启动 sshd
Ubuntu/Debian 系虚拟机通常自带 OpenSSH 客户端,但服务端不一定装了。先在虚拟机终端里执行:
bash复制sudo apt update
sudo apt install -y openssh-server
sudo systemctl enable --now ssh
安装完确认服务在监听:
bash复制ss -tlnp | grep 22
看到 sshd 监听在 0.0.0.0:22 就没问题。如果只有 127.0.0.1:22,说明 sshd 只监听了回环地址,宿主机连不上,需要检查 /etc/ssh/sshd_config 里的 ListenAddress 配置。
3.2 防火墙和 SELinux 的隐形拦截
很多初学者在这步栽跟头:sshd 明明起来了,本地 localhost 能连,宿主机却超时。这通常是防火墙在作怪。Ubuntu 默认没开 ufw,但如果你开启过,要放行 22 端口:
bash复制sudo ufw allow 22/tcp
CentOS/RHEL 系虚拟机还要注意 firewalld 和 SELinux。检查 firewalld 状态:
bash复制systemctl status firewalld
运行中的话执行 sudo firewall-cmd --permanent --add-service=ssh && sudo firewall-cmd --reload。SELinux 默认不会拦 SSH,但如果你手动改过布尔值也可以查一下:
bash复制getsebool -a | grep ssh
大多数家用环境不会动 SELinux,这一步可以快速跳过,但要心里有数,万一后面怎么都连不上,回来查这一层。
3.3 多用户环境下的最小化授权
虚拟机如果只有你自己用,dev 用户直接一把梭就行。但很多情况下虚拟机会分给几个人用,或者创建过一些测试账号,这时候要防止别人通过 SSH 登录后在里面乱逛、改文件。
我的做法是创建一个专用开发用户,然后在 sshd 配置里显式声明谁能登录:
bash复制sudo adduser dev
sudo usermod -aG sudo dev
编辑 /etc/ssh/sshd_config,找到 AllowUsers 这行(没有就自己加):
code复制AllowUsers dev
重启 sshd 后,除了 dev 这个用户,其他任何本地用户即使知道密码也无法通过 SSH 登录,自然也没机会通过远程通道修改你的文件。adduser 创建的 dev 用户默认家目录权限是 755,其他用户无法进入,你项目文件放在家目录里,别人通过 SSH 也进不来。
3.4 主机密钥和首次指纹确认
SSH 服务端启动时会生成一组主机密钥,放在 /etc/ssh/ssh_host_*,相当于虚拟机的身份证。你第一次从宿主机连接时,OpenSSH 客户端会提示确认远程主机指纹,避免连到冒充的机器。很多人会随手输 yes 跳过,我建议你花两秒对一下提示里的指纹和虚拟机里执行下面命令的结果是否一致:
bash复制ssh-keygen -lf /etc/ssh/ssh_host_ed25519_key.pub
这一步弄明白,后面遇到 REMOTE HOST IDENTIFICATION HAS CHANGED 的报错就不会慌了。
4. VSCode Remote-SSH 首次连接与配置落盘:从 config 到远程扩展
4.1 安装扩展并打开远程窗口
VSCode 里安装两个扩展:Remote - SSH 和 Remote - SSH: Editing Configuration Files。前者是主功能,后者让你在编辑器里直接编辑 SSH 配置文件,比手动去用户目录找文件方便。
装完后按 Ctrl+Shift+P,输入 Remote-SSH: Connect to Host,选"Configure SSH Hosts",VSCode 会打开 ~/.ssh/config(Windows 下是 C:\Users\你的用户名\.ssh\config)。把虚拟机信息填进去:
code复制Host vm-dev
HostName 192.168.20.100
User dev
Port 22
IdentityFile ~/.ssh/id_ed25519
ServerAliveInterval 60
ServerAliveCountMax 3
Host 是给这个连接起的别名,后面每次连接只看这个名字就行,不用记 IP。HostName 填虚拟机 IP,User 填刚才创建的开发用户。ServerAliveInterval 60 表示每 60 秒发一次心跳,防止长时间不操作后 SSH 连接被网络设备回收。
4.2 首次连接与 VS Code Server 下载
配置好之后,在 Remote-SSH: Connect to Host 列表里选 vm-dev,第一次连接会提示输入密码,然后在后台下载 VS Code Server 到虚拟机的 ~/.vscode-server 目录。这一步需要虚拟机能够访问外网,如果虚拟机网络受限,连接会卡在 Setting up SSH Host 很久然后报错。
我的建议是:网络受限的虚拟机,提前从 VSCode 官网找到对应版本的 vscode-server-linux-x64.tar.gz 下载到宿主机,用 scp 传到虚拟机,解压到 ~/.vscode-server/bin/对应commit目录。操作顺序在 VSCode 的输出日志里能看到它期望的 commit 号,按目录名对上即可。这步是纯离线环境下的常规补丁方案,实际用起来非常省事。
连接成功后,VSCode 左下角会变成绿色,显示 SSH: vm-dev,说明当前窗口已经切到远程环境。这时在扩展面板里搜到的插件,都默认安装到本地,需要找到"在 SSH: vm-dev 中安装"的选项,把 C/C++、Python、CMake 这些编译调试相关插件装到远端。装错位置的典型现象是:远程窗口里看不到插件,或者代码没有智能提示。
4.3 编译调试配置的一个最小示例
连接通了以后,最好马上验证一个编译调试闭环,避免后面边写边调试才暴露问题。以 C++ 项目为例,在项目根目录建 .vscode/tasks.json:
json复制{
"version": "2.0.0",
"tasks": [
{
"label": "build",
"type": "shell",
"command": "g++",
"args": ["-g", "main.cpp", "-o", "main"],
"group": "build",
"problemMatcher": ["$gcc"]
}
]
}
launch.json 里把可执行文件路径指到虚拟机上的真实路径,比如 /home/dev/projects/main。这样在宿主机窗口里按 F5,实际跑起来的是虚拟机的编译和调试进程。我觉得调试器能真正 attach 到远端进程,是这套方案最有价值的地方——本地 UI 操作,远端真实执行,两边感觉像在一个机器上。
5. 密钥免密登录:公钥部署、私钥权限与 ssh-agent 的一整套细节
5.1 为什么要换成密钥认证
密码登录虽然能用,但每次连接都要输密码,VSCode 在重连、插件同步时还会多次触发,体验很割裂。密钥认证的原理是:本地生成一对公私钥,公钥放到虚拟机,连接时客户端用自己的私钥签名,服务端用公钥验证。整个过程私钥不出本地,安全性比密码高得多。
生成密钥:
bash复制ssh-keygen -t ed25519 -C "dev@workstation" -f ~/.ssh/id_ed25519
ed25519 是目前推荐的非对称加密算法,比传统的 RSA 更安全、密钥更短。-C 只是加个注释方便辨认,-f 指定生成路径,Windows 下默认是 C:\Users\你的用户名\.ssh\id_ed25519。
5.2 部署公钥到虚拟机
最简单的方式是用 ssh-copy-id:
bash复制ssh-copy-id -i ~/.ssh/id_ed25519.pub dev@192.168.20.100
它会提示你输入一次密码,然后把公钥追加到虚拟机 dev 用户家目录下的 ~/.ssh/authorized_keys。如果虚拟机没装 ssh-copy-id,手动操作也很快:
bash复制cat ~/.ssh/id_ed25519.pub | ssh dev@192.168.20.100 "mkdir -p ~/.ssh && chmod 700 ~/.ssh && cat >> ~/.ssh/authorized_keys && chmod 600 ~/.ssh/authorized_keys"
5.3 服务器侧权限:OpenSSH 的严格模式
公钥部署完,连接时还是提示密码或直接拒绝,八成是权限问题。OpenSSH 默认开了 StrictModes,它要求 ~/.ssh 目录权限不能大于 700,authorized_keys 不能大于 600,且这两个文件属主必须是你当前登录用户。很多人习惯把家目录设成 777 或者把 authorized_keys 复制过去顺手 chmod 644,都会导致密钥认证被静默拒绝。
记住这三条铁律:
~/.ssh目录权限 700~/.ssh/authorized_keys文件权限 600- 文件属主必须是当前用户
5.4 本地侧 Windows 权限检查
Windows 下的 OpenSSH 客户端同样有权限检查,但很多人在这一步踩坑。如果私钥文件权限"太开放"(比如默认继承了 Users 组的读权限),ssh 会直接报:
code复制Permissions for 'id_ed25519' are too open.
解决办法是在 PowerShell 里执行:
powershell复制icacls "$env:USERPROFILE\.ssh\id_ed25519" /inheritance:r
icacls "$env:USERPROFILE\.ssh\id_ed25519" /grant:r "$env:USERNAME:R"
第一行去掉所有继承权限,第二行只给你当前用户保留读权限。这几行命令是我在 Windows 下配 SSH 时最高频使用的工具,比右键改安全属性快得多。
5.5 给私钥加密时搭配 ssh-agent 使用
如果你担心私钥泄露,生成密钥时可以设置 passphrase(口令)。代价是每次连接要输入一次口令,VSCode 重连会频繁触发,体验会打折扣。解决办法是把私钥交给 ssh-agent 托管:
bash复制ssh-add ~/.ssh/id_ed25519
输入一次口令后,agent 会在内存里保留解密后的密钥,后续连接都不需要再输。Windows 的 OpenSSH 服务还支持开机自动把私钥加进 agent,可以在服务配置里把 ssh-agent 服务设为自动启动。这样配完之后,VSCode 远程连接基本就是无感重连。
6. 连接失败的排查链路与日常开发优化
6.1 场景一:connect to host port 22: Connection timed out
这个报错说明网络根本不通。排查链路从上往下走:
- 看虚拟机能不能 ping 通宿主机,反过来再 ping 一次,确认虚拟机 IP 有没有误记。
- 在虚拟机里跑
ip a和ip route,确认网卡有 IP,默认路由指向 NAT 网段的网关。 - 在宿主机跑
Test-NetConnection 192.168.20.100 -Port 22,看端口通不通。如果 ping 通但端口不通,重点检查虚拟机内防火墙(ufw/firewalld)是否放行 22。 - 在虚拟机里跑
ss -tlnp | grep sshd,确认 sshd 监听了0.0.0.0:22而不是只监听127.0.0.1。
大多数超时问题都出在网卡没接好或防火墙没放开,很少是 SSH 本身的故障。
6.2 场景二:REMOTE HOST IDENTIFICATION HAS CHANGED
这个报错最常见于虚拟机被重装或重置之后,主机密钥变了,但宿主机 known_hosts 里还保存着旧指纹。OpenSSH 出于防中间人攻击的考虑,发现指纹不一致就会拒绝连接。
解决办法是删掉旧记录:
bash复制ssh-keygen -R 192.168.20.100
如果你用 Host vm-dev 这种方式连接,注意 -R 后面的地址要和 config 里的 HostName 一致。我见过有人删了 IP 的记录但 config 里写的是主机名,结果还是报错,这里提醒一下。
6.3 场景三:Permission denied (publickey,password)
密码和公钥都被拒绝了,排查链路稍微长一点:
- 先用
ssh -vvv dev@192.168.20.100看详细日志,重点关注Offering public key和Authentications that can continue这两行。 - 检查能否用密码登录。如果密码也失败,多半是
/etc/ssh/sshd_config里把PasswordAuthentication关了且公钥没配置好。可以先临时开启密码认证,登录后检查公钥权限和内容。 - 拉虚拟机里的系统日志确认服务端到底拒绝了什么:
sudo tail -f /var/log/auth.log(Debian/Ubuntu)或journalctl -u sshd -f。 - 确认 config 里
IdentityFile路径与本地私钥实际路径一致,别忘了刚才说的 Windows 私钥权限检查。
6.4 场景四:VSCode 反复重连或卡在 Setting up SSH Host
这类问题建议先打开 VSCode 的输出面板,切到 Remote - SSH 日志,看它内部实际上传了什么。常见原因:
- 虚拟机的
~/.vscode-server权限不对或残留了损坏版本,直接清空重连:rm -rf ~/.vscode-server。 - 虚拟机磁盘满了,导致 server 解压失败。检查
df -h。 - 虚拟机的 Linux 发行版太老,新版本 VSCode Server 要求较高的 glibc。如果系统比较旧,要么升级系统,要么使用匹配旧系统的 VSCode 版本。
6.5 日常开发优化:端口转发与连接保持
远程开发很多时候不只需要命令行,Web 服务也要从宿主机浏览器访问。VSCode Remote-SSH 自带"端口"面板,可以把虚拟机上监听的端口一键转发到本地,浏览器里直接打开 localhost:端口。如果想在终端环境下也保持转发,可以在 SSH config 里写:
code复制LocalForward 8080 localhost:8080
意思是把本地 8080 端口转发到虚拟机视角下的 localhost:8080。这样虚拟机里启动一个监听 8080 的 Web 服务,宿主机浏览器访问 localhost:8080 就能看到,非常实用。
连接保持方面,除了 ServerAliveInterval 60,还可以开启连接复用:
code复制ControlMaster auto
ControlPersist 10m
这样你多个终端窗口连接同一个虚拟机时,只在第一次建立完整 SSH 连接,后续连接直接复用,速度和稳定性都更好。VSCode Remote-SSH 内部本身已有类似机制,但如果你也喜欢在多个终端里用命令行 ssh,这个配置很有价值。
最后再分享一个个人习惯:所有虚拟机的连接信息都会集中写进 ~/.ssh/config,每个虚拟机一个 Host 别名,加上注释说明这台机器装了哪些服务。跑过一段时间之后,你就知道这套组合最大的价值不仅仅是省去来回传文件的麻烦,而是把"本地编辑"和"远程环境"的边界彻底模糊掉,开发体验像在本地一样顺滑。
